udap_security_test_kit 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/lib/udap_security_test_kit/authorization_code_authentication_group.rb +44 -0
  4. data/lib/udap_security_test_kit/authorization_code_group.rb +103 -0
  5. data/lib/udap_security_test_kit/authorization_code_received_test.rb +31 -0
  6. data/lib/udap_security_test_kit/authorization_code_redirect_test.rb +74 -0
  7. data/lib/udap_security_test_kit/authorization_code_token_exchange_test.rb +103 -0
  8. data/lib/udap_security_test_kit/authorization_endpoint_field_test.rb +43 -0
  9. data/lib/udap_security_test_kit/certs/InfernoCA.key +52 -0
  10. data/lib/udap_security_test_kit/certs/InfernoCA.pem +35 -0
  11. data/lib/udap_security_test_kit/certs/TestClient.pem +32 -0
  12. data/lib/udap_security_test_kit/certs/TestClientPrivateKey.key +28 -0
  13. data/lib/udap_security_test_kit/client_credentials_authentication_group.rb +40 -0
  14. data/lib/udap_security_test_kit/client_credentials_group.rb +105 -0
  15. data/lib/udap_security_test_kit/client_credentials_token_exchange_test.rb +130 -0
  16. data/lib/udap_security_test_kit/common_assertions.rb +16 -0
  17. data/lib/udap_security_test_kit/default_cert_file_loader.rb +27 -0
  18. data/lib/udap_security_test_kit/discovery_group.rb +90 -0
  19. data/lib/udap_security_test_kit/dynamic_client_registration_group.rb +129 -0
  20. data/lib/udap_security_test_kit/generate_client_certs_test.rb +60 -0
  21. data/lib/udap_security_test_kit/grant_types_supported_field_test.rb +53 -0
  22. data/lib/udap_security_test_kit/reg_endpoint_jwt_signing_alg_values_supported_field_test.rb +29 -0
  23. data/lib/udap_security_test_kit/registration_endpoint_field_test.rb +30 -0
  24. data/lib/udap_security_test_kit/registration_failure_invalid_contents_test.rb +68 -0
  25. data/lib/udap_security_test_kit/registration_failure_invalid_jwt_signature_test.rb +70 -0
  26. data/lib/udap_security_test_kit/registration_success_contents_test.rb +64 -0
  27. data/lib/udap_security_test_kit/registration_success_test.rb +68 -0
  28. data/lib/udap_security_test_kit/scopes_supported_field_test.rb +26 -0
  29. data/lib/udap_security_test_kit/signed_metadata_contents_test.rb +89 -0
  30. data/lib/udap_security_test_kit/signed_metadata_field_test.rb +31 -0
  31. data/lib/udap_security_test_kit/signed_metadata_trust_verification_test.rb +54 -0
  32. data/lib/udap_security_test_kit/software_statement_builder.rb +32 -0
  33. data/lib/udap_security_test_kit/token_endpoint_auth_methods_supported_field_test.rb +22 -0
  34. data/lib/udap_security_test_kit/token_endpoint_auth_signing_alg_values_supported_field_test.rb +32 -0
  35. data/lib/udap_security_test_kit/token_endpoint_field_test.rb +30 -0
  36. data/lib/udap_security_test_kit/token_exchange_response_body_test.rb +30 -0
  37. data/lib/udap_security_test_kit/token_exchange_response_headers_test.rb +30 -0
  38. data/lib/udap_security_test_kit/udap_auth_extensions_required_field_test.rb +38 -0
  39. data/lib/udap_security_test_kit/udap_auth_extensions_supported_field_test.rb +31 -0
  40. data/lib/udap_security_test_kit/udap_certifications_required_field_test.rb +45 -0
  41. data/lib/udap_security_test_kit/udap_certifications_supported_field_test.rb +33 -0
  42. data/lib/udap_security_test_kit/udap_client_assertion_payload_builder.rb +15 -0
  43. data/lib/udap_security_test_kit/udap_jwt_builder.rb +30 -0
  44. data/lib/udap_security_test_kit/udap_jwt_validator.rb +71 -0
  45. data/lib/udap_security_test_kit/udap_profiles_supported_field_test.rb +47 -0
  46. data/lib/udap_security_test_kit/udap_request_builder.rb +43 -0
  47. data/lib/udap_security_test_kit/udap_versions_supported_field_test.rb +21 -0
  48. data/lib/udap_security_test_kit/udap_x509_certificate.rb +42 -0
  49. data/lib/udap_security_test_kit/version.rb +3 -0
  50. data/lib/udap_security_test_kit/well_known_endpoint_test.rb +31 -0
  51. data/lib/udap_security_test_kit.rb +63 -0
  52. metadata +124 -0
