diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a158255ed..6dd1b2c5b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Change authn-k8s to expect the client cert (passed in `X-SSL-Client-Certificate`) to be url-escaped. +- Update Conjur issued certificates to use the common name derived from the authenticated + host, rather than use the value from the CSR. +- Update Conjur issued certificates to include a SPIFFE SVID as a subject alternative + name (SAN). ## [1.2.0] - 2018-09-07 ### Added diff --git a/app/controllers/certificate_authority_controller.rb b/app/controllers/certificate_authority_controller.rb index f133fe0bc4..a9af795da1 100644 --- a/app/controllers/certificate_authority_controller.rb +++ b/app/controllers/certificate_authority_controller.rb @@ -10,7 +10,7 @@ class CertificateAuthorityController < RestController before_action :verify_csr, only: :sign def sign - certificate = certificate_authority.sign_csr(csr, ttl) + certificate = certificate_authority.sign_csr(host, csr, ttl) render_certificate(certificate) end @@ -27,7 +27,6 @@ def verify_host def verify_csr raise Forbidden, 'CSR cannot be verified' unless csr.verify(csr.public_key) - raise Forbidden, 'CSR CN does not match host' unless host_name_matches?(csr) end def render_certificate(certificate) @@ -45,14 +44,6 @@ def render_certificate(certificate) end end - def host_name_matches?(csr) - csr_info = csr.subject.to_a.inject({}) do |result, (key, value)| - result.merge!(key => value) - end - - csr_info['CN'] == host.identifier.split('/').last - end - def certificate_authority ::CA::CertificateAuthority.new(ca_resource) end diff --git a/app/domain/ca/certificate_authority.rb b/app/domain/ca/certificate_authority.rb index 12d4e63161..e5f55e5532 100644 --- a/app/domain/ca/certificate_authority.rb +++ b/app/domain/ca/certificate_authority.rb @@ -24,20 +24,19 @@ def initialize(service) # csr: OpenSSL::X509::Request. Certificate signing request to sign # ttl: Integer. The desired lifetime, in seconds, for the # certificate - def sign_csr(csr, ttl) + def sign_csr(role, csr, ttl) csr_cert = OpenSSL::X509::Certificate.new # Generate a random 20 byte (160 bit) serial number for the certificate csr_cert.serial = SecureRandom.random_number(1<<160) - # This value is one less than the X509 version, so this is a - # version 3 certification + # This value is zero-based. This is a version 3 certificate. csr_cert.version = 2 now = Time.now csr_cert.not_before = now csr_cert.not_after = now + [ttl, max_ttl].min - csr_cert.subject = csr.subject + csr_cert.subject = subject(role) csr_cert.public_key = csr.public_key csr_cert.issuer = certificate.subject @@ -55,13 +54,52 @@ def sign_csr(csr, ttl) extension_factory.create_extension('subjectKeyIdentifier', 'hash') ) + csr_cert.add_extension( + extension_factory.create_extension("subjectAltName", subject_alt_name(role)) + ) + csr_cert.sign private_key, OpenSSL::Digest::SHA256.new - csr_cert end protected + def subject(role) + common_name = [ + role.account, + service_id, + role.kind, + role.identifier + ].join(':') + OpenSSL::X509::Name.new [['CN', common_name]] + end + + def service_id + # CA services have ids like 'conjur//ca' + @service_id ||= service.identifier.split('/')[1] + end + + def subject_alt_name(role) + [ + "DNS:#{leaf_domain_name(role)}", + "URI:#{spiffe_id(role)}" + ].join(', ') + end + + def leaf_domain_name(role) + role.identifier.split('/').last + end + + def spiffe_id(role) + [ + 'spiffe://conjur', + role.account, + service_id, + role.kind, + role.identifier + ].join('/') + end + def private_key @private_key ||= load_private_key end diff --git a/cucumber/api/features/certificate_authority.feature b/cucumber/api/features/certificate_authority.feature index c5be67ada6..6700fa34d8 100644 --- a/cucumber/api/features/certificate_authority.feature +++ b/cucumber/api/features/certificate_authority.feature @@ -72,11 +72,6 @@ Feature: Conjur signs certificates using a configured CA When I send a CSR for "toast" to the "kitchen" CA with a ttl of "P6M" and CN of "toast" Then the HTTP response status code is 403 - Scenario: The service returns 403 Forbidden if the CSR CN doesn't match the host - Given I login as "cucumber:host:bacon" - When I send a CSR for "bacon" to the "kitchen" CA with a ttl of "P6M" and CN of "toast" - Then the HTTP response status code is 403 - Scenario: I can sign a valid CSR with a configured Conjur CA Given I login as "cucumber:host:bacon" When I send a CSR for "bacon" to the "kitchen" CA with a ttl of "P6M" and CN of "bacon" @@ -91,6 +86,9 @@ Feature: Conjur signs certificates using a configured CA Then the HTTP response status code is 201 And the HTTP response content type is "application/x-pem-file" And the resulting pem certificate is valid according to the "kitchen" intermediate CA + And the common name is "cucumber:kitchen:host:bacon" + And the subject alternative names contain "DNS:bacon" + And the subject alternative names contain "URI:spiffe://conjur/cucumber/kitchen/host/bacon" Scenario: I can sign a CSR using an encrypted CA private key Given I login as "cucumber:host:table" diff --git a/cucumber/api/features/step_definitions/ca_steps.rb b/cucumber/api/features/step_definitions/ca_steps.rb index e5f2740aa8..4f4cc796c6 100644 --- a/cucumber/api/features/step_definitions/ca_steps.rb +++ b/cucumber/api/features/step_definitions/ca_steps.rb @@ -30,12 +30,32 @@ end Then(/^the resulting (pem|json) certificate is valid according to the "([^"]*)" intermediate CA$/) do |type, ca_name| - cert_body = (type == 'pem' ? @result : @result['certificate']) - cert = OpenSSL::X509::Certificate.new cert_body - + @certificate_response_type = type + store = OpenSSL::X509::Store.new store.add_cert @root_ca.cert store.add_cert intermediate_ca[ca_name].cert - expect(store.verify(cert)).to eq(true) + expect(store.verify(response_certificate)).to eq(true) +end + +Then(/^the common name is "([^"]*)"$/) do |cn| + subject_parts = response_certificate.subject + .to_a + .inject({}) do |result, (key, value)| + result.merge!(key => value) + end + + expect(subject_parts['CN']).to eq(cn) +end + +Then(/^the subject alternative names contain "([^"]*)"$/) do |san| + exists = false + response_certificate.extensions.each do |ext| + next unless ext.oid == 'subjectAltName' + + subject_alt_names = ext.value.split(', ') + exists = subject_alt_names.include?(san) + end + expect(exists).to eq(true) end diff --git a/cucumber/api/features/support/ca_helpers.rb b/cucumber/api/features/support/ca_helpers.rb index bcd1b21c33..0512f38bfd 100644 --- a/cucumber/api/features/support/ca_helpers.rb +++ b/cucumber/api/features/support/ca_helpers.rb @@ -24,6 +24,18 @@ def intermediate_ca @intermediate_ca ||= {} end + def response_certificate + @response_certificate ||= OpenSSL::X509::Certificate.new certificate_response_body + end + + def certificate_response_body + @certificate_response_body ||= (certificate_response_type == 'pem' ? @result : @result['certificate']) + end + + def certificate_response_type + @certificate_response_type ||= 'json' + end + # Provides certificate authority capabilities. This is # namely signing certificates for certificate signing requests (CSRs) module CertificateAuthority diff --git a/docs/CERTIFICATE_SIGNING.md b/docs/CERTIFICATE_SIGNING.md index a302794e0b..2a69cce721 100644 --- a/docs/CERTIFICATE_SIGNING.md +++ b/docs/CERTIFICATE_SIGNING.md @@ -87,15 +87,22 @@ Once the CA is configured in Conjur and a host has `sign` privileges, then a host may submit a CSR to the CA endpoint for signing: - The host first needs to generate its own private key and a - certificate signing request (CSR). The common name (CN) on the CSR - must match the host id in conjur. For example, if the host id is - `web-server/host-01`, then the CSR common name must be `host-01`. + certificate signing request (CSR). - The host must be authenticated to conjur, and then POST the PEM encoded CSR to `/ca///sign` - If the CSR is valid and the host is authorized, then the CA will respond with the PEM encoded certificate. + +- The CA will assign the follow subject data on the issued certificate: + - The common name (CN) will have the form + `{account}:{ca_service_id}:host:{host_id}`. + - A DNS subject alternative name will be added with the leaf + portion of the host. e.g. a host with id `production/cart/srv-01` + will include a DNS subject alternative name of `srv-01`. + - A SPIFFE SVID URI subject alternative name will be added of the form + `spiffe://conjur/{account}/{ca_service_id}/host/{host_id}` A full example of certificate signing is available -[here](https://github.com/conjurdemos/misc-util/tree/master/demos/certificate-authority/mutual-tls) \ No newline at end of file +[here](https://github.com/conjurdemos/misc-util/tree/master/demos/certificate-authority/mutual-tls)