]> gitweb.fluxo.info Git - puppet-stdlib.git/commitdiff
Add a function to validate an x509 RSA key pair
authorMatt Bostock <matt@mattbostock.com>
Mon, 23 Nov 2015 23:45:23 +0000 (23:45 +0000)
committerMatt Bostock <matt@mattbostock.com>
Fri, 8 Jan 2016 11:09:45 +0000 (11:09 +0000)
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
lib/puppet/parser/functions/validate_x509_rsa_key_pair.rb [new file with mode: 0644]
spec/functions/validate_x509_rsa_key_pair_spec.rb [new file with mode: 0755]

index 9ff242cd80db82cd1c5774889c5ddd62bf913eea..e126e5dc82dee57dcb72924bd98976256a4f67aa 100644 (file)
@@ -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 (file)
index 0000000..fc9f23f
--- /dev/null
@@ -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 (executable)
index 0000000..048d65e
--- /dev/null
@@ -0,0 +1,154 @@
+require 'spec_helper'
+
+describe 'validate_x509_rsa_key_pair' do
+
+  let(:valid_cert) do
+  <<EOS
+-----BEGIN CERTIFICATE-----
+MIIC9jCCAeCgAwIBAgIRAK11n3X7aypJ7FPM8UFyAeowCwYJKoZIhvcNAQELMBIx
+EDAOBgNVBAoTB0FjbWUgQ28wHhcNMTUxMTIzMjIzOTU4WhcNMTYxMTIyMjIzOTU4
+WjASMRAwDgYDVQQKEwdBY21lIENvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEAz9bY/piKahD10AiJSfbI2A8NG5UwRz0r9T/WfvNVdhgrsGFgNQjvpUoZ
+nNJpQIHBbgMOiXqfATFjJl5FjEkSf7GUHohlGVls9MX2JmVvknzsiitd75H/EJd+
+N+k915lix8Vqmj8d1CTlbF/8tEjzANI67Vqw5QTuqebO7rkIUvRg6yiRfSo75FK1
+RinCJyl++kmleBwQZBInQyg95GvJ5JTqMzBs67DeeyzskDhTeTePRYVF2NwL8QzY
+htvLIBERTNsyU5i7nkxY5ptUwgFUwd93LH4Q19tPqL5C5RZqXxhE51thOOwafm+a
+W/cRkqYqV+tv+j1jJ3WICyF1JNW0BQIDAQABo0swSTAOBgNVHQ8BAf8EBAMCAKAw
+EwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAUBgNVHREEDTALggls
+b2NhbGhvc3QwCwYJKoZIhvcNAQELA4IBAQAzRo0hpVTrFQZLIXpwvKwZVGvJdCkV
+P95DTsSk/VTGV+/YtxrRqks++hJZnctm2PbnTsCAoIP3AMx+vicCKiKrxvpsLU8/
++6cowUbcuGMdSQktwDqbAgEhQlLsETll06w1D/KC+ejOc4+LRn3GQcEyGDtMk/EX
+IeAvBZHr4/kVXWnfo6kzCLcku1f8yE/yDEFClZe9XV1Lk/s+3YfXVtNnMJJ1giZI
+QVOe6CkmuQq+4AtIeW8aLkvlfp632jag1F77a1y+L268koKkj0hBMrtcErVQaxmq
+xym0+soR4Tk4pTIGckeFglrLxkP2JpM/yTwSEAVlmG9vgTliYKyR0uMl
+-----END CERTIFICATE-----
+EOS
+  end
+
+  let(:valid_key) do
+  <<EOS
+-----BEGIN RSA PRIVATE KEY-----
+MIIEogIBAAKCAQEAz9bY/piKahD10AiJSfbI2A8NG5UwRz0r9T/WfvNVdhgrsGFg
+NQjvpUoZnNJpQIHBbgMOiXqfATFjJl5FjEkSf7GUHohlGVls9MX2JmVvknzsiitd
+75H/EJd+N+k915lix8Vqmj8d1CTlbF/8tEjzANI67Vqw5QTuqebO7rkIUvRg6yiR
+fSo75FK1RinCJyl++kmleBwQZBInQyg95GvJ5JTqMzBs67DeeyzskDhTeTePRYVF
+2NwL8QzYhtvLIBERTNsyU5i7nkxY5ptUwgFUwd93LH4Q19tPqL5C5RZqXxhE51th
+OOwafm+aW/cRkqYqV+tv+j1jJ3WICyF1JNW0BQIDAQABAoIBADAiZ/r+xP+vkd5u
+O61/lCBFzBlZQecdybJw6HJaVK6XBndA9hESUr4LHUdui6W+51ddKd65IV4bXAUk
+zCKjQb+FFvLDT/bA+TTvLATUdTSN7hJJ3OWBAHuNOlQklof6JCB0Hi4+89+P8/pX
+eKUgR/cmuTMDT/iaXdPHeqFbBQyA1ZpQFRjN5LyyJMS/9FkywuNc5wlpsArtc51T
+gIKENUZCuPhosR+kMFc2iuTNvqZWPhvouSrmhi2O6nSqV+oy0+irlqSpCF2GsCI8
+72TtLpq94Grrq0BEH5avouV+Lp4k83vO65OKCQKUFQlxz3Xkxm2U3J7KzxqnRtM3
+/b+cJ/kCgYEA6/yOnaEYhH/7ijhZbPn8RujXZ5VGJXKJqIuaPiHMmHVS5p1j6Bah
+2PcnqJA2IlLs3UloN+ziAxAIH6KCBiwlQ/uPBNMMaJsIjPNBEy8axjndKhKUpidg
+R0OJ7RQqMShOJ8akrSfWdPtXC/GBuwCYE//t77GgZaIMO3FcT9EKA48CgYEA4Xcx
+Fia0Jg9iyAhNmUOXI6hWcGENavMx01+x7XFhbnMjIKTZevFfTnTkrX6HyLXyGtMU
+gHOn+k4PE/purI4ARrKO8m5wYEKqSIt4dBMTkIXXirfQjXgfjR8E4T/aPe5fOFZo
+7OYuxLRtzmG1C2sW4txwKAKX1LaWcVx/RLSttSsCgYBbcj8Brk+F6OJcqYFdzXGJ
+OOlf5mSMVlopyg83THmwCqbZXtw8L6kAHqZrl5airmfDSJLuOQlMDoZXW+3u3mSC
+d5TwVahVUN57YDgzaumBLyMZDqIz0MZqVy23hTzkV64Rk9R0lR9xrYQJyMhw4sYL
+2f0mCTsSpzz+O+t9so+i2QKBgEC38gMlwPhb2kMI/x1LZYr6uzUu5qcYf+jowy4h
+KZKGwkKQj0zXFEB1FV8nvtpCP+irRmtIx6L13SYi8LnfWPzyLE4ynVdES5TfVAgd
+obQOdzx+XwL8xDHCAaiWp5K3ZeXKB/xYZnxYPlzLdyh76Ond1OPnOqX4c16+6llS
+c7pZAoGATd9NckT0XtXLEsF3IraDivq8dP6bccX2DNfS8UeEvRRrRwpFpSRrmuGb
+jbG4yzoIX4RjQfj/z48hwhJB+cKiN9WwcPsFXtHe7v3F6BRwK0JUfrCiXad8/SGZ
+KAf7Dfqi608zBdnPWHacre2Y35gPHB00nFQOLS6u46aBNSq07YA=
+-----END RSA PRIVATE KEY-----
+EOS
+  end
+
+  let(:another_valid_key) do
+  <<EOS
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAoISxYJBTPAeAzFnm+lE/ljLlmGal2Xr3vwZKkvJiuKA/m4QJ
+0ZNdtkBSDOVuG2dXVv6W4sChRtsCdvuVe7bjTYvlU8TWM3VEJDL9l9cRXScxxlKQ
+Xwb35y1yV35NJfaK/jzm9KcErtQQs1RxvGlWRaohmLM8uQcuhjZfMsSlQoHQD5LX
+sbPtk82RPyxYc1dj2vsaoi1VvuP2+jv4xLQOmNJY1bT5GTurqiltmxEtWhNNmGg0
+2wtK00ifqLVO5HNc3gXQCDM2M99Sbmn1YtbrgsU9xMYfcPmvQvb+YoKskyoqck+c
+HR//hi7vslbxABrny15LBkEfRc4TickphSGYXwIDAQABAoIBAATEzGw8/WwMIQRx
+K06GeWgh7PZBHm4+m/ud2TtSXiJ0CE+7dXs3cJJIiOd/LW08/bhE6gCkjmYHfaRB
+Ryicv1X/cPmzIFX5BuQ4a5ZGOmrVDkKBE27vSxAgJoR46RvWnjx9XLMp/xaekDxz
+psldK8X4DvV1ZbltgDFWji947hvyqUtHdKnkQnc5j7aCIFJf9GMfzaeeDPMaL8WF
+mVL4iy9EAOjNOHBshZj/OHyU5FbJ8ROwZQlCOiLCdFegftSIXt8EYDnjB3BdsALH
+N6hquqrD7xDKyRbTD0K7lqxUubuMwTQpi61jZD8TBTXEPyFVAnoMpXkc0Y+np40A
+YiIsR+kCgYEAyrc4Bh6fb9gt49IXGXOSRZ5i5+TmJho4kzIONrJ7Ndclwx9wzHfh
+eGBodWaw5CxxQGMf4vEiaZrpAiSFeDffBLR+Wa2TFE5aWkdYkR34maDjO00m4PE1
+S+YsZoGw7rGmmj+KS4qv2T26FEHtUI+F31RC1FPohLsQ22Jbn1ORipsCgYEAyrYB
+J2Ncf2DlX1C0GfxyUHQOTNl0V5gpGvpbZ0WmWksumYz2kSGOAJkxuDKd9mKVlAcz
+czmN+OOetuHTNqds2JJKKJy6hJbgCdd9aho3dId5Xs4oh4YwuFQiG8R/bJZfTlXo
+99Qr02L7MmDWYLmrR3BA/93UPeorHPtjqSaYU40CgYEAtmGfWwokIglaSDVVqQVs
+3YwBqmcrla5TpkMLvLRZ2/fktqfL4Xod9iKu+Klajv9ZKTfFkXWno2HHL7FSD/Yc
+hWwqnV5oDIXuDnlQOse/SeERb+IbD5iUfePpoJQgbrCQlwiB0TNGwOojR2SFMczf
+Ai4aLlQLx5dSND9K9Y7HS+8CgYEAixlHQ2r4LuQjoTs0ytwi6TgqE+vn3K+qDTwc
+eoods7oBWRaUn1RCKAD3UClToZ1WfMRQNtIYrOAsqdveXpOWqioAP0wE5TTOuZIo
+GiWxRgIsc7TNtOmNBv+chCdbNP0emxdyjJUIGb7DFnfCw47EjHnn8Guc13uXaATN
+B2ZXgoUCgYAGa13P0ggUf5BMJpBd8S08jKRyvZb1CDXcUCuGtk2yEx45ern9U5WY
+zJ13E5z9MKKO8nkGBqrRfjJa8Xhxk4HKNFuzHEet5lvNE7IKCF4YQRb0ZBhnb/78
++4ZKjFki1RrWRNSw9TdvrK6qaDKgTtCTtfRVXAYQXUgq7lSFOTtL3A==
+-----END RSA PRIVATE KEY-----
+EOS
+  end
+
+  let(:valid_cert_but_indented) do
+    valid_cert.gsub(/^/, '  ')
+  end
+
+  let(:valid_key_but_indented) do
+    valid_key.gsub(/^/, '  ')
+  end
+
+  let(:bad_cert) do
+    'foo'
+  end
+
+  let(:bad_key) do
+    'bar'
+  end
+
+  context 'function signature validation' do
+    it { is_expected.not_to eq(nil) }
+    it { is_expected.to run.with_params().and_raise_error(Puppet::ParseError, /wrong number of arguments/i) }
+    it { is_expected.to run.with_params(0, 1, 2, 3).and_raise_error(Puppet::ParseError, /wrong number of arguments/i) }
+  end
+
+  context 'valid input' do
+    describe 'valid certificate and key' do
+      it { is_expected.to run.with_params(valid_cert, valid_key) }
+    end
+  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
+
+    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 '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
+
+    describe 'validate certificate and key; certficate not signed by key' do
+      it { is_expected.to run.with_params(valid_cert, another_valid_key).and_raise_error(Puppet::ParseError, /Certificate signature does not match supplied key/) }
+    end
+
+    describe 'valid cert and key but arguments in wrong order' do
+      it { is_expected.to run.with_params(valid_key, valid_cert).and_raise_error(Puppet::ParseError, /Not a valid x509 certificate/) }
+    end
+
+    describe 'non-string arguments' do
+      it { is_expected.to run.with_params({}, {}).and_raise_error(Puppet::ParseError, /is not a string/) }
+      it { is_expected.to run.with_params(1, 1).and_raise_error(Puppet::ParseError, /is not a string/) }
+      it { is_expected.to run.with_params(true, true).and_raise_error(Puppet::ParseError, /is not a string/) }
+      it { is_expected.to run.with_params("foo", {}).and_raise_error(Puppet::ParseError, /is not a string/) }
+      it { is_expected.to run.with_params(1, "bar").and_raise_error(Puppet::ParseError, /is not a string/) }
+      it { is_expected.to run.with_params("baz", true).and_raise_error(Puppet::ParseError, /is not a string/) }
+    end
+  end
+end