udap_security_test_kit 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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