udap_security_test_kit 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|