@@ -0,0 +1,60 @@
1
+ require_relative 'udap_x509_certificate'
2
+ require_relative 'default_cert_file_loader'
3
+
4
+ module UDAPSecurityTestKit
5
+ class GenerateClientCertsTest < Inferno::Test
6
+ title 'Generate Client Certificates'
7
+ id :udap_generate_client_certs
8
+ description %(
9
+ This test may be included in test groups to generate and output a new client certificate for use in UDAP dynamic
10
+ client registration or authentication/authorization tests.
11
+ )
12
+
13
+ input :udap_client_cert_pem,
14
+ title: 'X.509 Client Certificate(s) (PEM Format)',
15
+ description: %(
16
+ A list of one or more X.509 certificates in PEM format separated by a newline. The first (leaf) certificate
17
+ MUST represent the client entity and the certificate chain must resolve to a CA trusted by the authorization
18
+ server under test.
19
+ Will be auto-generated if left blank.
20
+ ),
21
+ type: 'textarea',
22
+ optional: true
23
+
24
+ input :udap_client_private_key_pem,
25
+ title: 'Client Private Key (PEM Format)',
26
+ description: %(
27
+ The private key corresponding to the client certificate used for registration, in PEM format. Used to sign
28
+ registration and/or authentication JWTs.
29
+ Will be auto-generated if left blank.
30
+ ),
31
+ type: 'textarea',
32
+ optional: true
33
+
34
+ input :udap_cert_iss,
35
+ title: 'JWT Issuer (iss) Claim',
36
+ description: %(
37
+ MUST correspond to a unique URI entry in the Subject Alternative Name (SAN) extension of the client
38
+ certificate used for registration.
39
+ Will be auto-generated with the client cert if left blank.
40
+ ),
41
+ optional: true
42
+
43
+ output :udap_cert_iss
44
+ output :udap_client_cert_pem
45
+ output :udap_client_private_key_pem
46
+
47
+ run do
48
+ omit_if udap_client_cert_pem.present? && udap_client_private_key_pem.present?,
49
+ 'User has opted to provide client certs'
50
+
51
+ signing_key = DefaultCertFileLoader.load_default_ca_private_key_file
52
+
53
+ cert = UDAPX509Certificate.new(DefaultCertFileLoader.load_default_ca_pem_file, signing_key)
54
+
55
+ output udap_cert_iss: cert.san
56
+ output udap_client_cert_pem: cert.cert.to_pem
57
+ output udap_client_private_key_pem: cert.cert_private_key.to_pem
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,53 @@
1
+ require_relative 'common_assertions'
2
+
3
+ module UDAPSecurityTestKit
4
+ extend CommonAssertions
5
+ class GrantTypesSupportedFieldTest < Inferno::Test
6
+ title 'grant_types_supported field'
7
+ id :udap_grant_types_supported_field
8
+ description %(
9
+ `grant_types_supported` is an array of one or more grant types
10
+ )
11
+
12
+ input :udap_well_known_metadata_json
13
+ input :required_flow_type
14
+ output :udap_registration_grant_type
15
+
16
+ run do
17
+ assert_valid_json(udap_well_known_metadata_json)
18
+ config = JSON.parse(udap_well_known_metadata_json)
19
+
20
+ assert config.key?('grant_types_supported'), '`grant_types_supported` is a required field'
21
+
22
+ CommonAssertions.assert_array_of_strings(config, 'grant_types_supported')
23
+
24
+ grant_types = config['grant_types_supported']
25
+
26
+ assert grant_types.present?, 'Must include at least 1 supported grant type'
27
+
28
+ if grant_types.include?('refresh_token')
29
+ assert grant_types.include?('authorization_code'),
30
+ 'The `refresh_token` grant type **SHALL** only be included if the `authorization_code` grant type is ' \
31
+ 'also included.'
32
+ end
33
+
34
+ if required_flow_type.include? 'authorization_code'
35
+ assert grant_types.include?('authorization_code'), 'grant types must
36
+ include authorization_code for this workflow'
37
+
38
+ unless required_flow_type.include? 'client_credentials'
39
+ output udap_registration_grant_type: 'authorization_code'
40
+ end
41
+ end
42
+
43
+ if required_flow_type.include? 'client_credentials'
44
+ assert grant_types.include?('client_credentials'),
45
+ 'grant types must include client_credentials for this workflow'
46
+
47
+ unless required_flow_type.include? 'authorization_code'
48
+ output udap_registration_grant_type: 'client_credentials'
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,29 @@
1
+ require_relative 'common_assertions'
2
+
3
+ module UDAPSecurityTestKit
4
+ extend CommonAssertions
5
+ class RegEndpointJWTSigningAlgValuesSupportedFieldTest < Inferno::Test
6
+ include Inferno::DSL::Assertions
7
+
8
+ title 'registration_endpoint_jwt_signing_alg_values_supported field'
9
+ id :udap_reg_endpoint_jwt_signing_alg_values_supported_field
10
+ description %(
11
+ If present, `registration_endpoint_jwt_signing_alg_values_supported` is
12
+ an array of one or more strings identifying signature algorithms supported by the Authorization Server for
13
+ validation of signed software statements, certifications, and endorsements
14
+ submitted to the registration endpoint.
15
+ )
16
+
17
+ input :udap_well_known_metadata_json
18
+
19
+ run do
20
+ assert_valid_json(udap_well_known_metadata_json)
21
+ config = JSON.parse(udap_well_known_metadata_json)
22
+
23
+ omit_if !config.key?('registration_endpoint_jwt_signing_alg_values_supported'),
24
+ '`registration_endpoint_jwt_signing_alg_values_supported` field is recommended but not required'
25
+
26
+ CommonAssertions.assert_array_of_strings(config, 'registration_endpoint_jwt_signing_alg_values_supported')
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,30 @@
1
+ module UDAPSecurityTestKit
2
+ class RegistrationEndpointFieldTest < Inferno::Test
3
+ include Inferno::DSL::Assertions
4
+
5
+ title 'registration_endpoint field'
6
+ id :udap_registration_endpoint_field
7
+ description %(
8
+ `registration_endpoint` is a string containing the URL of
9
+ the Authorization Server's registration endpoint
10
+ )
11
+
12
+ input :udap_well_known_metadata_json
13
+ output :udap_registration_endpoint
14
+
15
+ run do
16
+ assert_valid_json(udap_well_known_metadata_json)
17
+ config = JSON.parse(udap_well_known_metadata_json)
18
+
19
+ assert config.key?('registration_endpoint'), '`registration_endpoint` is a required field'
20
+
21
+ endpoint = config['registration_endpoint']
22
+
23
+ assert endpoint.is_a?(String),
24
+ "`registration_endpoint` should be a String, but found #{endpoint.class.name}"
25
+ assert_valid_http_uri(endpoint, "`#{endpoint}` is not a valid URI")
26
+
27
+ output udap_registration_endpoint: endpoint
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,68 @@
1
+ require_relative 'software_statement_builder'
2
+ require_relative 'udap_jwt_builder'
3
+
4
+ module UDAPSecurityTestKit
5
+ class RegistrationFailureInvalidContentsTest < Inferno::Test
6
+ title 'Dynamic Client Registration request fails with improper software statement contents'
7
+ id :udap_registration_failure_invalid_contents
8
+ description %(
9
+ The [UDAP IG Section 3.1](https://hl7.org/fhir/us/udap-security/STU1/registration.html#software-statement) states:
10
+ > The unique client URI used for the iss claim SHALL match the uriName entry in the Subject Alternative Name
11
+ > extension of the client app operator’s X.509 certificate, and SHALL uniquely identify a single client app
12
+ > operator and application over time
13
+
14
+ The [UDAP IG Section 3.2.3](https://hl7.org/fhir/us/udap-security/STU1/registration.html#request-body) states:
15
+ > The Authorization Server SHALL validate the registration request as per Section 4 of UDAP Dynamic Client
16
+ > Registration. This includes validation of the JWT payload and signature, validation of the X.509 certificate
17
+ > chain, and **validation of the requested application registration parameters**.
18
+
19
+ This test will provide a software statement whose `iss` value does not match the uriName entry in the client's
20
+ certificate. The authorization server must reject this request with a 400 response code per
21
+ [RFC 7591 OAuth 2.0 Dynamic Client Registration Protocol Section 3.2.2](https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.2):
22
+ > When a registration error condition occurs, the authorization server
23
+ *returns an HTTP 400 status code* (unless otherwise specified) with
24
+ content type "application/json" consisting of a JSON object
25
+ describing the error in the response body.
26
+ )
27
+
28
+ input :udap_client_cert_pem
29
+ input :udap_client_private_key_pem
30
+
31
+ input :udap_registration_endpoint
32
+ input :udap_jwt_signing_alg
33
+ input :udap_registration_requested_scope
34
+ input :udap_registration_grant_type
35
+ input :udap_registration_certifications,
36
+ optional: true
37
+
38
+ run do
39
+ software_statement_payload = SoftwareStatementBuilder.build_payload(
40
+ 'invalid_iss',
41
+ udap_registration_endpoint,
42
+ udap_registration_grant_type,
43
+ udap_registration_requested_scope
44
+ )
45
+
46
+ x5c_certs = UDAPSecurityTestKit::UDAPJWTBuilder.split_user_input_cert_string(
47
+ udap_client_cert_pem
48
+ )
49
+
50
+ signed_jwt = UDAPSecurityTestKit::UDAPJWTBuilder.encode_jwt_with_x5c_header(
51
+ software_statement_payload,
52
+ udap_client_private_key_pem,
53
+ udap_jwt_signing_alg,
54
+ x5c_certs
55
+ )
56
+
57
+ registration_headers, registration_body = UDAPSecurityTestKit::UDAPRequestBuilder.build_registration_request(
58
+ signed_jwt,
59
+ udap_registration_certifications
60
+ )
61
+
62
+ post(udap_registration_endpoint, body: registration_body, headers: registration_headers)
63
+
64
+ assert_response_status(400)
65
+ assert_valid_json(response[:body])
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,70 @@
1
+ require_relative 'software_statement_builder'
2
+ require_relative 'udap_jwt_builder'
3
+ require_relative 'udap_request_builder'
4
+
5
+ module UDAPSecurityTestKit
6
+ class RegistrationFailureInvalidJWTSignatureTest < Inferno::Test
7
+ title 'Dynamic Client Registration request fails when software statement JWT is improperly signed'
8
+ id :udap_registration_failure_invalid_jwt_signature
9
+ description %(
10
+ The [UDAP IG Section 3.2.3](https://hl7.org/fhir/us/udap-security/STU1/registration.html#request-body) states:
11
+ > The Authorization Server SHALL validate the registration request as per Section 4 of UDAP Dynamic Client
12
+ > Registration. This includes **validation of the JWT payload and signature**, validation of the X.509 certificate
13
+ > chain, and validation of the requested application registration parameters.
14
+
15
+ Additionally, the [UDAP IG Section 1.2.3](https://hl7.org/fhir/us/udap-security/STU1/#jwt-headers) states that the
16
+ required `x5c` JWT header value is "An array of one or more strings containing the X.509 certificate or
17
+ certificate chain, where **the leaf certificate corresponds to the key used to digitally sign the JWT.**"
18
+
19
+ This test will provide a software statement signed with a randomly generated private key that does not correspond
20
+ to the client certificate included in the x5c header claim.
21
+ The authorization server must reject this request with a 400 response code per
22
+ [RFC 7591 OAuth 2.0 Dynamic Client Registration Protocol Section 3.2.2](https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.2):
23
+ > When a registration error condition occurs, the authorization server
24
+ *returns an HTTP 400 status code* (unless otherwise specified) with
25
+ content type "application/json" consisting of a JSON object
26
+ describing the error in the response body.
27
+ )
28
+
29
+ input :udap_client_cert_pem
30
+ input :udap_cert_iss
31
+
32
+ input :udap_registration_endpoint
33
+ input :udap_jwt_signing_alg
34
+ input :udap_registration_requested_scope
35
+ input :udap_registration_grant_type
36
+ input :udap_registration_certifications,
37
+ optional: true
38
+
39
+ run do
40
+ software_statement_payload = SoftwareStatementBuilder.build_payload(
41
+ udap_cert_iss,
42
+ udap_registration_endpoint,
43
+ udap_registration_grant_type,
44
+ udap_registration_requested_scope
45
+ )
46
+
47
+ x5c_certs = UDAPSecurityTestKit::UDAPJWTBuilder.split_user_input_cert_string(
48
+ udap_client_cert_pem
49
+ )
50
+
51
+ random_private_key = OpenSSL::PKey::RSA.generate 2048
52
+ signed_jwt = UDAPSecurityTestKit::UDAPJWTBuilder.encode_jwt_with_x5c_header(
53
+ software_statement_payload,
54
+ random_private_key.to_pem,
55
+ udap_jwt_signing_alg,
56
+ x5c_certs
57
+ )
58
+
59
+ reg_headers, reg_body = UDAPSecurityTestKit::UDAPRequestBuilder.build_registration_request(
60
+ signed_jwt,
61
+ udap_registration_certifications
62
+ )
63
+
64
+ post(udap_registration_endpoint, body: reg_body, headers: reg_headers)
65
+
66
+ assert_response_status(400)
67
+ assert_valid_json(response[:body])
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,64 @@
1
+ module UDAPSecurityTestKit
2
+ class RegistrationSuccessContentsTest < Inferno::Test
3
+ title 'Successful Dynamic Client Registration request response contains required contents'
4
+ id :udap_registration_success_contents
5
+ description %(
6
+ The [UDAP IG Section 3.2.3](https://hl7.org/fhir/us/udap-security/STU1/registration.html#request-body) states:
7
+ > If a new registration is successful, the Authorization Server SHALL return a registration response with a 201
8
+ > Created HTTP response code as per Section 5.1 of UDAP Dynamic Client Registration, including the unique
9
+ > client_id assigned by the Authorization Server to that client app.
10
+
11
+ And the [UDAP Profile Section 5.1](https://www.udap.org/udap-dynamic-client-registration-stu1.html#section-5.1)
12
+ states:
13
+ > If the request is granted, the Authorization Server returns a registration response as per Section 3.2.1 of RFC
14
+ > 7591. The top-level elements of the response SHALL include the client_id issued by the Authorization Server for
15
+ > use by the Client App, the software statement as submitted by the Client App, and all of the registration
16
+ > related parameters that were included in the software statement.
17
+
18
+ This test verifies:
19
+ - `client_id` claim is included in the registration response and its value is not blank
20
+ - `software_statement` claim in the registration response matches the software statement JWT provided in the
21
+ original registration request
22
+ - The registration response includes claims for `client_name`, `grant_types`, `token_endpoint_auth_method`, and
23
+ `scope`, whose values match those in the originally submitted software statement
24
+ - If the registered grant type is `authorization_code`, then the response includes claims for `redirect_uris` and
25
+ `response_type` whose values match those in the originally submitted software statement
26
+ )
27
+
28
+ input :udap_software_statement_json
29
+ input :udap_software_statement_jwt
30
+ input :udap_registration_response
31
+ input :udap_registration_grant_type
32
+
33
+ output :udap_client_id
34
+
35
+ run do
36
+ assert_valid_json(udap_registration_response)
37
+ registration_response = JSON.parse(udap_registration_response)
38
+
39
+ assert registration_response.key?('client_id'), 'Successful registration response must contain a client_id'
40
+ client_id = registration_response['client_id']
41
+ assert client_id.present?, 'client_id cannot be blank'
42
+
43
+ output udap_client_id: client_id
44
+
45
+ assert registration_response['software_statement'] == udap_software_statement_jwt,
46
+ 'Successful registration response must include the ' \
47
+ 'software statement JWT submitted by client'
48
+
49
+ original_software_statement = JSON.parse(udap_software_statement_json)
50
+
51
+ expected_claims = ['client_name', 'grant_types', 'token_endpoint_auth_method', 'scope']
52
+ auth_code_claims = ['redirect_uris', 'response_types']
53
+
54
+ expected_claims.concat auth_code_claims if udap_registration_grant_type == 'authorization_code'
55
+
56
+ expected_claims.each do |claim|
57
+ assert registration_response.key?(claim), "Successful registration response must include #{claim} claim"
58
+ assert registration_response[claim] == original_software_statement[claim],
59
+ "Registration response value for #{claim} does not match " \
60
+ 'in client-submitted software statement'
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,68 @@
1
+ require_relative 'software_statement_builder'
2
+ require_relative 'udap_jwt_builder'
3
+
4
+ module UDAPSecurityTestKit
5
+ class RegistrationSuccessTest < Inferno::Test
6
+ title 'Dynamic Client Registration request succeeds with valid software statement, JWT signature, and client certs'
7
+
8
+ id :udap_registration_success
9
+ description %(
10
+ When the Dynamic Client registration request includes a properly signed software statement JWT with the required
11
+ contents, the registration request should succeed.
12
+
13
+ The [UDAP IG Section 3.2.3](https://hl7.org/fhir/us/udap-security/STU1/registration.html#request-body) states:
14
+ > If a new registration is successful, the Authorization Server SHALL return a registration response with a 201
15
+ > Created HTTP response code as per Section 5.1 of UDAP Dynamic Client Registration
16
+ )
17
+
18
+ input :udap_client_cert_pem
19
+ input :udap_client_private_key_pem
20
+ input :udap_cert_iss
21
+
22
+ input :udap_registration_endpoint
23
+ input :udap_jwt_signing_alg
24
+ input :udap_registration_requested_scope
25
+ input :udap_registration_grant_type
26
+ input :udap_registration_certifications,
27
+ optional: true
28
+
29
+ output :udap_software_statement_jwt
30
+ output :udap_software_statement_json
31
+ output :udap_registration_response
32
+
33
+ run do
34
+ software_statement_payload = SoftwareStatementBuilder.build_payload(
35
+ udap_cert_iss,
36
+ udap_registration_endpoint,
37
+ udap_registration_grant_type,
38
+ udap_registration_requested_scope
39
+ )
40
+
41
+ output udap_software_statement_json: software_statement_payload.to_json
42
+
43
+ x5c_certs = UDAPSecurityTestKit::UDAPJWTBuilder.split_user_input_cert_string(
44
+ udap_client_cert_pem
45
+ )
46
+
47
+ signed_jwt = UDAPSecurityTestKit::UDAPJWTBuilder.encode_jwt_with_x5c_header(
48
+ software_statement_payload,
49
+ udap_client_private_key_pem,
50
+ udap_jwt_signing_alg,
51
+ x5c_certs
52
+ )
53
+
54
+ output udap_software_statement_jwt: signed_jwt
55
+
56
+ reg_headers, reg_body = UDAPSecurityTestKit::UDAPRequestBuilder.build_registration_request(
57
+ signed_jwt,
58
+ udap_registration_certifications
59
+ )
60
+
61
+ post(udap_registration_endpoint, body: reg_body, headers: reg_headers)
62
+
63
+ assert_response_status(201)
64
+ assert_valid_json(response[:body])
65
+ output udap_registration_response: response[:body]
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,26 @@
1
+ require_relative 'common_assertions'
2
+
3
+ module UDAPSecurityTestKit
4
+ extend CommonAssertions
5
+ class ScopesSupportedFieldTest < Inferno::Test
6
+ include Inferno::DSL::Assertions
7
+
8
+ title 'scopes_supported field'
9
+ id :udap_scopes_supported_field
10
+ description %(
11
+ If present, `scopes_supported` is an array of one or more
12
+ strings containing scopes
13
+ )
14
+
15
+ input :udap_well_known_metadata_json
16
+
17
+ run do
18
+ assert_valid_json(udap_well_known_metadata_json)
19
+ config = JSON.parse(udap_well_known_metadata_json)
20
+
21
+ omit_if !config.key?('scopes_supported')
22
+
23
+ CommonAssertions.assert_array_of_strings(config, 'scopes_supported')
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,89 @@
1
+ require 'jwt'
2
+ require_relative 'udap_jwt_validator'
3
+ module UDAPSecurityTestKit
4
+ class SignedMetadataContentsTest < Inferno::Test
5
+ include Inferno::DSL::Assertions
6
+
7
+ title 'signed_metadata contents'
8
+ id :udap_signed_metadata_contents
9
+ description %(
10
+ `signed_metadata` is a string containing a JWT listing the server's endpoints. This test will validate the JWT
11
+ signature as specified in [UDAP IG Section 1.2 JSON Web Token (JWT) Requirements](https://hl7.org/fhir/us/udap-security/STU1/index.html#json-web-token-jwt-requirements)
12
+ and validate the JWT contents as outlined in [UDAP Discovery IG Section 2.3 Signed Metadata Elements](https://hl7.org/fhir/us/udap-security/STU1/discovery.html#signed-metadata-elements).
13
+ )
14
+
15
+ input :signed_metadata_jwt, optional: true
16
+ input :udap_well_known_metadata_json, :udap_fhir_base_url
17
+
18
+ run do
19
+ skip_if signed_metadata_jwt.blank?
20
+
21
+ assert_valid_json(udap_well_known_metadata_json)
22
+ config = JSON.parse(udap_well_known_metadata_json)
23
+
24
+ token_body, token_header = JWT.decode(signed_metadata_jwt, nil, false)
25
+
26
+ assert token_header.key?('x5c'), 'JWT header does not contain `x5c` field'
27
+ assert token_header.key?('alg'), 'JWT header does not contain `alg` field'
28
+
29
+ leaf_cert_der = Base64.urlsafe_decode64(token_header['x5c'].first)
30
+ leaf_cert = OpenSSL::X509::Certificate.new(leaf_cert_der)
31
+ signature_validation_result = UDAPSecurityTestKit::UDAPJWTValidator.validate_signature(
32
+ signed_metadata_jwt,
33
+ token_header['alg'],
34
+ leaf_cert
35
+ )
36
+
37
+ assert signature_validation_result[:success], signature_validation_result[:error_message]
38
+
39
+ ['iss', 'sub', 'exp', 'iat', 'jti'].each do |key|
40
+ assert token_body.key?(key), "JWT does not contain `#{key}` claim"
41
+ end
42
+
43
+ ['token_endpoint', 'registration_endpoint']
44
+ .each do |key|
45
+ assert token_body.key?(key), "JWT must contain `#{key}` claim"
46
+ assert token_body[key].is_a?(String), "Value for `#{key}` must be a String"
47
+ end
48
+
49
+ if config.key?('authorization_endpoint')
50
+ assert token_body.key?('authorization_endpoint'),
51
+ 'JWT must contain `authorization_endpoint` key because it is present in unsigned metadata'
52
+ assert token_body['authorization_endpoint'].is_a?(String), 'Value for `authorization_endpoint` must be a String'
53
+
54
+ assert token_body['iss'] == udap_fhir_base_url,
55
+ "`iss` claim `#{token_body['iss']}` is not the same as server base url `#{udap_fhir_base_url}`"
56
+
57
+ begin
58
+ alt_names =
59
+ leaf_cert.extensions
60
+ .find { |extension| extension.oid == 'subjectAltName' }
61
+ .value
62
+ rescue NoMethodError
63
+ assert false, 'Could not find Subject Alternative Name extension in leaf certificate'
64
+ end
65
+
66
+ # Certification may have more than one SAN value
67
+ assert alt_names.include?("URI:#{token_body['iss']}"),
68
+ "`iss` claim `#{token_body['iss']}` not found in Subject Alternative Name extension " \
69
+ "from the `x5c` JWT header: `#{alt_names}`"
70
+
71
+ assert token_body['iss'] == token_body['sub'],
72
+ "`iss` claim `#{token_body['iss']}` does not match `sub` claim `#{token_body['sub']}`"
73
+
74
+ ['iat', 'exp'].each do |key|
75
+ assert token_body[key].is_a?(Numeric),
76
+ "Expected `#{key}` to be numeric, but found #{token_body[key].class.name}"
77
+ end
78
+ issue_time = Time.at(token_body['iat'])
79
+ expiration_time = Time.at(token_body['exp'])
80
+
81
+ assert expiration_time <= issue_time + 1.year, %(
82
+ `exp` is more than a year after `iat`'.
83
+ * `iat`: #{token_body['iat']} - #{issue_time.iso8601}
84
+ * `exp`: #{token_body['exp']} - #{expiration_time.iso8601}
85
+ )
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,31 @@
1
+ require 'jwt'
2
+
3
+ module UDAPSecurityTestKit
4
+ class SignedMetadataFieldTest < Inferno::Test
5
+ include Inferno::DSL::Assertions
6
+
7
+ title 'signed_metadata field'
8
+ id :udap_signed_metadata_field
9
+ description %(
10
+ `signed_metadata` is a string containing a JWT listing the server's endpoints
11
+ )
12
+
13
+ input :udap_well_known_metadata_json
14
+ output :signed_metadata_jwt
15
+
16
+ run do
17
+ assert_valid_json(udap_well_known_metadata_json)
18
+ config = JSON.parse(udap_well_known_metadata_json)
19
+
20
+ assert config.key?('signed_metadata'), '`signed_metadata is a required field'
21
+ jwt = config['signed_metadata']
22
+
23
+ assert jwt.is_a?(String), "`signed_metadata` should be a String, but found #{jwt.class.name}"
24
+ output signed_metadata_jwt: jwt
25
+
26
+ jwt_regex = %r{^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$}
27
+
28
+ assert jwt.match?(jwt_regex), '`signed_metadata` is not a valid JWT'
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,54 @@
1
+ require 'jwt'
2
+ require_relative 'udap_jwt_validator'
3
+ module UDAPSecurityTestKit
4
+ class SignedMetadataTrustVerificationTest < Inferno::Test
5
+ include Inferno::DSL::Assertions
6
+
7
+ title 'signed_metadata contents: trust can be verified from server certificates'
8
+ id :udap_signed_metadata_trust_verification
9
+ description %(
10
+ The [UDAP IG profile on UDAP Server Metadata Section 3.2](https://www.udap.org/udap-server-metadata.html) says:
11
+ > The Client app attempts to construct a valid certificate chain from the Server’s certificate (cert1) to an
12
+ > anchor certificate trusted by the Client app using conventional X.509 chain building techniques and path
13
+ > validation, including certificate validity and revocation status checking. The Server MAY provide a complete
14
+ > certificate chain in the x5c element. The Client app MAY use additional certificates not included by the Server
15
+ > to construct a chain.
16
+
17
+ This test will establish trust against the root CA(s) provided as test inputs.
18
+ Currently, the use of Authority Information Access (AIA) extensions is NOT supported. As such, servers must
19
+ include any intermediate CAs necessary for building a trust chain in the JWT `x5c` header OR as an additional
20
+ trust anchor certificate input to the test (see input instructions for more details).
21
+ )
22
+
23
+ input :signed_metadata_jwt
24
+ input :udap_server_trust_anchor_certs,
25
+ title: 'Auth Server Trust Anchor X509 Certificate(s) (PEM Format)',
26
+ description: %(
27
+ A list of one or more trust anchor root CA X.509 certificates, separated by a newline. Inferno will use
28
+ these to establish
29
+ trust with the authorization server's certificates provided in the discovery response signed_metadata JWT.
30
+ ),
31
+ type: 'textarea'
32
+
33
+ run do
34
+ skip_if udap_server_trust_anchor_certs.blank?
35
+ _token_body, token_header = JWT.decode(signed_metadata_jwt, nil, false)
36
+
37
+ assert token_header.key?('x5c'), 'JWT header does not contain `x5c` field'
38
+ assert token_header.key?('alg'), 'JWT header does not contain `alg` field'
39
+
40
+ trust_anchor_certs = UDAPJWTBuilder.split_user_input_cert_string(udap_server_trust_anchor_certs).map do |cert_pem|
41
+ OpenSSL::X509::Certificate.new(cert_pem)
42
+ end
43
+
44
+ validation_result = UDAPJWTValidator.validate_trust_chain(
45
+ token_header['x5c'],
46
+ trust_anchor_certs
47
+ )
48
+
49
+ assert validation_result[:success],
50
+ "Trust could not be established with server certificates, /
51
+ error message: #{validation_result[:error_message]}"
52
+ end
53
+ end
54
+ end