|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +module PuppetX |
| 4 | + class NodeEncrypt # rubocop:disable Style/Documentation |
| 5 | + def self.encrypted?(data) |
| 6 | + raise ArgumentError, 'Only strings can be encrypted' unless data.instance_of?(String) |
| 7 | + |
| 8 | + # ridiculously faster than a regex |
| 9 | + data.start_with?('-----BEGIN PKCS7-----') |
| 10 | + end |
| 11 | + |
| 12 | + def self.encrypt(data, destination) |
| 13 | + raise ArgumentError, 'Can only encrypt strings' unless data.instance_of?(String) |
| 14 | + raise ArgumentError, 'Need a node name to encrypt for' unless destination.instance_of?(String) |
| 15 | + |
| 16 | + certpath = Puppet.settings[:hostcert] |
| 17 | + keypath = Puppet.settings[:hostprivkey] |
| 18 | + |
| 19 | + # A dummy password with at least 4 characters is required here |
| 20 | + # since Ruby 2.4 which enforces a minimum password length |
| 21 | + # of 4 bytes. This is true even if the key has no password |
| 22 | + # at all--in which case the password we supply is ignored. |
| 23 | + # We can pass in a dummy here, since we know the certificate |
| 24 | + # has no password. |
| 25 | + key = OpenSSL::PKey::RSA.new(File.read(keypath), '1234') |
| 26 | + cert = OpenSSL::X509::Certificate.new(File.read(certpath)) |
| 27 | + |
| 28 | + # if we're on the CA, we've got a copy of the clientcert from the start. |
| 29 | + # This allows the module to work with no classification at all on single |
| 30 | + # monolithic server setups |
| 31 | + destpath = [ |
| 32 | + "#{Puppet.settings[:signeddir]}/#{destination}.pem", |
| 33 | + "#{Puppet.settings[:certdir]}/#{destination}.pem", |
| 34 | + ].find { |path| File.exist? path } |
| 35 | + |
| 36 | + # for safer upgrades, let's default to the known good pathway for now |
| 37 | + if destpath |
| 38 | + target = OpenSSL::X509::Certificate.new(File.read(destpath)) |
| 39 | + else |
| 40 | + # if we don't have a cert, check for it in $facts |
| 41 | + scope = Puppet.lookup(:global_scope) |
| 42 | + |
| 43 | + if scope.exist?('clientcert_pem') |
| 44 | + hostcert = scope.lookupvar('clientcert_pem') |
| 45 | + target = OpenSSL::X509::Certificate.new(hostcert) |
| 46 | + else |
| 47 | + url = 'https://github.com/puppetlabs/puppetlabs-node_encrypt#automatically-distributing-certificates-to-compile-servers' |
| 48 | + raise ArgumentError, "Client certificate does not exist. See #{url} for more info." |
| 49 | + end |
| 50 | + end |
| 51 | + |
| 52 | + signed = OpenSSL::PKCS7.sign(cert, key, data, [], OpenSSL::PKCS7::BINARY) |
| 53 | + cipher = OpenSSL::Cipher.new('AES-128-CFB') |
| 54 | + |
| 55 | + OpenSSL::PKCS7.encrypt([target], signed.to_der, cipher, OpenSSL::PKCS7::BINARY).to_s |
| 56 | + end |
| 57 | + |
| 58 | + def self.decrypt(data) |
| 59 | + raise ArgumentError, 'Can only decrypt strings' unless data.instance_of?(String) |
| 60 | + |
| 61 | + cert = OpenSSL::X509::Certificate.new(File.read(Puppet.settings[:hostcert])) |
| 62 | + # Same dummy password as above. |
| 63 | + key = OpenSSL::PKey::RSA.new(File.read(Puppet.settings[:hostprivkey]), '1234') |
| 64 | + source = OpenSSL::X509::Certificate.new(File.read(Puppet.settings[:localcacert])) |
| 65 | + |
| 66 | + store = OpenSSL::X509::Store.new |
| 67 | + store.add_cert(source) |
| 68 | + |
| 69 | + blob = OpenSSL::PKCS7.new(data) |
| 70 | + decrypted = blob.decrypt(key, cert) |
| 71 | + verified = OpenSSL::PKCS7.new(decrypted) |
| 72 | + |
| 73 | + raise ArgumentError, 'Signature verification failed' unless verified.verify(nil, store, nil, OpenSSL::PKCS7::NOVERIFY) |
| 74 | + |
| 75 | + verified.data |
| 76 | + end |
| 77 | + end |
| 78 | +end |
0 commit comments