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.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/lib/udap_security_test_kit/authorization_code_authentication_group.rb +44 -0
- data/lib/udap_security_test_kit/authorization_code_group.rb +103 -0
- data/lib/udap_security_test_kit/authorization_code_received_test.rb +31 -0
- data/lib/udap_security_test_kit/authorization_code_redirect_test.rb +74 -0
- data/lib/udap_security_test_kit/authorization_code_token_exchange_test.rb +103 -0
- data/lib/udap_security_test_kit/authorization_endpoint_field_test.rb +43 -0
- data/lib/udap_security_test_kit/certs/InfernoCA.key +52 -0
- data/lib/udap_security_test_kit/certs/InfernoCA.pem +35 -0
- data/lib/udap_security_test_kit/certs/TestClient.pem +32 -0
- data/lib/udap_security_test_kit/certs/TestClientPrivateKey.key +28 -0
- data/lib/udap_security_test_kit/client_credentials_authentication_group.rb +40 -0
- data/lib/udap_security_test_kit/client_credentials_group.rb +105 -0
- data/lib/udap_security_test_kit/client_credentials_token_exchange_test.rb +130 -0
- data/lib/udap_security_test_kit/common_assertions.rb +16 -0
- data/lib/udap_security_test_kit/default_cert_file_loader.rb +27 -0
- data/lib/udap_security_test_kit/discovery_group.rb +90 -0
- data/lib/udap_security_test_kit/dynamic_client_registration_group.rb +129 -0
- data/lib/udap_security_test_kit/generate_client_certs_test.rb +60 -0
- data/lib/udap_security_test_kit/grant_types_supported_field_test.rb +53 -0
- data/lib/udap_security_test_kit/reg_endpoint_jwt_signing_alg_values_supported_field_test.rb +29 -0
- data/lib/udap_security_test_kit/registration_endpoint_field_test.rb +30 -0
- data/lib/udap_security_test_kit/registration_failure_invalid_contents_test.rb +68 -0
- data/lib/udap_security_test_kit/registration_failure_invalid_jwt_signature_test.rb +70 -0
- data/lib/udap_security_test_kit/registration_success_contents_test.rb +64 -0
- data/lib/udap_security_test_kit/registration_success_test.rb +68 -0
- data/lib/udap_security_test_kit/scopes_supported_field_test.rb +26 -0
- data/lib/udap_security_test_kit/signed_metadata_contents_test.rb +89 -0
- data/lib/udap_security_test_kit/signed_metadata_field_test.rb +31 -0
- data/lib/udap_security_test_kit/signed_metadata_trust_verification_test.rb +54 -0
- data/lib/udap_security_test_kit/software_statement_builder.rb +32 -0
- data/lib/udap_security_test_kit/token_endpoint_auth_methods_supported_field_test.rb +22 -0
- data/lib/udap_security_test_kit/token_endpoint_auth_signing_alg_values_supported_field_test.rb +32 -0
- data/lib/udap_security_test_kit/token_endpoint_field_test.rb +30 -0
- data/lib/udap_security_test_kit/token_exchange_response_body_test.rb +30 -0
- data/lib/udap_security_test_kit/token_exchange_response_headers_test.rb +30 -0
- data/lib/udap_security_test_kit/udap_auth_extensions_required_field_test.rb +38 -0
- data/lib/udap_security_test_kit/udap_auth_extensions_supported_field_test.rb +31 -0
- data/lib/udap_security_test_kit/udap_certifications_required_field_test.rb +45 -0
- data/lib/udap_security_test_kit/udap_certifications_supported_field_test.rb +33 -0
- data/lib/udap_security_test_kit/udap_client_assertion_payload_builder.rb +15 -0
- data/lib/udap_security_test_kit/udap_jwt_builder.rb +30 -0
- data/lib/udap_security_test_kit/udap_jwt_validator.rb +71 -0
- data/lib/udap_security_test_kit/udap_profiles_supported_field_test.rb +47 -0
- data/lib/udap_security_test_kit/udap_request_builder.rb +43 -0
- data/lib/udap_security_test_kit/udap_versions_supported_field_test.rb +21 -0
- data/lib/udap_security_test_kit/udap_x509_certificate.rb +42 -0
- data/lib/udap_security_test_kit/version.rb +3 -0
- data/lib/udap_security_test_kit/well_known_endpoint_test.rb +31 -0
- data/lib/udap_security_test_kit.rb +63 -0
- 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
|