|
module Enterprise |
|
module Crypto |
|
# Base class for a Vault, which consists of 0 or more gpg keys. |
|
# Keys can be public, secret, or a paired public & secret. Secret |
|
# keys must me signed by the master key and public keys must be signed |
|
# by their secret key pair, if it exists. |
|
# |
|
# A vault is used for any cryptographic work, such as generating a new |
|
# customer keypair, creating a license or package, and verifying anything |
|
# signed by a gpg key. |
|
# |
|
class Vault |
|
include SafeDir |
|
|
|
MASTER_FINGERPRINT = 'CE502095844D9C760228C545E8D26122D972534D' |
|
|
|
attr_accessor :fingerprint |
|
|
|
# Raise an error unless the key is signed by the signing_key. Useful |
|
# for asserting that a keypair is signed by the master key. |
|
# |
|
def self.verify_key_signature!(key, signing_key) |
|
subkey = signing_key.subkeys.detect {|sk| sk.fingerprint == signing_key.fingerprint } |
|
|
|
unless key.uids.any? {|uid| uid.signatures.any? {|s| s.keyid == subkey.keyid }} |
|
raise(Error, "'#{key.name}' was not not signed by the '#{signing_key.name}' key") |
|
end |
|
end |
|
|
|
# Raise an error unless the key's fingerprint matches the provided |
|
# fingerprint. Useful for asserting a keypair. |
|
# |
|
def self.verify_key_fingerprint!(key, fingerprint) |
|
unless key && key.fingerprint == fingerprint |
|
raise(Error, "'#{key.name}' fingerprint is invalid") |
|
end |
|
end |
|
|
|
# Create a vault with a homedir and 0 or more (paired) keys. |
|
# |
|
# key_data - An Array of gpg key data as Strings. |
|
# |
|
def initialize(*key_data, blank_password: false) |
|
@blank_password = blank_password |
|
@key_data = key_data |
|
end |
|
|
|
# The public key of the Master keypair. Used to verify that other |
|
# keypairs are signed by the Master key. |
|
# |
|
# Note: the private key should never be loaded into this library. |
|
# |
|
def master_key |
|
@master_key ||= find_public_key(MASTER_FINGERPRINT) |
|
end |
|
|
|
# The secret key portion for this vault. May be nil. |
|
# Used for signing data. (e.g. new keypairs, licenses, |
|
# packages.) |
|
# |
|
def secret_key |
|
@secret_key ||= begin |
|
find_private_key(fingerprint) unless fingerprint.nil? |
|
end |
|
end |
|
|
|
# The public key portion for this vault. May be nil. |
|
# Used to verify signed data. |
|
# |
|
def public_key |
|
@public_key ||= begin |
|
find_public_key(fingerprint) unless fingerprint.nil? |
|
end |
|
end |
|
|
|
# Add a new key to the Vault. It must match the fingerprint |
|
# of the existing key(s), if there is one. |
|
# |
|
# key_data - The gpg data of the key as a String. |
|
# |
|
def add_key(key_data) |
|
import_main_key(key_data) |
|
|
|
@key_data << key_data |
|
|
|
fingerprint |
|
end |
|
|
|
# Returns the secret gpg key data as a String. |
|
# |
|
# fingerprint - The fingerprint of the secret key to export. |
|
# |
|
def export_secret(fingerprint) |
|
## GPGME doesn't support exporting secret keys |
|
## http://lists.gnupg.org/pipermail/gnupg-devel/2008-September/024581.html |
|
|
|
command = %w[gpg] |
|
command << "--homedir=#{home_dir}" |
|
command << '--batch' << '--yes' |
|
command << '--armor' |
|
command << "--export-secret-key=#{fingerprint}" |
|
command << "2>/dev/null" |
|
|
|
%x{#{command.join(' ')}} |
|
end |
|
|
|
# Returns the public gpg key data as a String. |
|
# |
|
# fingerprint - The gpg fingerprint of the public key to export. |
|
# |
|
def export_public(fingerprint) |
|
with_context(:armor => true) do |context| |
|
data = context.export(fingerprint) |
|
data.seek(0) |
|
data.read |
|
end |
|
end |
|
|
|
# Symmetrically encrypts then signs the data. |
|
# Note: only signs for now. |
|
# |
|
# raw - A String of data. |
|
# |
|
def encrypt_and_sign(raw) |
|
with_context do |context| |
|
raw_data = GPGME::Data.new(raw) |
|
context.add_signer(*secret_key) |
|
signed_data = context.sign(raw_data) |
|
signed_data.seek(0) |
|
signed_data.read |
|
end |
|
end |
|
|
|
# Symmetrically decrypts and verifies the object. |
|
# Note: only verifies for now. |
|
# |
|
# object - An encrypted and signed object as a String. |
|
# |
|
def decrypt_and_verify(object) |
|
with_context do |context| |
|
signed_data = new_signed_data(object) |
|
|
|
temp_file = Tempfile.new('enterprisecrypto', :encoding => Encoding::BINARY) |
|
raw_data = GPGME::Data.from_io(temp_file) |
|
|
|
context.verify(signed_data, nil, raw_data) |
|
check_signatures(context.verify_result.signatures) |
|
|
|
temp_file.close |
|
|
|
# Return file reference so we don't GC it. |
|
temp_file |
|
end |
|
end |
|
|
|
def new_signed_data(object) |
|
GPGME::Data.new(object) |
|
end |
|
|
|
def check_signatures(signatures) |
|
if signatures.empty? || !signatures.all? {|sig| sig.valid? } |
|
raise Error, "Invalid signature detected!" |
|
end |
|
end |
|
|
|
def find_public_key(fingerprint) |
|
find_key(:public, fingerprint) |
|
end |
|
|
|
def find_secret_key(fingerprint) |
|
find_private_key(fingerprint) |
|
end |
|
|
|
def find_private_key(fingerprint) |
|
find_key(:private, fingerprint) |
|
end |
|
|
|
def find_key(type, fingerprint) |
|
with_context do |context| |
|
context.get_key(fingerprint, type == :private) |
|
end |
|
|
|
rescue EOFError |
|
nil |
|
end |
|
|
|
def with_context(options = {}) |
|
with_vault do |
|
GPGME::Ctx.new(default_context_options.merge(options)) do |ctx| |
|
return yield(ctx) |
|
end |
|
end |
|
rescue GPGME::Error => e |
|
raise Error, e.message |
|
end |
|
|
|
def with_vault |
|
if Crypto.current_vault == self |
|
yield |
|
else |
|
Crypto.with_vault(self) { yield } |
|
end |
|
end |
|
|
|
def default_context_options |
|
{ |
|
:keylist_mode => ( |
|
GPGME::KEYLIST_MODE_SIGS | |
|
GPGME::GPGME_KEYLIST_MODE_SIG_NOTATIONS | |
|
GPGME::GPGME_KEYLIST_MODE_VALIDATE | |
|
GPGME::GPGME_KEYLIST_MODE_LOCAL) |
|
}.merge(blank_password_context_options) |
|
end |
|
|
|
def blank_password_context_options |
|
return {} unless @blank_password |
|
# Check to see if gpg is at least 2.1.12 (needs to include https://dev.gnupg.org/rGeea139c56ef55081d8cd8df2a35ce507386e0f17) |
|
maj, min, patch = GPGME::Engine.info.first.version.split(".").map(&:to_i) |
|
return {} if maj < 2 |
|
return {} if maj == 2 && min < 1 |
|
return {} if maj == 2 && min == 1 && patch < 12 |
|
|
|
{ |
|
:pinentry_mode => GPGME::PINENTRY_MODE_LOOPBACK, # bypass prompt |
|
:password => '' # default to no password |
|
} |
|
end |
|
|
|
def home_dir |
|
@home_dir ||= mktmpdir.to_path |
|
end |
|
|
|
def open! |
|
GPGME::Engine.home_dir = home_dir |
|
write_agent_config |
|
|
|
load_master_public_key |
|
load_vault |
|
|
|
VaultValidator.validate!(self) |
|
end |
|
|
|
def close! |
|
GPGME::Engine.home_dir = nil |
|
end |
|
|
|
def cleanup! |
|
close! |
|
FileUtils.rm_rf home_dir |
|
FileUtils.rm_rf tmpdir |
|
@home_dir = nil |
|
end |
|
|
|
def import_main_key(key_data) |
|
@fingerprint = import_key(key_data) |
|
|
|
if @fingerprint.nil? |
|
@fingerprint = fingerprint |
|
elsif @fingerprint != fingerprint |
|
raise Error, "Can't import #{fingerprint}, doesn't match #{@fingerprint}." |
|
end |
|
end |
|
|
|
def load_vault |
|
@key_data.map do |key| |
|
import_main_key(key) |
|
end |
|
|
|
Vault.verify_key_fingerprint!(secret_key, public_key.fingerprint) unless secret_key.nil? |
|
Vault.verify_key_signature!(public_key, master_key) unless public_key.nil? |
|
end |
|
|
|
def import_key(key_data) |
|
import_results = GPGME::Key.import(key_data) |
|
key = import_results.imports.last |
|
key && key.fingerprint |
|
end |
|
|
|
def load_master_public_key |
|
import_key <<-MASTER_PUBLIC_KEY |
|
-----BEGIN PGP PUBLIC KEY BLOCK----- |
|
|
|
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 |
|
0TFLgOM= |
|
=LdHF |
|
-----END PGP PUBLIC KEY BLOCK----- |
|
MASTER_PUBLIC_KEY |
|
end |
|
|
|
def write_agent_config |
|
return if RUBY_PLATFORM !~ /darwin/ || File.exist?(agent_config_path) |
|
|
|
File.open(agent_config_path, 'w') do |f| |
|
f.puts agent_config |
|
end |
|
end |
|
|
|
def agent_config |
|
# any gpg-agent options should go here |
|
# allow-loopback-pinentry allows the GPGME password option to be set |
|
['allow-loopback-pinentry'] |
|
end |
|
|
|
def agent_config_path |
|
File.join(home_dir, 'gpg-agent.config') |
|
end |
|
end |
|
end |
|
end |
hesitate with caution