From f66f0b3a180d28fa6a2dcad63e1a8668c3e0331b Mon Sep 17 00:00:00 2001 From: Richard Nixon Date: Wed, 15 Mar 2017 23:57:31 +0000 Subject: [PATCH 1/3] Initial support for x509_certificate and rsa_key * Includes unit tests * Includes 2 new resources * Includes documentation Signed-off-by: Richard Nixon --- docs/resources/rsa_key.md | 41 +++++ docs/resources/x509_certificate.md | 161 ++++++++++++++++++ lib/inspec/resource.rb | 2 + lib/resources/rsa_key.rb | 57 +++++++ lib/resources/x509_certificate.rb | 133 +++++++++++++++ test/helper.rb | 4 + test/unit/mock/files/test_ca_public.key.pem | 9 + .../mock/files/test_certificate.rsa.crt.pem | 24 +++ .../mock/files/test_certificate.rsa.key.pem | 27 +++ test/unit/resources/rsa_key_test.rb | 19 +++ test/unit/resources/x509_certificate_test.rb | 92 ++++++++++ 11 files changed, 569 insertions(+) create mode 100644 docs/resources/rsa_key.md create mode 100644 docs/resources/x509_certificate.md create mode 100644 lib/resources/rsa_key.rb create mode 100644 lib/resources/x509_certificate.rb create mode 100644 test/unit/mock/files/test_ca_public.key.pem create mode 100644 test/unit/mock/files/test_certificate.rsa.crt.pem create mode 100644 test/unit/mock/files/test_certificate.rsa.key.pem create mode 100644 test/unit/resources/rsa_key_test.rb create mode 100644 test/unit/resources/x509_certificate_test.rb diff --git a/docs/resources/rsa_key.md b/docs/resources/rsa_key.md new file mode 100644 index 000000000..509ebfffc --- /dev/null +++ b/docs/resources/rsa_key.md @@ -0,0 +1,41 @@ +--- +title: The rsa_key Resource +--- + +# rsa_key + +Use the `rsa_key` InSpec audit resource to test RSA public/private keypairs. + +This resource is mainly useful when used in conjunction with the x509_certificate resource but it can also be used for checking SSH keys. + + +## Syntax + +An `rsa_key` resource block declares a `key file` to be tested. + + describe rsa_key('mycertificate.key') do + its('public_key') { should match "-----BEGIN PUBLIC KEY-----\n3597459df9f3982" } + its('key_length') { should eq 2048 } + end + +## Supported Properties + +### public_key (String) + +The `public_key` property returns the public part of the RSA keypair + + describe rsa_key('/etc/pki/www.mywebsite.com.key') do + its('public_key') { should match "-----BEGIN PUBLIC KEY-----\n3597459df9f3982......" } + end + +### private_key (String) + +See the `public key` property + +### key_length + +The key_length` property allows testing the number of bits in the keypair. + + describe rsa_key('/etc/pki/www.mywebsite.com.key') do + its('key_length') { should eq 2048 } + end diff --git a/docs/resources/x509_certificate.md b/docs/resources/x509_certificate.md new file mode 100644 index 000000000..3aabb4972 --- /dev/null +++ b/docs/resources/x509_certificate.md @@ -0,0 +1,161 @@ +--- +title: The x509_certificate Resource +--- + +# x509_certificate + +Use the `x509_certificate` InSpec audit resource to test the fields and validity of an x.509 certificate. + +X.509 certificates use public/private key pairs to sign and encrypt documents +or communications over a network. They may also be used for authentication. + +Examples include SSL certificates, S/MIME certificates and VPN authentication +certificates. + +## Syntax + +An `x509_certificate` resource block declares a certificate `key file` to be tested. + + describe x509_certificate('mycertificate.pem') do + its('days_remaining') { should be > 30 } + end + +It can optionally specify a private key file and a ca public key file for key verification + + describe x509_certificate('mycertificate.cert','mycertificate.key','ca_key.pub') do + its('private_key_matches?') { should be true } + its('ca_key_matches?') { should be true } + end + +## Supported Properties + +### subject (String) + +The `subject` string contains several fields seperated by forward slashes. The +field identifiers are the same ones used by OpenSSL to generate CSR's and certs. + +e.g. `/C=US/L=Seattle/O=Chef Software Inc/OU=Chefs/CN=Richard Nixon` + + describe x509_certificate('/etc/pki/www.mywebsite.com.pem') do + its('subject') { should match "CN=www.mywebsite.com" } + end + +### parsed_subject.XX + +`parsed_subject` property makes it easier to access individual subject elements. + + describe x509_certificate('/etc/pki/www.mywebsite.com.pem') do + its('parsed_subject.CN') { should eq "www.mywebsite.com" } + end + +### issuer (String) + +The `issuer` string is copied from a CA (certificate authority) during the +certificate signing process. It describes which authority is guaranteeing the +identity of our certificate. + +e.g. `/C=US/L=Seattle/CN=Acme Trust CA/emailAddress=support@acmetrust.org` + + describe x509_certificate('/etc/pki/www.mywebsite.com.pem') do + its('issuer') { should match "CN=Acme Trust CA" } + end + +### parsed_issuer.XX + +`parsed_issuer` makes it easier to access individual issuer elements. + + describe x509_certificate('/etc/pki/www.mywebsite.com.pem') do + its('parsed_issuer.CN') { should eq "Acme Trust CA" } + end + +### public_key (String) + +The `public_key` property returns a base64 encoded public key in PEM format. + + describe x509_certificate('/etc/pki/www.mywebsite.com.pem') do + its('public_key') { should match "-----BEGIN PUBLIC KEY-----\nblah blah blah..." } + end + +### key_length (Integer) + +The `key_length` property calculates the number of bits in the public key. +More bits increase security, but at the cost of speed and in extreme cases, compatibility. + + describe x509_certificate('mycert.pem') do + its('key_length') { should be 2048 } + end + +### signature_algorithm (String) + +The `signature_algorithm` property describes which hash function was used by the CA to +sign the certificate. + + describe x509_certificate('/etc/pki/www.mywebsite.com.pem') do + its('signature_algorithm') { should be 'sha256WithRSAEncryption' } + end + +### private_key_matches? (Boolean) ca_key_matches? (Boolean) + +The `private_key_matches?` and `ca_key_matches?` methods check + +* If the supplied private key matches the certificate +* If the CA public key belongs to the CA that signed the certificate + + + describe x509_certificate('mycertificate.cert','mycertificate.key','ca_key.pub') do + its('private_key_matches?') { should be true } + its('ca_key_matches?') { should be true } + end + + +### days_remaining (Float) + +The `days_remaining` property can be used to check that certificates are not in +danger of expiring soon. + + describe x509_certificate('/etc/pki/www.mywebsite.com.pem') do + its('days_remaining') { should be > 30 } + end + +### not_before and not_after (Time) + +The `not_before` and `not_after` properties expose the start and end dates of certificate +validity. They are exposed as ruby Time class so that date arithmetic can be easily performed. + + describe x509_certificate('/etc/pki/www.mywebsite.com.pem') do + its('not_before') { should be <= Time.utc.now } + its('not_after') { should be >= Time.utc.now } + end + +### serial (Integer) + +The `serial` property exposes the serial number of the certificate. The serial number is set by the CA during the signing process and should be unique within that CA. + +### version (Integer) + +The `version` property exposes the certificate version. + +### extensions (Hash) + +The `extensions` hash property is mainly used to determine what the certificate can be used for. + + describe x509_certificate('/etc/pki/www.mywebsite.com.pem') do + its('extensions').length) {should eq 3 } + + # Check what extension categories we have + its('extensions')) { should include 'keyUsage' } + its('extensions')) { should include 'extendedKeyUsage' } + its('extensions')) { should include 'subjectAltName' } + + # Check examples of basic 'keyUsage' + its('extensions')['keyUsage']) { should include "Digital Signature" } + its('extensions')['keyUsage']).must_include "Non Repudiation" + its('extensions')['keyUsage']).must_include "Data Encipherment" + + # Check examples of newer 'extendedKeyUsage' + its('extensions')['extendedKeyUsage']) { should include "TLS Web Server Authentication" } + its('extensions')['extendedKeyUsage']) { should include "Code Signing" } + + # Check examples of 'subjectAltName' + its('extensions')['subjectAltName']) { should include "email:support@chef.io" } + end diff --git a/lib/inspec/resource.rb b/lib/inspec/resource.rb index 4163f5a09..0057c5a8b 100644 --- a/lib/inspec/resource.rb +++ b/lib/inspec/resource.rb @@ -119,6 +119,7 @@ require 'resources/postgres_session' require 'resources/powershell' require 'resources/processes' require 'resources/registry_key' +require 'resources/rsa_key' require 'resources/security_policy' require 'resources/service' require 'resources/shadow' @@ -131,6 +132,7 @@ require 'resources/windows_feature' require 'resources/windows_task' require 'resources/xinetd' require 'resources/wmi' +require 'resources/x509_certificate' require 'resources/yum' require 'resources/zfs_dataset' require 'resources/zfs_pool' diff --git a/lib/resources/rsa_key.rb b/lib/resources/rsa_key.rb new file mode 100644 index 000000000..dfebe45f0 --- /dev/null +++ b/lib/resources/rsa_key.rb @@ -0,0 +1,57 @@ +# encoding: utf-8 +# author: Richard Nixon + +require 'openssl' +require 'hashie/mash' + +module Inspec::Resources + class RsaKey < Inspec.resource(1) + name 'rsa_key' + desc 'Used to test RSA keys' + example " + describe rsa_key('/etc/pki/www.mywebsite.com.key') do + its('public_key') { should match /BEGIN RSA PUBLIC KEY/ } + end + " + def initialize(keypath) + @keypath = keypath + @keyfile = inspec.backend.file(@keypath) + if @keyfile.exist? + load_key + else + @key = RuntimeError.new('Key file not found') + end + end + + private def load_key + @keyraw ||= @keyfile.content + @key ||= OpenSSL::PKey::RSA.new(@keyraw) + rescue OpenSSL::X509::RSAError => error_code + @key ||= error_code + end + + def public_key + @key.public_key.to_s + end + + def private_key + @key.to_s + end + + def key_length + n.num_bytes * 8 # Answer in bits + end + + def exist? + @keyfile.exist? + end + + def key_length + @key.public_key.n.num_bytes * 8 + end + + def to_s + "rsa_key #{@keypath}" + end + end +end diff --git a/lib/resources/x509_certificate.rb b/lib/resources/x509_certificate.rb new file mode 100644 index 000000000..b4c2f8be9 --- /dev/null +++ b/lib/resources/x509_certificate.rb @@ -0,0 +1,133 @@ +# encoding: utf-8 +# author: Richard Nixon + +require 'openssl' +require 'hashie/mash' + +module Inspec::Resources + class X509CertificateResource < Inspec.resource(1) + name 'x509_certificate' + desc 'Used to test x.509 certificates' + example " + describe x509_certificate('/etc/pki/www.mywebsite.com.pem') do + its('subject') { should match /CN=My Website/ } + its('days_remaining') { should be > 30 } + end + " + + def initialize(certpath,private_keypath=nil,ca_public_keypath=nil) + @certpath = certpath + @issuer = nil + @parsed_subject = nil + @parsed_issuer = nil + @extensions = nil + @cert = cert_from_file(certpath) + @key = key_from_file(private_keypath) + @cakey = key_from_file(ca_public_keypath) + end + + private def cert_from_file(certpath) + certfile = inspec.backend.file(certpath) + if certfile.exist? + certraw = certfile.content + certcooked = OpenSSL::X509::Certificate.new(certraw) + else + certcooked = RuntimeError.new("Certificate #{certpath} not found") + end + certcooked + rescue OpenSSL::X509::CertificateError => error_code + error_code + end + + private def key_from_file(keypath) + keyfile = inspec.backend.file(keypath) + if keyfile.exist? + keyraw = keyfile.content + keycooked = OpenSSL::PKey::RSA.new(keyraw) + else + keycooked = RuntimeError.new("Keyfile #{keypath} not found") + end + keycooked + rescue OpenSSL::PKey::RSAError => error_code + error_code + end + + + # Forward these methods directly to OpenSSL::X509::Certificate instance + %w{serial version not_before not_after signature_algorithm public_key }.each do |m| + define_method m.to_sym do |*args| + @cert.method(m.to_sym).call(*args) + end + end + + def exist? + @certfile.exist? + end + + def private_key_matches? + @cert.check_private_key(@key) + end + + def ca_key_matches? + @cert.verify(@cakey) + end + + def subject + @cert.subject.to_s + end + + def parsed_subject + # Return cached subject if we have already parsed it + return @parsed_subject if @parsed_subject + # Use a Mash to make it easier to access hash elements in "its('subject') {should ...}" + @parsed_subject = Hashie::Mash.new(Hash[@cert.subject.to_a.map { |k, v, _| [k, v] }]) + end + + def issuer + @cert.issuer.to_s + end + + def parsed_issuer + # Return cached subject if we have already parsed it + return @parsed_issuer if @parsed_issuer + # Use a Mash to make it easier to access hash elements in "its('issuer') {should ...}" + @parsed_issuer = Hashie::Mash.new(Hash[@cert.issuer.to_a.map { |k, v, _| [k, v] }]) + end + + def key_length + @cert.public_key.n.num_bytes * 8 + end + + def days_remaining + (@cert.not_after - Time.now.utc) / 86400 + end + + def extensions + # Return cached Mash if we already parsed the certificate extensions + return @extensions if @extensions + # Return the exception class if we failed to instantiate a Cert from file + return @cert unless @cert.respond_to? :extensions + # Use a Mash to make it easier to access hash elements in "its('entensions') {should ...}" + @extensions = Hashie::Mash.new({}) + # Make sure standard extensions exist so we don't get nil for nil:NilClass + # when the user tests for extensions which aren't present + %w{ + keyUsage extendedKeyUsage basicConstraints subjectKeyIdentifier + authorityKeyIdentifier subjectAltName issuerAltName authorityInfoAccess + crlDistributionPoints issuingDistributionPoint certificatePolicies + policyConstraints nameConstraints noCheck tlsfeature nsComment + }.each { |extension| @extensions[extension] ||= [] } + # Now parse the extensions into the Mash + extension_array = @cert.extensions.map { |e| e.to_s } + extension_array.each do |extension| + kv = extension.split(/ *= */, 2) + @extensions[kv.first] = kv.last.split(/ *, */) + end + @extensions + end + + def to_s + "x509_certificate #{@certpath}" + end + end +end diff --git a/test/helper.rb b/test/helper.rb index a92577961..37179c8f4 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -134,6 +134,10 @@ class MockLoader '/etc/xinetd.d/echo' => mockfile.call('xinetd.d_echo'), '/etc/sysctl.conf' => mockfile.call('sysctl.conf'), '/etc/postgresql/9.4/main/postgresql.conf' => mockfile.call('postgresql.conf'), + # Test certificate/key for x509_certificate using RSA keys in PEM format + 'test_certificate.rsa.crt.pem' => mockfile.call('test_certificate.rsa.crt.pem'), + 'test_certificate.rsa.key.pem' => mockfile.call('test_certificate.rsa.key.pem'), + 'test_ca_public.key.pem' => mockfile.call('test_ca_public.key.pem'), } # create all mock commands diff --git a/test/unit/mock/files/test_ca_public.key.pem b/test/unit/mock/files/test_ca_public.key.pem new file mode 100644 index 000000000..a7a48034f --- /dev/null +++ b/test/unit/mock/files/test_ca_public.key.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm2Ceiaf2E+xnv2f0IHl8 +SAd9j0CK+zuA1BbMStBaslkAD+XHTZ5vvC0NKyiuEFu/WpPegOavydpMaBD+l01Z +c3pHZxj9Ayl6uDCv27SKYTYmoiRybb1wFGMBuzb8Q2hseJH+XCx8rd1Kjn1VfAeC +5n36QoMJ8qghdnTSFSqVMP8IAz8hI0xKe89FRf5YbqLhYUmk+PMili0XxkwG723D +NXF62spg4runWmk//nsneGB5gqKLEHKxlgFDTYOrG2SgAcVtz5urAI+o24ikzhiN +nlZ5fmHKbpxn0wEHxVqQWieAx1gj5BLk8TUDZUdkWla8q9mJ3ZhxXm3H7OJ9vI65 +gwIDAQAB +-----END PUBLIC KEY----- diff --git a/test/unit/mock/files/test_certificate.rsa.crt.pem b/test/unit/mock/files/test_certificate.rsa.crt.pem new file mode 100644 index 000000000..bc2bc68b7 --- /dev/null +++ b/test/unit/mock/files/test_certificate.rsa.crt.pem @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEGjCCAwKgAwIBAgIBJTANBgkqhkiG9w0BAQsFADCBljEXMBUGA1UEAwwOSW5z +cGVjIFRlc3QgQ0ExGjAYBgNVBAoMEUNoZWYgU29mdHdhcmUgSW5jMRMwEQYDVQQL +DApDZXJ0aWZpZXJzMQswCQYDVQQIDAJXQTELMAkGA1UEBhMCVVMxEDAOBgNVBAcM +B1NlYXR0bGUxHjAcBgkqhkiG9w0BCQEWD3N1cHBvcnRAY2hlZi5pbzAeFw0xNzAz +MDEwMTI4NTdaFw0xODAzMDEwMTI4NTdaMIGjMSAwHgYDVQQDDBdJbnNwZWMgVGVz +dCBDZXJ0aWZpY2F0ZTEaMBgGA1UECgwRQ2hlZiBTb2Z0d2FyZSBJbmMxFzAVBgNV +BAsMDkluc3BlYyBUZXN0ZXJzMQswCQYDVQQIDAJXQTELMAkGA1UEBhMCVVMxEDAO +BgNVBAcMB1NlYXR0bGUxHjAcBgkqhkiG9w0BCQEWD3N1cHBvcnRAY2hlZi5pbzCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMYtU6eHT0PhlPkYqbILlFk+ +dGun7AQqXvkE9GHVv+xhupMp0c0fn80NKtSw8aYKgB/ngyFhABWKEz3PIWgI2bwG +J+yZV1EKXqCu33NgT4GANJa99xHtB/Z6nmZZ8rUPvxFVKqg8etXIHltwEQCtSDg8 +Ibs4rk4u1zbvh08d33h+KIavxaaQduj2HyaNYwpre2MR8ZegBq2KxF+c7m0EdALT +wCc4NP2qgx+RiRyRE45aCMYfyi5xpWxtwycpRVPrz8b57NUyjNHXs4PFHn13q0bc +QwMEose7DXLV8GtMU6HOjvnv0rTT0dHi7yC0vIkfPX7jC+e2eABhVjDXak2KI6sC +AwEAAaNkMGIwDgYDVR0PAQH/BAQDAgTQMDQGA1UdJQEB/wQqMCgGCCsGAQUFBwME +BggrBgEFBQcDAgYIKwYBBQUHAwEGCCsGAQUFBwMDMBoGA1UdEQQTMBGBD3N1cHBv +cnRAY2hlZi5pbzANBgkqhkiG9w0BAQsFAAOCAQEAOYrs1VK5AA3kL39wh9PVHbqG +d54YnurpDVPzHBDAt6BS5naMQ5hFPlT9Mb9ksyh86B7m/MtVUCkRmUiKk12Pv3t8 +bs05NRYy6efAAAe4lvmQaAxmPoRdHRWQkoX7BM7o6GdM7sJN9Wyz8iKlKwcpg9KL +OsBJ37TTkkMElr/yGFgVmm+uXWLsj5JqOYL+hNXkZBY42bMgcDodiOe7kCoPO3Vm +h0Ygd9TBqMoMSxQNVeD6hgsoej47XIs1K16LU31bExsFV4YW5bLezJNN9U0FW+il +LoDQcg6wxt8BvVuH7qxtzdu4uKDvLebvWQDeATjjdAu9m4t0AMtrkVk7dzDLUQ== +-----END CERTIFICATE----- diff --git a/test/unit/mock/files/test_certificate.rsa.key.pem b/test/unit/mock/files/test_certificate.rsa.key.pem new file mode 100644 index 000000000..b028d4655 --- /dev/null +++ b/test/unit/mock/files/test_certificate.rsa.key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAxi1Tp4dPQ+GU+RipsguUWT50a6fsBCpe+QT0YdW/7GG6kynR +zR+fzQ0q1LDxpgqAH+eDIWEAFYoTPc8haAjZvAYn7JlXUQpeoK7fc2BPgYA0lr33 +Ee0H9nqeZlnytQ+/EVUqqDx61cgeW3ARAK1IODwhuziuTi7XNu+HTx3feH4ohq/F +ppB26PYfJo1jCmt7YxHxl6AGrYrEX5zubQR0AtPAJzg0/aqDH5GJHJETjloIxh/K +LnGlbG3DJylFU+vPxvns1TKM0dezg8UefXerRtxDAwSix7sNctXwa0xToc6O+e/S +tNPR0eLvILS8iR89fuML57Z4AGFWMNdqTYojqwIDAQABAoIBAEhPd6u0Mpb5M6tk +dV7S3NFneYFipzqp3zeLuEQOg1YUHsjdxIDNHjjqqgsreTD1ueRqTC2cwDQbyoOO +FYlpWVFDCcRJ+1NFrluBE2V86eW9yvKJ5CH1VCd6fFuqTGYGldgUNFlooAVrXLBO +htWxZJ2oS0KOHwPGEZ8o7T1QEB9dfGpqROJJ444gnAWZ5iUTDqXwy3+pQlPUdAP3 +A2VpBG/QyT6UKtUpCcAdkzENJpy8Z1BEIigLp+24fYtLRaIme8yEm/JrLneWqgYr +fNcKqh7z2MlNElOflyaT5381Kj3GgYM7qrPz6SQ92VEmUemg4xhMAsO9lMLEoUly +aT5InUkCgYEA75OXkqY7c12txpZxkRR49dTWc6V97RuSEfkJuTuJQe/bQ7vhKWFt +uRhpBjc6czfr3KcwLNz9axQpnzQmhVfJ7GyYH8VLwaAHgwEAiTcdOf3Xp0fdGe1N +wLcaeSxH+goMff410YWbtJGlyf5AetKl6XKsjeWa/+3vx1JcmnufYY0CgYEA08Mz +nNycP+74X9yfHWRMFi/DNhX3jnOc897yc1WC7TUj3+l9KcOdEz21uVOUZod+ThJJ +vUhmMXzBs7yeQiiv80Wm4ONrPjo0dgttsob3VFo419c5x9uzqJWhg9LqryQRFJKL +GskKIhwN8ErZil8Tm5yg6ihX8xh6L6SesGyR4BcCgYEA2pUci85jG4TzEecdQrMd +EZ3Y87agR/8JrKA9QOWS+7fto8T9UBX2WBRvbh5hk9IHvlBD4grWpCXHO9wG8U4B +i1YhDYui7MwnTl1Rsd+5KLnzUkp87jTW5eepnbjLCtS0RRf03m86eusQClWRWv5q +Ja5cxTIh0zOxu3fnyYLVDdkCgYAMzvXEOyPISjADvFhzcqmXffQUxWdf2mZX6dhI +WZe9uUUeOgU0DXzmuQjQ2NlVCkT9e+Wx6TslKyKcOIBqCAP8du4NFDRcYzDhIvfT +oI49L+fYRlBcYlGPlN1cF9nSFiBiWirHx/kw7vl4204lLHMHKoYhI6eOMKDTWOWw +TiDUqQKBgG+xIdvtIxpjtqddJwLgMlEhpVCdG5Xrpj9TjVfoEL2u7Lmr+beEf6DU +Xx7U8zTSI4RgaK/ckkPbI8s+WzBLvDCY3pWc58m8f+Od8hDKQsIijATE0se2lkPa +pspN5y2nI9dphprCzSrr5VOtffxIt88OX+ecCEwyR64uvoQtcLPj +-----END RSA PRIVATE KEY----- diff --git a/test/unit/resources/rsa_key_test.rb b/test/unit/resources/rsa_key_test.rb new file mode 100644 index 000000000..0780c6270 --- /dev/null +++ b/test/unit/resources/rsa_key_test.rb @@ -0,0 +1,19 @@ +# encoding: utf-8 +# author: Richard Nixon + +require 'helper' +require 'inspec/resource' + +describe 'Inspec::Resources::RsaKey' do + let (:resource_key) { load_resource('rsa_key', 'test_certificate.rsa.key.pem')} + + it 'parses the public key' do + _(resource_key.send('public_key')).must_match "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxi1Tp4dPQ+GU+RipsguU\nWT50a6fsBCpe+QT0YdW/7GG6kynRzR+fzQ0q1LDxpgqAH+eDIWEAFYoTPc8haAjZ\nvAYn7JlXUQpeoK7fc2BPgYA0lr33Ee0H9nqeZlnytQ+/EVUqqDx61cgeW3ARAK1I\nODwhuziuTi7XNu+HTx3feH4ohq/FppB26PYfJo1jCmt7YxHxl6AGrYrEX5zubQR0\nAtPAJzg0/aqDH5GJHJETjloIxh/KLnGlbG3DJylFU+vPxvns1TKM0dezg8UefXer\nRtxDAwSix7sNctXwa0xToc6O+e/StNPR0eLvILS8iR89fuML57Z4AGFWMNdqTYoj\nqwIDAQAB\n-----END PUBLIC KEY-----\n" + end + + it 'decodes the key length' do + _(resource_key.send('key_length')).must_equal 2048 + end + + +end diff --git a/test/unit/resources/x509_certificate_test.rb b/test/unit/resources/x509_certificate_test.rb new file mode 100644 index 000000000..023153cfa --- /dev/null +++ b/test/unit/resources/x509_certificate_test.rb @@ -0,0 +1,92 @@ +# encoding: utf-8 +# author: Richard Nixon + +require 'helper' +require 'inspec/resource' + +describe 'Inspec::Resources::X509Certificate' do + let (:resource_cert) { + load_resource( + 'x509_certificate', + 'test_certificate.rsa.crt.pem', + 'test_certificate.rsa.key.pem', + 'test_ca_public.key.pem' + ) + } + + it 'decodes the subject as a string' do + _(resource_cert.send('subject')).must_match 'Inspec Test Certificate' + end + + it 'parses the certificate subject' do + _(resource_cert.send('parsed_subject').CN).must_equal 'Inspec Test Certificate' + _(resource_cert.send('parsed_subject').emailAddress).must_equal 'support@chef.io' + end + + it 'decodes the issuer as a string' do + _(resource_cert.send('issuer')).must_match 'Inspec Test CA' + end + + it 'parses the issuer' do + _(resource_cert.send('parsed_issuer').CN).must_equal 'Inspec Test CA' + end + + it 'parses the public key' do + _(resource_cert.send('public_key').to_s).must_match "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxi1Tp4dPQ+GU+RipsguU\nWT50a6fsBCpe+QT0YdW/7GG6kynRzR+fzQ0q1LDxpgqAH+eDIWEAFYoTPc8haAjZ\nvAYn7JlXUQpeoK7fc2BPgYA0lr33Ee0H9nqeZlnytQ+/EVUqqDx61cgeW3ARAK1I\nODwhuziuTi7XNu+HTx3feH4ohq/FppB26PYfJo1jCmt7YxHxl6AGrYrEX5zubQR0\nAtPAJzg0/aqDH5GJHJETjloIxh/KLnGlbG3DJylFU+vPxvns1TKM0dezg8UefXer\nRtxDAwSix7sNctXwa0xToc6O+e/StNPR0eLvILS8iR89fuML57Z4AGFWMNdqTYoj\nqwIDAQAB\n-----END PUBLIC KEY-----\n" + end + + it 'can check if the private key matches the certificate' do + _(resource_cert.send('private_key_matches?')).must_equal true + end + + it 'can check if a CA key was used to sign this cert' do + _(resource_cert.send('ca_key_matches?')).must_equal true + end + + it 'can determine the key length' do + _(resource_cert.send('key_length')).must_equal 2048 + end + + it 'parses the serial number' do + _(resource_cert.send('serial')).must_equal 37 + end + + it 'parses the signature algorithm' do + _(resource_cert.send('signature_algorithm')).must_equal 'sha256WithRSAEncryption' + end + + it 'parses the x.509 certificate version' do + _(resource_cert.send('version')).must_equal 2 + end + + it 'includes the standard extensions even if they are not in the certificate' do + _(resource_cert.send('extensions').length).must_equal 16 + _(resource_cert.send('extensions')).must_include 'keyUsage' + _(resource_cert.send('extensions')).must_include 'extendedKeyUsage' + _(resource_cert.send('extensions')).must_include 'subjectAltName' + end + + it 'parses the x.509 certificate extensions' do + _(resource_cert.send('extensions')['keyUsage']).must_include "Digital Signature" + _(resource_cert.send('extensions')['keyUsage']).must_include "Non Repudiation" + _(resource_cert.send('extensions')['keyUsage']).must_include "Data Encipherment" + _(resource_cert.send('extensions')['extendedKeyUsage']).must_include "TLS Web Server Authentication" + _(resource_cert.send('extensions')['extendedKeyUsage']).must_include "Code Signing" + _(resource_cert.send('extensions')['subjectAltName']).must_include "email:support@chef.io" + end + + it 'parses missing x.509 certificate extensions' do + _(resource_cert.send('extensions')['nameConstraints']).wont_include "Fried Chicken" + end + + it 'calculates the remaining days of validity' do + # Still valid + Time.stub :now, Time.new(2018, 2, 1, 1, 28, 57, '+00:00') do + _(resource_cert.send('days_remaining')).must_equal 28 + end + # Expired + Time.stub :now, Time.new(2018, 4, 1, 1, 28, 57, '+00:00') do + _(resource_cert.send('days_remaining')).must_equal (-31) + end + end +end From d2f000e4353006721550012a31120251e6450a0a Mon Sep 17 00:00:00 2001 From: Christoph Hartmann Date: Tue, 21 Mar 2017 00:26:57 +0100 Subject: [PATCH 2/3] refactor x509 resources and rsa key Signed-off-by: Christoph Hartmann --- docs/resources/key_rsa.md | 70 ++++++++++++ docs/resources/rsa_key.md | 41 ------- docs/resources/x509_certificate.md | 93 +++++++--------- lib/inspec/resource.rb | 2 +- lib/resources/key_rsa.rb | 67 ++++++++++++ lib/resources/rsa_key.rb | 57 ---------- lib/resources/x509_certificate.rb | 102 ++++++++++-------- .../{rsa_key_test.rb => key_rsa_test.rb} | 4 +- test/unit/resources/x509_certificate_test.rb | 30 +++--- 9 files changed, 246 insertions(+), 220 deletions(-) create mode 100644 docs/resources/key_rsa.md delete mode 100644 docs/resources/rsa_key.md create mode 100644 lib/resources/key_rsa.rb delete mode 100644 lib/resources/rsa_key.rb rename test/unit/resources/{rsa_key_test.rb => key_rsa_test.rb} (92%) diff --git a/docs/resources/key_rsa.md b/docs/resources/key_rsa.md new file mode 100644 index 000000000..691fe1f86 --- /dev/null +++ b/docs/resources/key_rsa.md @@ -0,0 +1,70 @@ +--- +title: The key_rsa Resource +--- + +# key_rsa + +Use the `key_rsa` InSpec audit resource to test RSA public/private keypairs. + +This resource is mainly useful when used in conjunction with the x509_certificate resource but it can also be used for checking SSH keys. + + +## Syntax + +An `key_rsa` resource block declares a `key file` to be tested. + + describe key_rsa('mycertificate.key') do + it { should be_private } + it { should be_public } + its('public_key') { should match "-----BEGIN PUBLIC KEY-----\n3597459df9f3982" } + its('key_length') { should eq 2048 } + end + +You can use an optional passphrase with `key_rsa` + + describe key_rsa('mycertificate.key', 'passphrase') do + it { should be_private } + end + +## Supported Properties + +### public? + +To verify if a key is public use the following: + + describe key_rsa('/etc/pki/www.mywebsite.com.key') do + it { should be_public } + end + +### public_key (String) + +The `public_key` property returns the public part of the RSA key pair + + describe key_rsa('/etc/pki/www.mywebsite.com.key') do + its('public_key') { should match "-----BEGIN PUBLIC KEY-----\n3597459df9f3982......" } + end + +### private? + +This property verifies that the key includes a private key: + + describe key_rsa('/etc/pki/www.mywebsite.com.key') do + it { should be_private } + end + + +### private_key (String) + +The `private_key` property returns the private key or the RSA key pair. + + describe key_rsa('/etc/pki/www.mywebsite.com.key') do + its('private_key') { should match "-----BEGIN RSA PRIVATE KEY-----\nMIIJJwIBAAK......" } + end + +### key_length + +The `key_length` property allows testing the number of bits in the key pair. + + describe key_rsa('/etc/pki/www.mywebsite.com.key') do + its('key_length') { should eq 2048 } + end diff --git a/docs/resources/rsa_key.md b/docs/resources/rsa_key.md deleted file mode 100644 index 509ebfffc..000000000 --- a/docs/resources/rsa_key.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: The rsa_key Resource ---- - -# rsa_key - -Use the `rsa_key` InSpec audit resource to test RSA public/private keypairs. - -This resource is mainly useful when used in conjunction with the x509_certificate resource but it can also be used for checking SSH keys. - - -## Syntax - -An `rsa_key` resource block declares a `key file` to be tested. - - describe rsa_key('mycertificate.key') do - its('public_key') { should match "-----BEGIN PUBLIC KEY-----\n3597459df9f3982" } - its('key_length') { should eq 2048 } - end - -## Supported Properties - -### public_key (String) - -The `public_key` property returns the public part of the RSA keypair - - describe rsa_key('/etc/pki/www.mywebsite.com.key') do - its('public_key') { should match "-----BEGIN PUBLIC KEY-----\n3597459df9f3982......" } - end - -### private_key (String) - -See the `public key` property - -### key_length - -The key_length` property allows testing the number of bits in the keypair. - - describe rsa_key('/etc/pki/www.mywebsite.com.key') do - its('key_length') { should eq 2048 } - end diff --git a/docs/resources/x509_certificate.md b/docs/resources/x509_certificate.md index 3aabb4972..36c25c334 100644 --- a/docs/resources/x509_certificate.md +++ b/docs/resources/x509_certificate.md @@ -17,55 +17,47 @@ certificates. An `x509_certificate` resource block declares a certificate `key file` to be tested. describe x509_certificate('mycertificate.pem') do - its('days_remaining') { should be > 30 } - end - -It can optionally specify a private key file and a ca public key file for key verification - - describe x509_certificate('mycertificate.cert','mycertificate.key','ca_key.pub') do - its('private_key_matches?') { should be true } - its('ca_key_matches?') { should be true } + its('validity_in_days') { should be > 30 } end ## Supported Properties -### subject (String) +### subject.XX -The `subject` string contains several fields seperated by forward slashes. The -field identifiers are the same ones used by OpenSSL to generate CSR's and certs. +`subject` property makes it easier to access individual subject elements. + + describe x509_certificate('/etc/pki/www.mywebsite.com.pem') do + its('subject.CN') { should eq "www.mywebsite.com" } + end + +### subject_dn (String) + +The `subject_dn` string returns the distinguished name of the subject field. It contains several fields separated by forward slashes. The field identifiers are the same ones used by OpenSSL to generate CSR's and certs. Use `subject.XX` instead to access the parsed version. e.g. `/C=US/L=Seattle/O=Chef Software Inc/OU=Chefs/CN=Richard Nixon` describe x509_certificate('/etc/pki/www.mywebsite.com.pem') do - its('subject') { should match "CN=www.mywebsite.com" } + its('subject_dn') { should match "CN=www.mywebsite.com" } end -### parsed_subject.XX +### issuer.XX -`parsed_subject` property makes it easier to access individual subject elements. +`issuer` makes it easier to access individual issuer elements. describe x509_certificate('/etc/pki/www.mywebsite.com.pem') do - its('parsed_subject.CN') { should eq "www.mywebsite.com" } + its('issuer.CN') { should eq "Acme Trust CA" } end -### issuer (String) +### issuer_dn (String) -The `issuer` string is copied from a CA (certificate authority) during the +The `issuer_dn` is the distinguished name from a CA (certificate authority) during the certificate signing process. It describes which authority is guaranteeing the identity of our certificate. e.g. `/C=US/L=Seattle/CN=Acme Trust CA/emailAddress=support@acmetrust.org` describe x509_certificate('/etc/pki/www.mywebsite.com.pem') do - its('issuer') { should match "CN=Acme Trust CA" } - end - -### parsed_issuer.XX - -`parsed_issuer` makes it easier to access individual issuer elements. - - describe x509_certificate('/etc/pki/www.mywebsite.com.pem') do - its('parsed_issuer.CN') { should eq "Acme Trust CA" } + its('issuer_cn') { should match "CN=Acme Trust CA" } end ### public_key (String) @@ -81,7 +73,7 @@ The `public_key` property returns a base64 encoded public key in PEM format. The `key_length` property calculates the number of bits in the public key. More bits increase security, but at the cost of speed and in extreme cases, compatibility. - describe x509_certificate('mycert.pem') do + describe x509_certificate('/etc/pki/www.mywebsite.com.pem') do its('key_length') { should be 2048 } end @@ -94,27 +86,14 @@ sign the certificate. its('signature_algorithm') { should be 'sha256WithRSAEncryption' } end -### private_key_matches? (Boolean) ca_key_matches? (Boolean) -The `private_key_matches?` and `ca_key_matches?` methods check +### validity_in_days (Float) -* If the supplied private key matches the certificate -* If the CA public key belongs to the CA that signed the certificate - - - describe x509_certificate('mycertificate.cert','mycertificate.key','ca_key.pub') do - its('private_key_matches?') { should be true } - its('ca_key_matches?') { should be true } - end - - -### days_remaining (Float) - -The `days_remaining` property can be used to check that certificates are not in +The `validity_in_days` property can be used to check that certificates are not in danger of expiring soon. describe x509_certificate('/etc/pki/www.mywebsite.com.pem') do - its('days_remaining') { should be > 30 } + its('validity_in_days') { should be > 30 } end ### not_before and not_after (Time) @@ -131,31 +110,37 @@ validity. They are exposed as ruby Time class so that date arithmetic can be eas The `serial` property exposes the serial number of the certificate. The serial number is set by the CA during the signing process and should be unique within that CA. + describe x509_certificate('/etc/pki/www.mywebsite.com.pem') do + its('serial') { should eq 9623283588743302433 } + end + ### version (Integer) The `version` property exposes the certificate version. + describe x509_certificate('/etc/pki/www.mywebsite.com.pem') do + its('version') { should eq 2 } + end + ### extensions (Hash) The `extensions` hash property is mainly used to determine what the certificate can be used for. describe x509_certificate('/etc/pki/www.mywebsite.com.pem') do - its('extensions').length) {should eq 3 } - # Check what extension categories we have - its('extensions')) { should include 'keyUsage' } - its('extensions')) { should include 'extendedKeyUsage' } - its('extensions')) { should include 'subjectAltName' } + its('extensions') { should include 'keyUsage' } + its('extensions') { should include 'extendedKeyUsage' } + its('extensions') { should include 'subjectAltName' } # Check examples of basic 'keyUsage' - its('extensions')['keyUsage']) { should include "Digital Signature" } - its('extensions')['keyUsage']).must_include "Non Repudiation" - its('extensions')['keyUsage']).must_include "Data Encipherment" + its('extensions.keyUsage') { should include 'Digital Signature' } + its('extensions.keyUsage') { should include 'Non Repudiation' } + its('extensions.keyUsage') { should include 'Data Encipherment' } # Check examples of newer 'extendedKeyUsage' - its('extensions')['extendedKeyUsage']) { should include "TLS Web Server Authentication" } - its('extensions')['extendedKeyUsage']) { should include "Code Signing" } + its('extensions.extendedKeyUsage') { should include 'TLS Web Server Authentication' } + its('extensions.extendedKeyUsage') { should include 'Code Signing' } # Check examples of 'subjectAltName' - its('extensions')['subjectAltName']) { should include "email:support@chef.io" } + its('extensions.subjectAltName') { should include 'email:support@chef.io' } end diff --git a/lib/inspec/resource.rb b/lib/inspec/resource.rb index 0057c5a8b..29fdc7916 100644 --- a/lib/inspec/resource.rb +++ b/lib/inspec/resource.rb @@ -95,6 +95,7 @@ require 'resources/iptables' require 'resources/json' require 'resources/kernel_module' require 'resources/kernel_parameter' +require 'resources/key_rsa' require 'resources/limits_conf' require 'resources/login_def' require 'resources/mount' @@ -119,7 +120,6 @@ require 'resources/postgres_session' require 'resources/powershell' require 'resources/processes' require 'resources/registry_key' -require 'resources/rsa_key' require 'resources/security_policy' require 'resources/service' require 'resources/shadow' diff --git a/lib/resources/key_rsa.rb b/lib/resources/key_rsa.rb new file mode 100644 index 000000000..190125578 --- /dev/null +++ b/lib/resources/key_rsa.rb @@ -0,0 +1,67 @@ +# encoding: utf-8 +# author: Richard Nixon +# author: Christoph Hartmann + +require 'openssl' +require 'hashie/mash' + +module Inspec::Resources + class RsaKey < Inspec.resource(1) + name 'key_rsa' + desc 'public/private RSA key pair test' + example " + describe rsa_key('/etc/pki/www.mywebsite.com.key') do + its('public_key') { should match /BEGIN RSA PUBLIC KEY/ } + end + + describe rsa_key('/etc/pki/www.mywebsite.com.key', 'passphrase') do + it { should be_private } + it { should be_public } + end + " + + def initialize(keypath, passphrase = nil) + @key_path = keypath + @key_file = inspec.file(@key_path) + @key = nil + @passphrase = passphrase + + return skip_resource "Unable to find key file #{@key_path}" unless @key_file.exist? + + begin + @key = OpenSSL::PKey.read(@key_file.content, @passphrase) + rescue OpenSSL::PKey::RSAError => _ + return skip_resource "Unable to load key file #{@key_path}" + end + end + + def public? + return if @key.nil? + @key.public? + end + + def public_key + return if @key.nil? + @key.public_key.to_s + end + + def private? + return if @key.nil? + @key.private? + end + + def private_key + return if @key.nil? + @key.to_s + end + + def key_length + return if @key.nil? + @key.public_key.n.num_bytes * 8 + end + + def to_s + "rsa_key #{@key_path}" + end + end +end diff --git a/lib/resources/rsa_key.rb b/lib/resources/rsa_key.rb deleted file mode 100644 index dfebe45f0..000000000 --- a/lib/resources/rsa_key.rb +++ /dev/null @@ -1,57 +0,0 @@ -# encoding: utf-8 -# author: Richard Nixon - -require 'openssl' -require 'hashie/mash' - -module Inspec::Resources - class RsaKey < Inspec.resource(1) - name 'rsa_key' - desc 'Used to test RSA keys' - example " - describe rsa_key('/etc/pki/www.mywebsite.com.key') do - its('public_key') { should match /BEGIN RSA PUBLIC KEY/ } - end - " - def initialize(keypath) - @keypath = keypath - @keyfile = inspec.backend.file(@keypath) - if @keyfile.exist? - load_key - else - @key = RuntimeError.new('Key file not found') - end - end - - private def load_key - @keyraw ||= @keyfile.content - @key ||= OpenSSL::PKey::RSA.new(@keyraw) - rescue OpenSSL::X509::RSAError => error_code - @key ||= error_code - end - - def public_key - @key.public_key.to_s - end - - def private_key - @key.to_s - end - - def key_length - n.num_bytes * 8 # Answer in bits - end - - def exist? - @keyfile.exist? - end - - def key_length - @key.public_key.n.num_bytes * 8 - end - - def to_s - "rsa_key #{@keypath}" - end - end -end diff --git a/lib/resources/x509_certificate.rb b/lib/resources/x509_certificate.rb index b4c2f8be9..21c3e89fb 100644 --- a/lib/resources/x509_certificate.rb +++ b/lib/resources/x509_certificate.rb @@ -1,93 +1,97 @@ # encoding: utf-8 # author: Richard Nixon +# author: Christoph Hartmann require 'openssl' require 'hashie/mash' module Inspec::Resources - class X509CertificateResource < Inspec.resource(1) + class X509CertificateResource < Inspec.resource(1) # rubocop:disable Metrics/ClassLength name 'x509_certificate' desc 'Used to test x.509 certificates' example " describe x509_certificate('/etc/pki/www.mywebsite.com.pem') do its('subject') { should match /CN=My Website/ } - its('days_remaining') { should be > 30 } + its('validity_in_days') { should be > 30 } + end + + describe x509_certificate('trials/x509/cert.pem') do + it { should be_certificate } + it { should be_valid } + its('fingerprint') { should eq '62b137bdf427e7273dc2e487877b3033e4c8ce17' } + its('signature_algorithm') { should eq 'sha1WithRSAEncryption' } + its('validity_in_days') { should_not be < 100 } + its('validity_in_days') { should be >= 100 } + its('subject_dn') { should eq '/C=DE/ST=Berlin/L=Berlin/O=InSpec/OU=Chef Software, Inc/CN=inspec.io/emailAddress=support@chef.io' } + its('subject.C') { should eq 'DE' } + its('subject.emailAddress') { should_not be_empty } + its('subject.emailAddress') { should eq 'support@chef.io' } + its('issuer_dn') { should eq '/C=DE/ST=Berlin/L=Berlin/O=InSpec/OU=Chef Software, Inc/CN=inspec.io/emailAddress=support@chef.io' } + its('key_length') { should be >= 2048 } + its('extensions.subjectKeyIdentifier') { should cmp 'A5:16:0B:12:F4:48:0F:06:6C:32:29:67:98:12:DF:3D:0D:75:9D:5C' } end " - def initialize(certpath,private_keypath=nil,ca_public_keypath=nil) - @certpath = certpath + # @see https://tools.ietf.org/html/rfc5280#page-23 + def initialize(filename) + @certpath = filename @issuer = nil @parsed_subject = nil @parsed_issuer = nil @extensions = nil - @cert = cert_from_file(certpath) - @key = key_from_file(private_keypath) - @cakey = key_from_file(ca_public_keypath) - end - private def cert_from_file(certpath) - certfile = inspec.backend.file(certpath) - if certfile.exist? - certraw = certfile.content - certcooked = OpenSSL::X509::Certificate.new(certraw) - else - certcooked = RuntimeError.new("Certificate #{certpath} not found") + file = inspec.file(@certpath) + return skip_resource "Unable to find certificate file #{@certpath}" unless file.exist? + + begin + @cert = OpenSSL::X509::Certificate.new file.content + rescue OpenSSL::X509::CertificateError + @cert = nil + return skip_resource "Unable to load certificate #{@certpath}" end - certcooked - rescue OpenSSL::X509::CertificateError => error_code - error_code end - private def key_from_file(keypath) - keyfile = inspec.backend.file(keypath) - if keyfile.exist? - keyraw = keyfile.content - keycooked = OpenSSL::PKey::RSA.new(keyraw) - else - keycooked = RuntimeError.new("Keyfile #{keypath} not found") - end - keycooked - rescue OpenSSL::PKey::RSAError => error_code - error_code - end - - # Forward these methods directly to OpenSSL::X509::Certificate instance - %w{serial version not_before not_after signature_algorithm public_key }.each do |m| + %w{version not_before not_after signature_algorithm public_key }.each do |m| define_method m.to_sym do |*args| @cert.method(m.to_sym).call(*args) end end - def exist? - @certfile.exist? + def certificate? + !@cert.nil? end - def private_key_matches? - @cert.check_private_key(@key) + def fingerprint + return if @cert.nil? + OpenSSL::Digest::SHA1.new(@cert.to_der).to_s end - def ca_key_matches? - @cert.verify(@cakey) + def serial + return if @cert.nil? + @cert.serial.to_i end - def subject + def subject_dn + return if @cert.nil? @cert.subject.to_s end - def parsed_subject + def subject + return if @cert.nil? # Return cached subject if we have already parsed it return @parsed_subject if @parsed_subject # Use a Mash to make it easier to access hash elements in "its('subject') {should ...}" @parsed_subject = Hashie::Mash.new(Hash[@cert.subject.to_a.map { |k, v, _| [k, v] }]) end - def issuer + def issuer_dn + return if @cert.nil? @cert.issuer.to_s end - def parsed_issuer + def issuer + return if @cert.nil? # Return cached subject if we have already parsed it return @parsed_issuer if @parsed_issuer # Use a Mash to make it easier to access hash elements in "its('issuer') {should ...}" @@ -95,11 +99,17 @@ module Inspec::Resources end def key_length + return if @cert.nil? @cert.public_key.n.num_bytes * 8 end - def days_remaining - (@cert.not_after - Time.now.utc) / 86400 + def validity_in_days + (not_after - Time.now.utc) / 86400 + end + + def valid? + now = Time.now + certificate? && (now >= not_before && now <= not_after) end def extensions @@ -118,7 +128,7 @@ module Inspec::Resources policyConstraints nameConstraints noCheck tlsfeature nsComment }.each { |extension| @extensions[extension] ||= [] } # Now parse the extensions into the Mash - extension_array = @cert.extensions.map { |e| e.to_s } + extension_array = @cert.extensions.map(&:to_s) extension_array.each do |extension| kv = extension.split(/ *= */, 2) @extensions[kv.first] = kv.last.split(/ *, */) diff --git a/test/unit/resources/rsa_key_test.rb b/test/unit/resources/key_rsa_test.rb similarity index 92% rename from test/unit/resources/rsa_key_test.rb rename to test/unit/resources/key_rsa_test.rb index 0780c6270..442e99a3f 100644 --- a/test/unit/resources/rsa_key_test.rb +++ b/test/unit/resources/key_rsa_test.rb @@ -5,7 +5,7 @@ require 'helper' require 'inspec/resource' describe 'Inspec::Resources::RsaKey' do - let (:resource_key) { load_resource('rsa_key', 'test_certificate.rsa.key.pem')} + let (:resource_key) { load_resource('key_rsa', 'test_certificate.rsa.key.pem')} it 'parses the public key' do _(resource_key.send('public_key')).must_match "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxi1Tp4dPQ+GU+RipsguU\nWT50a6fsBCpe+QT0YdW/7GG6kynRzR+fzQ0q1LDxpgqAH+eDIWEAFYoTPc8haAjZ\nvAYn7JlXUQpeoK7fc2BPgYA0lr33Ee0H9nqeZlnytQ+/EVUqqDx61cgeW3ARAK1I\nODwhuziuTi7XNu+HTx3feH4ohq/FppB26PYfJo1jCmt7YxHxl6AGrYrEX5zubQR0\nAtPAJzg0/aqDH5GJHJETjloIxh/KLnGlbG3DJylFU+vPxvns1TKM0dezg8UefXer\nRtxDAwSix7sNctXwa0xToc6O+e/StNPR0eLvILS8iR89fuML57Z4AGFWMNdqTYoj\nqwIDAQAB\n-----END PUBLIC KEY-----\n" @@ -14,6 +14,4 @@ describe 'Inspec::Resources::RsaKey' do it 'decodes the key length' do _(resource_key.send('key_length')).must_equal 2048 end - - end diff --git a/test/unit/resources/x509_certificate_test.rb b/test/unit/resources/x509_certificate_test.rb index 023153cfa..11885afa1 100644 --- a/test/unit/resources/x509_certificate_test.rb +++ b/test/unit/resources/x509_certificate_test.rb @@ -8,39 +8,33 @@ describe 'Inspec::Resources::X509Certificate' do let (:resource_cert) { load_resource( 'x509_certificate', - 'test_certificate.rsa.crt.pem', - 'test_certificate.rsa.key.pem', - 'test_ca_public.key.pem' + 'test_certificate.rsa.crt.pem' ) } - it 'decodes the subject as a string' do - _(resource_cert.send('subject')).must_match 'Inspec Test Certificate' + it 'verify subject distingushed name' do + _(resource_cert.send('subject_dn')).must_match 'Inspec Test Certificate' end it 'parses the certificate subject' do - _(resource_cert.send('parsed_subject').CN).must_equal 'Inspec Test Certificate' - _(resource_cert.send('parsed_subject').emailAddress).must_equal 'support@chef.io' + _(resource_cert.send('subject').CN).must_equal 'Inspec Test Certificate' + _(resource_cert.send('subject').emailAddress).must_equal 'support@chef.io' end - it 'decodes the issuer as a string' do - _(resource_cert.send('issuer')).must_match 'Inspec Test CA' + it 'verify issue distingushed name' do + _(resource_cert.send('issuer_dn')).must_match 'Inspec Test CA' end it 'parses the issuer' do - _(resource_cert.send('parsed_issuer').CN).must_equal 'Inspec Test CA' + _(resource_cert.send('issuer').CN).must_equal 'Inspec Test CA' end it 'parses the public key' do _(resource_cert.send('public_key').to_s).must_match "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxi1Tp4dPQ+GU+RipsguU\nWT50a6fsBCpe+QT0YdW/7GG6kynRzR+fzQ0q1LDxpgqAH+eDIWEAFYoTPc8haAjZ\nvAYn7JlXUQpeoK7fc2BPgYA0lr33Ee0H9nqeZlnytQ+/EVUqqDx61cgeW3ARAK1I\nODwhuziuTi7XNu+HTx3feH4ohq/FppB26PYfJo1jCmt7YxHxl6AGrYrEX5zubQR0\nAtPAJzg0/aqDH5GJHJETjloIxh/KLnGlbG3DJylFU+vPxvns1TKM0dezg8UefXer\nRtxDAwSix7sNctXwa0xToc6O+e/StNPR0eLvILS8iR89fuML57Z4AGFWMNdqTYoj\nqwIDAQAB\n-----END PUBLIC KEY-----\n" end - it 'can check if the private key matches the certificate' do - _(resource_cert.send('private_key_matches?')).must_equal true - end - - it 'can check if a CA key was used to sign this cert' do - _(resource_cert.send('ca_key_matches?')).must_equal true + it 'can determine fingerprint' do + _(resource_cert.send('fingerprint')).must_equal '62bb500b0190ae47fd593c29a0b92ddbeb6c1eb6' end it 'can determine the key length' do @@ -82,11 +76,11 @@ describe 'Inspec::Resources::X509Certificate' do it 'calculates the remaining days of validity' do # Still valid Time.stub :now, Time.new(2018, 2, 1, 1, 28, 57, '+00:00') do - _(resource_cert.send('days_remaining')).must_equal 28 + _(resource_cert.send('validity_in_days')).must_equal 28 end # Expired Time.stub :now, Time.new(2018, 4, 1, 1, 28, 57, '+00:00') do - _(resource_cert.send('days_remaining')).must_equal (-31) + _(resource_cert.send('validity_in_days')).must_equal (-31) end end end From a96059a3eb47e53b5541bd8782854f1849d25661 Mon Sep 17 00:00:00 2001 From: Christoph Hartmann Date: Tue, 21 Mar 2017 22:17:50 +0100 Subject: [PATCH 3/3] x509 integration tests Signed-off-by: Christoph Hartmann --- test/cookbooks/os_prepare/metadata.rb | 1 + test/cookbooks/os_prepare/recipes/default.rb | 1 + test/cookbooks/os_prepare/recipes/x509.rb | 15 ++++++++++++ test/integration/default/x509_spec.rb | 24 ++++++++++++++++++++ 4 files changed, 41 insertions(+) create mode 100644 test/cookbooks/os_prepare/recipes/x509.rb create mode 100644 test/integration/default/x509_spec.rb diff --git a/test/cookbooks/os_prepare/metadata.rb b/test/cookbooks/os_prepare/metadata.rb index 4f1b8a3cb..c6bce4cc7 100644 --- a/test/cookbooks/os_prepare/metadata.rb +++ b/test/cookbooks/os_prepare/metadata.rb @@ -12,3 +12,4 @@ depends 'postgresql' depends 'httpd', '~> 0.2' depends 'windows' depends 'ssh-hardening' +depends 'openssl' diff --git a/test/cookbooks/os_prepare/recipes/default.rb b/test/cookbooks/os_prepare/recipes/default.rb index 1d85a33a4..b9f7ae435 100644 --- a/test/cookbooks/os_prepare/recipes/default.rb +++ b/test/cookbooks/os_prepare/recipes/default.rb @@ -18,6 +18,7 @@ include_recipe('os_prepare::package') include_recipe('os_prepare::registry_key') include_recipe('os_prepare::iis_site') include_recipe('os_prepare::iptables') unless node['osprepare']['docker'] +include_recipe('os_prepare::x509') # config file parsing include_recipe('os_prepare::json_yaml_csv_ini') diff --git a/test/cookbooks/os_prepare/recipes/x509.rb b/test/cookbooks/os_prepare/recipes/x509.rb new file mode 100644 index 000000000..30e4d9617 --- /dev/null +++ b/test/cookbooks/os_prepare/recipes/x509.rb @@ -0,0 +1,15 @@ +if node['platform_family'] != 'windows' + + openssl_x509 '/tmp/mycert.pem' do + common_name 'www.f00bar.com' + org 'Foo Bar' + org_unit 'Lab' + country 'US' + expire 360 + end + + openssl_rsa_key '/tmp/server.key' do + key_length 2048 + end + +end diff --git a/test/integration/default/x509_spec.rb b/test/integration/default/x509_spec.rb new file mode 100644 index 000000000..f19645243 --- /dev/null +++ b/test/integration/default/x509_spec.rb @@ -0,0 +1,24 @@ +# encoding: utf-8 + +if os.windows? + STDERR.puts "\033[1;33mTODO: Not running #{__FILE__} because we are not on Linux.\033[0m" + return +end + +describe x509_certificate('/tmp/mycert.pem') do + it { should be_certificate } + it { should be_valid } + its('signature_algorithm') { should eq 'sha256WithRSAEncryption' } + its('validity_in_days') { should_not be < 100 } + its('validity_in_days') { should be >= 100 } + its('subject_dn') { should eq '/C=US/O=Foo Bar/OU=Lab/CN=www.f00bar.com' } + its('subject.C') { should eq 'US' } + its('issuer_dn') { should eq '/C=US/O=Foo Bar/OU=Lab/CN=www.f00bar.com' } + its('key_length') { should be >= 2048 } +end + +describe key_rsa('/tmp/server.key') do + it { should be_private } + it { should be_public } + its('key_length') { should eq 2048 } +end