From 97320ab42121a10b76c642b8378c82a888148e4b Mon Sep 17 00:00:00 2001 From: Matt Bostock Date: Mon, 23 Nov 2015 23:45:23 +0000 Subject: [PATCH 1/3] Add a function to validate an x509 RSA key pair Add a function to validate an x509 RSA certificate and key pair, as commonly used for TLS certificates. The rationale behind this is that we store our TLS certificates and private keys in Hiera YAML files, and poor indentation or formatting in the YAML file could cause a valid certificate to be considered invalid. Will cause the Puppet run to fail if: - an invalid certificate is detected - an invalid RSA key is detected - the certificate does not match the key, i.e. the certificate has not been signed by the supplied key The test certificates I've used in the spec tests were generated using the Go standard library: $ go run $GOROOT/src/crypto/tls/generate_cert.go -host localhost Example output: ==> cache-1.router: Error: Not a valid RSA key: Neither PUB key nor PRIV key:: nested asn1 error at /var/govuk/puppet/modules/nginx/manifests/config/ssl.pp:30 on node cache-1.router.dev.gov.uk --- README.markdown | 16 ++ .../functions/validate_x509_rsa_key_pair.rb | 47 ++++++ .../validate_x509_rsa_key_pair_spec.rb | 154 ++++++++++++++++++ 3 files changed, 217 insertions(+) create mode 100644 lib/puppet/parser/functions/validate_x509_rsa_key_pair.rb create mode 100755 spec/functions/validate_x509_rsa_key_pair_spec.rb diff --git a/README.markdown b/README.markdown index 9ff242c..e126e5d 100644 --- a/README.markdown +++ b/README.markdown @@ -1182,6 +1182,22 @@ Instead, use: *Type*: statement. +#### `validate_x509_rsa_key_pair` + +Validates a PEM-formatted X.509 certificate and private key using OpenSSL. +Verifies that the certficate's signature was created from the supplied key. + +Fails catalog compilation if any value fails this check. + +Takes two arguments, the first argument must be a X.509 certificate and the +second must be an RSA private key: + + ~~~ + validate_x509_rsa_key_pair($cert, $key) + ~~~ + +*Type*: statement. + #### `values` Returns the values of a given hash. For example, given `$hash = {'a'=1, 'b'=2, 'c'=3} values($hash)` returns [1,2,3]. diff --git a/lib/puppet/parser/functions/validate_x509_rsa_key_pair.rb b/lib/puppet/parser/functions/validate_x509_rsa_key_pair.rb new file mode 100644 index 0000000..fc9f23f --- /dev/null +++ b/lib/puppet/parser/functions/validate_x509_rsa_key_pair.rb @@ -0,0 +1,47 @@ +module Puppet::Parser::Functions + + newfunction(:validate_x509_rsa_key_pair, :doc => <<-ENDHEREDOC + Validates a PEM-formatted X.509 certificate and RSA private key using + OpenSSL. Verifies that the certficate's signature was created from the + supplied key. + + Fail compilation if any value fails this check. + + validate_x509_rsa_key_pair($cert, $key) + + ENDHEREDOC + ) do |args| + + require 'openssl' + + NUM_ARGS = 2 unless defined? NUM_ARGS + + unless args.length == NUM_ARGS then + raise Puppet::ParseError, + ("validate_x509_rsa_key_pair(): wrong number of arguments (#{args.length}; must be #{NUM_ARGS})") + end + + args.each do |arg| + unless arg.is_a?(String) + raise Puppet::ParseError, "#{arg.inspect} is not a string." + end + end + + begin + cert = OpenSSL::X509::Certificate.new(args[0]) + rescue OpenSSL::X509::CertificateError => e + raise Puppet::ParseError, "Not a valid x509 certificate: #{e}" + end + + begin + key = OpenSSL::PKey::RSA.new(args[1]) + rescue OpenSSL::PKey::RSAError => e + raise Puppet::ParseError, "Not a valid RSA key: #{e}" + end + + unless cert.verify(key) + raise Puppet::ParseError, "Certificate signature does not match supplied key" + end + end + +end diff --git a/spec/functions/validate_x509_rsa_key_pair_spec.rb b/spec/functions/validate_x509_rsa_key_pair_spec.rb new file mode 100755 index 0000000..048d65e --- /dev/null +++ b/spec/functions/validate_x509_rsa_key_pair_spec.rb @@ -0,0 +1,154 @@ +require 'spec_helper' + +describe 'validate_x509_rsa_key_pair' do + + let(:valid_cert) do + < Date: Fri, 8 Jan 2016 11:01:51 +0000 Subject: [PATCH 2/3] Test certificate and key with a truncated middle Test a valid certificate and valid key that have had 48 characters removed from their middle, to simulate a malformed certificate and key. Suggested by @DavidS in https://github.com/puppetlabs/puppetlabs-stdlib/pull/552 --- .../validate_x509_rsa_key_pair_spec.rb | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/spec/functions/validate_x509_rsa_key_pair_spec.rb b/spec/functions/validate_x509_rsa_key_pair_spec.rb index 048d65e..bfa7e38 100755 --- a/spec/functions/validate_x509_rsa_key_pair_spec.rb +++ b/spec/functions/validate_x509_rsa_key_pair_spec.rb @@ -97,6 +97,14 @@ EOS valid_key.gsub(/^/, ' ') end + let(:malformed_cert) do + truncate_middle(valid_cert) + end + + let(:malformed_key) do + truncate_middle(valid_key) + end + let(:bad_cert) do 'foo' end @@ -126,6 +134,14 @@ EOS it { is_expected.to run.with_params(valid_cert, valid_key_but_indented).and_raise_error(Puppet::ParseError, /Not a valid RSA key/) } end + describe 'valid certificate, malformed key' do + it { is_expected.to run.with_params(valid_cert, malformed_key).and_raise_error(Puppet::ParseError, /Not a valid RSA key/) } + end + + describe 'malformed certificate, valid key' do + it { is_expected.to run.with_params(malformed_cert, valid_key).and_raise_error(Puppet::ParseError, /Not a valid x509 certificate/) } + end + describe 'valid certificate, bad key' do it { is_expected.to run.with_params(valid_cert, bad_key).and_raise_error(Puppet::ParseError, /Not a valid RSA key/) } end @@ -151,4 +167,14 @@ EOS it { is_expected.to run.with_params("baz", true).and_raise_error(Puppet::ParseError, /is not a string/) } end end + + def truncate_middle(string) + chars_to_truncate = 48 + middle = (string.length / 2).floor + start_pos = middle - (chars_to_truncate / 2) + end_pos = middle + (chars_to_truncate / 2) + + string[start_pos...end_pos] = '' + return string + end end From 41f9319bbd96547f9c2226524918e4b748527048 Mon Sep 17 00:00:00 2001 From: Matt Bostock Date: Fri, 8 Jan 2016 11:06:57 +0000 Subject: [PATCH 3/3] Change order of tests to be more logical Put the tests using a valid certificate fixture together and put tests using a valid key fixture together. --- .../functions/validate_x509_rsa_key_pair_spec.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/spec/functions/validate_x509_rsa_key_pair_spec.rb b/spec/functions/validate_x509_rsa_key_pair_spec.rb index bfa7e38..eb63310 100755 --- a/spec/functions/validate_x509_rsa_key_pair_spec.rb +++ b/spec/functions/validate_x509_rsa_key_pair_spec.rb @@ -126,10 +126,6 @@ EOS end context 'bad input' do - describe 'valid but indented certificate, valid key' do - it { is_expected.to run.with_params(valid_cert_but_indented, valid_key).and_raise_error(Puppet::ParseError, /Not a valid x509 certificate/) } - end - describe 'valid certificate, valid but indented key' do it { is_expected.to run.with_params(valid_cert, valid_key_but_indented).and_raise_error(Puppet::ParseError, /Not a valid RSA key/) } end @@ -138,14 +134,18 @@ EOS it { is_expected.to run.with_params(valid_cert, malformed_key).and_raise_error(Puppet::ParseError, /Not a valid RSA key/) } end - describe 'malformed certificate, valid key' do - it { is_expected.to run.with_params(malformed_cert, valid_key).and_raise_error(Puppet::ParseError, /Not a valid x509 certificate/) } - end - describe 'valid certificate, bad key' do it { is_expected.to run.with_params(valid_cert, bad_key).and_raise_error(Puppet::ParseError, /Not a valid RSA key/) } end + describe 'valid but indented certificate, valid key' do + it { is_expected.to run.with_params(valid_cert_but_indented, valid_key).and_raise_error(Puppet::ParseError, /Not a valid x509 certificate/) } + end + + describe 'malformed certificate, valid key' do + it { is_expected.to run.with_params(malformed_cert, valid_key).and_raise_error(Puppet::ParseError, /Not a valid x509 certificate/) } + end + describe 'bad certificate, valid key' do it { is_expected.to run.with_params(bad_cert, valid_key).and_raise_error(Puppet::ParseError, /Not a valid x509 certificate/) } end