Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Subject Name handling for Conjur CA certificates #712

Merged
merged 6 commits into from Sep 11, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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
Expand Down
11 changes: 1 addition & 10 deletions app/controllers/certificate_authority_controller.rb
Expand Up @@ -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

Expand All @@ -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)
Expand All @@ -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
Expand Down
48 changes: 43 additions & 5 deletions app/domain/ca/certificate_authority.rb
Expand Up @@ -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

Expand All @@ -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/<service_id>/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
Expand Down
8 changes: 3 additions & 5 deletions cucumber/api/features/certificate_authority.feature
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
28 changes: 24 additions & 4 deletions cucumber/api/features/step_definitions/ca_steps.rb
Expand Up @@ -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
12 changes: 12 additions & 0 deletions cucumber/api/features/support/ca_helpers.rb
Expand Up @@ -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
Expand Down
15 changes: 11 additions & 4 deletions docs/CERTIFICATE_SIGNING.md
Expand Up @@ -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/<account>/<ca_id>/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)
[here](https://github.com/conjurdemos/misc-util/tree/master/demos/certificate-authority/mutual-tls)