-
Notifications
You must be signed in to change notification settings - Fork 0
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
FI-2912: Implement trust verification of authorization server certificates #1
Conversation
@@ -13,8 +13,9 @@ class GenerateClientCertsTest < Inferno::Test | |||
input :udap_client_cert_pem, | |||
title: 'X.509 Client Certificate(s) (PEM Format)', | |||
description: %( | |||
A client X.509 certificate in PEM format. It MUST be signed by a cert trusted by the authorization server | |||
under test. | |||
A comma-separted list of one or more X.509 certificates in PEM format. The first (leaf) certificate MUST |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is a comma typically used as a delimiter for certs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as my research has indicated, there isn't really a standard delimiting character for certificate chains, rather when making a chain of certs (in PEM format) they are just appended like so:
---- BEGIN CERTIFICATE ----
...
---- END CERTIFICATE ----
---- BEGIN CERTIFICATE ----
...
---- END CERTIFICATE ----
Ideally there would be an existing Ruby/OpenSSL library method that splits a cert chain PEM string into an array of certs, but the OpenSSL library only supports loading in from a file path (see this OpenSSL PR), which won't work for Inferno test inputs.
For time and simplicity's sake I went ahead with a comma delimiter. If there's a straightforward way of doing this without the comma let me know though, I'm not very familiar with Regexes or existing Ruby features that would make this easy to do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ᐅ pry
[1] pry(main)> pem_string = <<~PEM
[1] pry(main)* ---- BEGIN CERTIFICATE ----
CERT1
---- END CERTIFICATE ----
---- BEGIN CERTIFICATE ----
CERT2
---- END CERTIFICATE ----
[1] pry(main)* PEM
=> "---- BEGIN CERTIFICATE ----\nCERT1\n---- END CERTIFICATE ----\n---- BEGIN CERTIFICATE ----\nCERT2\n---- END CERTIFICATE ----\n"
[2] pry(main)> regex = /(---- BEGIN CERTIFICATE ----
.*?
---- END CERTIFICATE ----)/
=> /(---- BEGIN CERTIFICATE ----
.*?
---- END CERTIFICATE ----)/
[3] pry(main)> pem_string.scan(regex)
=> [["---- BEGIN CERTIFICATE ----\nCERT1\n---- END CERTIFICATE ----"], ["---- BEGIN CERTIFICATE ----\nCERT2\n---- END CERTIFICATE ----"]]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Jammjammjamm I updated the code to use a regex instead of a comma, and refactored the splitting of user input to its own method for code reuse.
assert token_header.key?('x5c'), 'JWT header does not contain `x5c` field' | ||
assert token_header.key?('alg'), 'JWT header does not contain `alg` field' | ||
|
||
anchor_certs_parsed = udap_server_trust_anchor_certs.split(',') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These certs don't seem parsed. You should probably get rid of this variable so that you don't have to come up with a name for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, misnomer there, I meant "split" rather than parsed but removed the variable like you suggested.
OpenSSL::X509::Certificate.new(cert_der) | ||
end | ||
crl_uris = cert_chain.map(&:crl_uris).compact | ||
crl_uris = crl_uris.flatten |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there's no reason to separate flatten
from the rest of the method calls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leftover from a debugging session, fixed so it's all one line now!
# As a result, these capabilities are decoupled for testing purposes | ||
JWT::X5cKeyFinder.new(trust_anchor_certs, | ||
crls).from(x5c_header_encoded) | ||
[true, nil] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A hash is a much better way to return multiple values from a method than relying on array position to determine the meaning.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good to know, I refactored both validate_trust_chain
and validate_signature
to return a hash with entries for success
and error_message
.
@@ -0,0 +1,30 @@ | |||
-----BEGIN CERTIFICATE----- |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't feel good about having someone else's certs in our repo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I totally get that, unfortunately until our self-generated certs are up to snuff, others' certs are all that are available for testing the trust chain validation method. The good-ish news is that, based on the way I broke this up the validate trust chain method does not require UDAP compliant certs, just any valid set of certificates.
Have you heard of there being publicly available test certificates for use cases like this? If we can find some we could include those instead. The alternative is to remove the trust chain validation unit tests (for both the module and the Inferno test) from the test kit since both use the EMR Direct certs. What are your thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Files which are only used for unit tests should live in spec/fixtures
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good to know, I moved the EMR Direct certs to spec/fixtures
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this looks good, but unit tests and linting need to pass.
result = run(runnable, signed_metadata_jwt: create_test_jwt, | ||
udap_server_trust_anchor_certs: invalid_trust_anchor) | ||
expect(result.result).to eq('fail') | ||
expect(result.result_message).to match(self_signed_cert_error) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Running locally, I get:
Failures:
1) UDAPSecurity::SignedMetadataTrustVerificationTest when JWT includes client, intermediate, and root certs fails when incorrect root CA is provided as trust anchor
Failure/Error: expect(result.result_message).to match(self_signed_cert_error)
expected "Trust could not be established with server certificates, error message: /Users/smacvicar/.asdf/insta...icar/.asdf/installs/ruby/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.12.0/exe/rspec:4:in `<main>'\n" to match "self-signed certificate in certificate chain"
Diff:
@@ -1,62 +1,123 @@
-self-signed certificate in certificate chain
+Trust could not be established with server certificates, error message: /Users/smacvicar/.asdf/installs/ruby/3.1.2/lib/ruby/gems/3.1.0/gems/jwt-2.7.1/lib/jwt/x5c_key_finder.rb:30:in `from': Certificate verification failed: self signed certificate in certificate chain. Certificate subject: /C=US/ST=CA/L=San Diego/O=EMR Direct/OU=Certification Authority (certs.emrdirect.com)/CN=EMR Direct Test CA. (JWT::VerificationError)
+ from /Users/smacvicar/code/udap-test-kit/lib/udap_security/udap_jwt_validator.rb:45:in `validate_trust_chain'
+ from /Users/smacvicar/code/udap-test-kit/lib/udap_security/signed_metadata_trust_verification_test.rb:44:in `block in <class:SignedMetadataTrustVerificationTest>'
+ from /Users/smacvicar/.asdf/installs/ruby/3.1.2/lib/ruby/gems/3.1.0/gems/inferno_core-0.4.20/lib/inferno/test_runner.rb:77:in `instance_eval'
+ from /Users/smacvicar/.asdf/installs/ruby/3.1.2/lib/ruby/gems/3.1.0/gems/inferno_core-0.4.20/lib/inferno/test_runner.rb:77:in `run_test'
+ from /Users/smacvicar/.asdf/installs/ruby/3.1.2/lib/ruby/gems/3.1.0/gems/inferno_core-0.4.20/lib/inferno/test_runner.rb:51:in `run'
+ from /Users/smacvicar/code/udap-test-kit/spec/udap_security/signed_metadata_trust_verification_test_spec.rb:65:in `run'
+ from /Users/smacvicar/code/udap-test-kit/spec/udap_security/signed_metadata_trust_verification_test_spec.rb:120:in `block (3 levels) in <top (required)>'
I see it passes on CI, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Odd, looks like semantically the error message is the same but on your machine it's self signed
and on mine/CI it's self-signed
. I updated the expected error messages to use a regex which should fix this.
@Jammjammjamm I believe I fixed the source of the failing unit test but it seems to be machine-dependent (I had no errors with it initially or now), so maybe verify it's also passing on your machine. Also, I fixed all the linting errors locally and rubocop runs clean for me, but on CI I'm seeing the following error that I don't know how to fix:
|
Summary
This PR implements Inferno (as a mock UDAP client) establishing trust with the authorization server's X.509 certificates provided in the server's Discovery response, specifically the
signed_metadata
JWT element. This behavior is included in an additional, optional test and requires testers to provide one or more trust anchor certificates (i.e., root CAs) as test inputs.Additional notes:
x5c
header certificates are intentionally separated into two separate steps to better support unit testing and error debugging of both capabilities.x5c
header. Dynamic Client registration tests and their inputs were updated to accept a comma separated list of one or more certs instead of a single certTesting Guidance
EMRDirectTestIntermediateCA.pem
andEMRDirectTestRootCA.pem
(inlib/udap_security/certs/testing
)