refactor x509 resources and rsa key

Signed-off-by: Christoph Hartmann <chris@lollyrock.com>
This commit is contained in:
Christoph Hartmann 2017-03-21 00:26:57 +01:00
parent f66f0b3a18
commit d2f000e435
9 changed files with 246 additions and 220 deletions

70
docs/resources/key_rsa.md Normal file
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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'

67
lib/resources/key_rsa.rb Normal file
View file

@ -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

View file

@ -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

View file

@ -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(/ *, */)

View file

@ -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

View file

@ -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