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,32 @@
|
|
1
|
+
require 'jwt'
|
2
|
+
|
3
|
+
module UDAPSecurityTestKit
|
4
|
+
class SoftwareStatementBuilder
|
5
|
+
def self.build_payload(iss, aud, grant_type, scope)
|
6
|
+
if grant_type == 'authorization_code'
|
7
|
+
redirect_uris = ["#{Inferno::Application['base_url']}/custom/udap_security_test_kit/redirect"]
|
8
|
+
response_types = ['code']
|
9
|
+
client_name = 'Inferno UDAP Authorization Code Test Client'
|
10
|
+
elsif grant_type == 'client_credentials'
|
11
|
+
client_name = 'Inferno UDAP Client Credentials Test Client'
|
12
|
+
end
|
13
|
+
|
14
|
+
{
|
15
|
+
iss:,
|
16
|
+
sub: iss,
|
17
|
+
aud:,
|
18
|
+
exp: 5.minutes.from_now.to_i,
|
19
|
+
iat: Time.now.to_i,
|
20
|
+
jti: SecureRandom.hex(32),
|
21
|
+
client_name:,
|
22
|
+
redirect_uris:,
|
23
|
+
contacts: ['mailto:inferno@groups.mitre.org'],
|
24
|
+
logo_uri: 'https://inferno-framework.github.io/assets/inferno_logo.png',
|
25
|
+
grant_types: [grant_type],
|
26
|
+
response_types:,
|
27
|
+
token_endpoint_auth_method: 'private_key_jwt',
|
28
|
+
scope:
|
29
|
+
}.compact
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module UDAPSecurityTestKit
|
2
|
+
class TokenEndpointAuthMethodsSupportedFieldTest < Inferno::Test
|
3
|
+
include Inferno::DSL::Assertions
|
4
|
+
|
5
|
+
title 'token_endpoint_auth_methods_supported field'
|
6
|
+
id :udap_token_endpoint_auth_methods_supported_field
|
7
|
+
description %(
|
8
|
+
`token_endpoint_auth_methods_supported` must contain a fixed
|
9
|
+
array with one string element: `["private_key_jwt"]`
|
10
|
+
)
|
11
|
+
input :udap_well_known_metadata_json
|
12
|
+
|
13
|
+
run do
|
14
|
+
assert_valid_json(udap_well_known_metadata_json)
|
15
|
+
config = JSON.parse(udap_well_known_metadata_json)
|
16
|
+
|
17
|
+
assert config['token_endpoint_auth_methods_supported'] == ['private_key_jwt'],
|
18
|
+
'`token_endpoint_auth_methods_supported` field must contain an array ' \
|
19
|
+
"with one string element 'private_key_jwt'"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/udap_security_test_kit/token_endpoint_auth_signing_alg_values_supported_field_test.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require_relative 'common_assertions'
|
2
|
+
|
3
|
+
module UDAPSecurityTestKit
|
4
|
+
extend CommonAssertions
|
5
|
+
class TokenEndpointAuthSigningAlgValuesSupportedFieldTest < Inferno::Test
|
6
|
+
include Inferno::DSL::Assertions
|
7
|
+
|
8
|
+
title 'token_endpoint_auth_signing_alg_values_supported field'
|
9
|
+
id :udap_token_endpoint_auth_signing_alg_values_supported_field
|
10
|
+
description %(
|
11
|
+
`token_endpoint_auth_signing_alg_values_supported` is an
|
12
|
+
array of one or more strings identifying signature algorithms supported by the Authorization Server for
|
13
|
+
validation of signed JWTs submitted to the token endpoint for client authentication.
|
14
|
+
)
|
15
|
+
|
16
|
+
input :udap_well_known_metadata_json
|
17
|
+
|
18
|
+
run do
|
19
|
+
assert_valid_json(udap_well_known_metadata_json)
|
20
|
+
config = JSON.parse(udap_well_known_metadata_json)
|
21
|
+
|
22
|
+
assert config.key?('token_endpoint_auth_signing_alg_values_supported'),
|
23
|
+
'`token_endpoint_auth_signing_alg_values_supported` is a required field'
|
24
|
+
|
25
|
+
CommonAssertions.assert_array_of_strings(config, 'token_endpoint_auth_signing_alg_values_supported')
|
26
|
+
|
27
|
+
algs_supported = config['token_endpoint_auth_signing_alg_values_supported']
|
28
|
+
|
29
|
+
assert algs_supported.present?, 'Must support at least one signature algorithm'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module UDAPSecurityTestKit
|
2
|
+
class TokenEndpointFieldTest < Inferno::Test
|
3
|
+
include Inferno::DSL::Assertions
|
4
|
+
|
5
|
+
title 'token_endpoint field'
|
6
|
+
id :udap_token_endpoint_field
|
7
|
+
description %(
|
8
|
+
`token_endpoint` is a string containing the URL of
|
9
|
+
the Authorization Server's token endpoint
|
10
|
+
)
|
11
|
+
|
12
|
+
input :udap_well_known_metadata_json
|
13
|
+
output :udap_token_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?('token_endpoint'), '`token_endpoint` is a required field'
|
20
|
+
|
21
|
+
endpoint = config['token_endpoint']
|
22
|
+
|
23
|
+
assert endpoint.is_a?(String),
|
24
|
+
"`token_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_token_endpoint: endpoint
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module UDAPSecurityTestKit
|
2
|
+
class TokenExchangeResponseBodyTest < Inferno::Test
|
3
|
+
title 'Token exchange response body contains required information encoded in JSON'
|
4
|
+
description %(
|
5
|
+
The [UDAP JWT-Based Client Authentication Profile, Section 7.1](https://www.udap.org/udap-jwt-client-auth.html)
|
6
|
+
states:
|
7
|
+
> If the (token exchange) request is approved, the Authorization Server returns a token response as per Section
|
8
|
+
5.1 of RFC 6749.
|
9
|
+
|
10
|
+
[RFC 6749 OAuth 2.0 Authorization Framework, Section 5.1](https://datatracker.ietf.org/doc/html/rfc6749#section-5.1)
|
11
|
+
lists the `access_token` and `token_type` parameters as REQUIRED.
|
12
|
+
)
|
13
|
+
|
14
|
+
id :udap_token_exchange_response_body
|
15
|
+
|
16
|
+
input :token_response_body
|
17
|
+
|
18
|
+
run do
|
19
|
+
assert_valid_json(token_response_body)
|
20
|
+
token_response_body_parsed = JSON.parse(token_response_body)
|
21
|
+
|
22
|
+
required_keys = ['access_token', 'token_type']
|
23
|
+
|
24
|
+
required_keys.each do |key|
|
25
|
+
assert token_response_body_parsed.key?(key), "Token exchange response does not contain key #{key}"
|
26
|
+
assert token_response_body_parsed[key].present?, "Value for key #{key} cannot be empty"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module UDAPSecurityTestKit
|
2
|
+
class TokenExchangeResponseHeadersTest < Inferno::Test
|
3
|
+
title 'Response includes correct HTTP Cache-Control and Pragma headers'
|
4
|
+
description %(
|
5
|
+
[RFC 6749 OAuth 2.0 Authorization Framework Section 5.1](https://datatracker.ietf.org/doc/html/rfc6749#section-5.1)
|
6
|
+
states the following:
|
7
|
+
> The authorization server MUST include the HTTP "Cache-Control" response
|
8
|
+
header field with a value of
|
9
|
+
> "no-store" in any response containing tokens, credentials, or other
|
10
|
+
sensitive information, as well as the "Pragma" response header field with a value of "no-cache".
|
11
|
+
)
|
12
|
+
id :udap_token_exchange_response_headers
|
13
|
+
|
14
|
+
uses_request :token_exchange
|
15
|
+
|
16
|
+
run do
|
17
|
+
skip_if request.status != 200, 'Token exchange was unsuccessful'
|
18
|
+
|
19
|
+
cc_header = request.response_header('Cache-Control')&.value
|
20
|
+
|
21
|
+
assert cc_header&.downcase&.include?('no-store'),
|
22
|
+
'Token response must have `Cache-Control` header containing `no-store`.'
|
23
|
+
|
24
|
+
pragma_header = request.response_header('Pragma')&.value
|
25
|
+
|
26
|
+
assert pragma_header&.downcase&.include?('no-cache'),
|
27
|
+
'Token response must have `Pragma` header containing `no-cache`.'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module UDAPSecurityTestKit
|
2
|
+
class UDAPAuthExtensionsRequiredFieldTest < Inferno::Test
|
3
|
+
include Inferno::DSL::Assertions
|
4
|
+
|
5
|
+
title 'udap_authorization_extensions_required field'
|
6
|
+
id :udap_auth_extensions_required_field
|
7
|
+
description %(
|
8
|
+
`udap_authorization_extensions_required field` is an array of zero or more recognized key names for
|
9
|
+
Authorization Extension Objects required by the Authorization Server in every token request.
|
10
|
+
This metadata parameter SHALL be present if the value of the `udap_authorization_extensions_supported`
|
11
|
+
parameter is not an empty array.
|
12
|
+
)
|
13
|
+
|
14
|
+
input :udap_well_known_metadata_json
|
15
|
+
|
16
|
+
run do
|
17
|
+
assert_valid_json(udap_well_known_metadata_json)
|
18
|
+
config = JSON.parse(udap_well_known_metadata_json)
|
19
|
+
|
20
|
+
skip_if !config.key?('udap_authorization_extensions_supported'),
|
21
|
+
'Cannot access data needed to assess `udap_authorization_extensions_required` field:
|
22
|
+
`udap_authorization_extensions_supported` field is not present'
|
23
|
+
|
24
|
+
skip_if !config['udap_authorization_extensions_supported'].is_a?(Array),
|
25
|
+
'Cannot access data needed to assess `udap_authorization_extensions_required` field:
|
26
|
+
`udap_authorization_extensions_supported` field is not present'
|
27
|
+
|
28
|
+
omit_if config['udap_authorization_extensions_supported'].blank?, 'No UDAP authorization extensions are supported'
|
29
|
+
|
30
|
+
assert config.key?('udap_authorization_extensions_required'),
|
31
|
+
'`udap_authorization_extensions_required` field must be present because
|
32
|
+
`udap_authorization_extensions_supported field is not empty'
|
33
|
+
|
34
|
+
assert config['udap_authorization_extensions_required'].is_a?(Array),
|
35
|
+
'`udap_authorization_extensions_required` must be an array'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module UDAPSecurityTestKit
|
2
|
+
class UDAPAuthExtensionsSupportedFieldTest < Inferno::Test
|
3
|
+
include Inferno::DSL::Assertions
|
4
|
+
|
5
|
+
title 'udap_authorization_extensions_supported field'
|
6
|
+
id :udap_auth_extensions_supported_field
|
7
|
+
description %(
|
8
|
+
`udap_authorization_extensions_supported` is an array of zero or more recognized key names for Authorization
|
9
|
+
Extension Objects supported by the Authorization Server.
|
10
|
+
)
|
11
|
+
|
12
|
+
input :udap_well_known_metadata_json
|
13
|
+
input :required_flow_type
|
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?('udap_authorization_extensions_supported'),
|
20
|
+
'`udap_authorization_extensions_supported` is a required field'
|
21
|
+
|
22
|
+
assert config['udap_authorization_extensions_supported'].is_a?(Array),
|
23
|
+
'`udap_authorization_extensions_supported` must be an array'
|
24
|
+
|
25
|
+
if required_flow_type.include? 'client_credentials'
|
26
|
+
assert config['udap_authorization_extensions_supported'].include?('hl7-b2b'),
|
27
|
+
'Must support hl7-b2b authorization extension for client credentials workflow'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require_relative 'common_assertions'
|
2
|
+
|
3
|
+
module UDAPSecurityTestKit
|
4
|
+
extend CommonAssertions
|
5
|
+
class UDAPCertificationsRequiredFieldTest < Inferno::Test
|
6
|
+
include Inferno::DSL::Assertions
|
7
|
+
|
8
|
+
title 'udap_certifications_required field'
|
9
|
+
id :udap_certifications_required_field
|
10
|
+
description %(
|
11
|
+
If `udap_certifications_supported` is not empty, then `udap_certifications_required` is an array of zero or more
|
12
|
+
certification URIs
|
13
|
+
)
|
14
|
+
|
15
|
+
input :udap_well_known_metadata_json
|
16
|
+
output :udap_certifications_required
|
17
|
+
|
18
|
+
run do
|
19
|
+
assert_valid_json(udap_well_known_metadata_json)
|
20
|
+
config = JSON.parse(udap_well_known_metadata_json)
|
21
|
+
|
22
|
+
skip_if !config.key?('udap_certifications_supported'),
|
23
|
+
'Assessment of `udap_certifications_required` field is dependent on values in
|
24
|
+
`udap_certifications_supported`field, which is not present'
|
25
|
+
|
26
|
+
omit_if config['udap_certifications_supported'].blank?, 'No UDAP certifications are supported'
|
27
|
+
|
28
|
+
CommonAssertions.assert_array_of_strings(config, 'udap_certifications_required')
|
29
|
+
|
30
|
+
if config['udap_certifications_required'].blank?
|
31
|
+
output udap_certifications_required: 'false'
|
32
|
+
else
|
33
|
+
output udap_certifications_required: 'true'
|
34
|
+
end
|
35
|
+
|
36
|
+
non_uri_values =
|
37
|
+
config['udap_certifications_required']
|
38
|
+
.grep_v(URI::DEFAULT_PARSER.make_regexp)
|
39
|
+
|
40
|
+
assert non_uri_values.blank?,
|
41
|
+
'`udap_certifacations_required` should be an Array of ' \
|
42
|
+
"URI strings but found #{non_uri_values.map(&:class).map(&:name).join(', ')}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative 'common_assertions'
|
2
|
+
module UDAPSecurityTestKit
|
3
|
+
extend CommonAssertions
|
4
|
+
class UDAPCertificationsSupportedFieldTest < Inferno::Test
|
5
|
+
include Inferno::DSL::Assertions
|
6
|
+
|
7
|
+
title 'udap_certifications_supported field'
|
8
|
+
id :udap_certifications_supported_field
|
9
|
+
description %(
|
10
|
+
`udap_certifications_supported` is an array of zero or more
|
11
|
+
certification URIs
|
12
|
+
)
|
13
|
+
|
14
|
+
input :udap_well_known_metadata_json
|
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?('udap_certifications_supported'), '`udap_certifications_supported` is a required field'
|
21
|
+
|
22
|
+
CommonAssertions.assert_array_of_strings(config, 'udap_certifications_supported')
|
23
|
+
|
24
|
+
non_uri_values =
|
25
|
+
config['udap_certifications_supported']
|
26
|
+
.grep_v(URI::DEFAULT_PARSER.make_regexp)
|
27
|
+
|
28
|
+
error_message = '`udap_certifacations_supported` should be an Array of URI strings, but found
|
29
|
+
#{non_uri_values.map(&:class).map(&:name).join(', ')}'
|
30
|
+
assert non_uri_values.blank?, error_message
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module UDAPSecurityTestKit
|
2
|
+
class UDAPClientAssertionPayloadBuilder
|
3
|
+
def self.build(iss, aud, extensions)
|
4
|
+
{
|
5
|
+
iss:,
|
6
|
+
sub: iss,
|
7
|
+
aud:,
|
8
|
+
exp: 5.minutes.from_now.to_i,
|
9
|
+
iat: Time.now.to_i,
|
10
|
+
jti: SecureRandom.hex(32),
|
11
|
+
extensions:
|
12
|
+
}.compact
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'jwt'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
module UDAPSecurityTestKit
|
5
|
+
class UDAPJWTBuilder
|
6
|
+
def self.generate_private_key(pkey_string)
|
7
|
+
OpenSSL::PKey.read(pkey_string)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.split_user_input_cert_string(user_input_string)
|
11
|
+
regex = /-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----/m
|
12
|
+
user_input_string.scan(regex)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.encode_jwt_no_x5c_header(payload, private_key, alg)
|
16
|
+
JWT.encode payload, private_key, alg
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.encode_jwt_with_x5c_header(payload, private_key_pem_string, alg, x5c_certs_pem_string)
|
20
|
+
private_key = OpenSSL::PKey.read(private_key_pem_string)
|
21
|
+
|
22
|
+
x5c_certs_encoded = x5c_certs_pem_string.map do |cert|
|
23
|
+
cert_pem = OpenSSL::X509::Certificate.new(cert)
|
24
|
+
Base64.urlsafe_encode64(cert_pem.to_der)
|
25
|
+
end
|
26
|
+
|
27
|
+
JWT.encode payload, private_key, alg, { x5c: x5c_certs_encoded }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'jwt'
|
2
|
+
|
3
|
+
module UDAPSecurityTestKit
|
4
|
+
class UDAPJWTValidator
|
5
|
+
def self.validate_signature(signed_metadata_jwt, algorithm, cert)
|
6
|
+
JWT.decode(
|
7
|
+
signed_metadata_jwt,
|
8
|
+
cert.public_key,
|
9
|
+
true,
|
10
|
+
algorithm:
|
11
|
+
)
|
12
|
+
{
|
13
|
+
success: true,
|
14
|
+
error_message: nil
|
15
|
+
}
|
16
|
+
rescue JWT::DecodeError => e
|
17
|
+
{
|
18
|
+
success: false,
|
19
|
+
error_message: e.full_message
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.validate_trust_chain(x5c_header_encoded, trust_anchor_certs)
|
24
|
+
cert_chain = x5c_header_encoded.map do |cert|
|
25
|
+
cert_der = Base64.urlsafe_decode64(cert)
|
26
|
+
OpenSSL::X509::Certificate.new(cert_der)
|
27
|
+
end
|
28
|
+
crl_uris = cert_chain.map(&:crl_uris).compact.flatten
|
29
|
+
crl_uris_anchors = trust_anchor_certs.map(&:crl_uris).compact.flatten
|
30
|
+
crl_uris.concat(crl_uris_anchors)
|
31
|
+
begin
|
32
|
+
crls = crl_uris.map do |uri|
|
33
|
+
get_crl_from_uri(uri)
|
34
|
+
end
|
35
|
+
rescue OpenSSL::X509::CRLError => e
|
36
|
+
return {
|
37
|
+
success: false,
|
38
|
+
error_message: e.message
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
begin
|
43
|
+
# JWT library can validate the trust chain while decoding the provided
|
44
|
+
# JWT/verifying its signature, but we don't have currently have access
|
45
|
+
# to certs that satisfy both prerequisites to doing this. We have:
|
46
|
+
# A) client certs that can establish a legitimate trust chain (but don't
|
47
|
+
# have access to private key needed to create a valid, signed JWT with them)
|
48
|
+
# B) client certs that have a valid private key (but which cannot
|
49
|
+
# establish a legitimate trust chain)
|
50
|
+
# As a result, these capabilities are decoupled for testing purposes
|
51
|
+
JWT::X5cKeyFinder.new(trust_anchor_certs,
|
52
|
+
crls).from(x5c_header_encoded)
|
53
|
+
{
|
54
|
+
success: true,
|
55
|
+
error_message: nil
|
56
|
+
}
|
57
|
+
rescue JWT::VerificationError => e
|
58
|
+
{
|
59
|
+
success: false,
|
60
|
+
error_message: e.full_message
|
61
|
+
}
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.get_crl_from_uri(crl_uri)
|
66
|
+
uri = URI(crl_uri)
|
67
|
+
crl = Net::HTTP.get(uri)
|
68
|
+
OpenSSL::X509::CRL.new(crl)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require_relative 'common_assertions'
|
2
|
+
|
3
|
+
module UDAPSecurityTestKit
|
4
|
+
extend CommonAssertions
|
5
|
+
class UDAPProfilesSupportedFieldTest < Inferno::Test
|
6
|
+
include Inferno::DSL::Assertions
|
7
|
+
|
8
|
+
title 'udap_profiles_supported field'
|
9
|
+
id :udap_profiles_supported_field
|
10
|
+
description %(
|
11
|
+
`udap_profiles_supported` is an array of two or more strings identifying the core UDAP profiles supported by the
|
12
|
+
Authorization Server. The array SHALL include:
|
13
|
+
`udap_dcr` for UDAP Dynamic Client Registration, and
|
14
|
+
`udap_authn` for UDAP JWT-Based Client Authentication.
|
15
|
+
If the `grant_types_supported` parameter includes the string `client_credentials`, then the array SHALL also
|
16
|
+
include:
|
17
|
+
`udap_authz` for UDAP Client Authorization Grants using JSON Web Tokens to indicate support for
|
18
|
+
Authorization Extension Objects.
|
19
|
+
)
|
20
|
+
|
21
|
+
input :udap_well_known_metadata_json
|
22
|
+
|
23
|
+
run do
|
24
|
+
assert_valid_json(udap_well_known_metadata_json)
|
25
|
+
config = JSON.parse(udap_well_known_metadata_json)
|
26
|
+
|
27
|
+
assert config.key?('udap_profiles_supported'), '`udap_profiles_supported` is a required field'
|
28
|
+
|
29
|
+
CommonAssertions.assert_array_of_strings(config, 'udap_profiles_supported')
|
30
|
+
|
31
|
+
profiles_supported = config['udap_profiles_supported']
|
32
|
+
|
33
|
+
assert profiles_supported.include?('udap_dcr'),
|
34
|
+
'Array must include `udap_dcr` to indicate support for required UDAP Dynamic Client Registration profile'
|
35
|
+
|
36
|
+
assert profiles_supported.include?('udap_authn'),
|
37
|
+
'Array must include `udap_authn` value to indicate support for required UDAP JWT-Based Client
|
38
|
+
Authentication profile'
|
39
|
+
|
40
|
+
if config['grant_types_supported']&.include?('client_credentials')
|
41
|
+
assert profiles_supported.include?('udap_authz'),
|
42
|
+
'`client_credentials` grant type is supported, so array must include `udap_authz` to indicate support for
|
43
|
+
UDAP Client Authorization Grants using JSON Web Tokens'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'uri'
|
2
|
+
module UDAPSecurityTestKit
|
3
|
+
class UDAPRequestBuilder
|
4
|
+
def self.build_registration_request(software_statement_jwt, certifications_jwt)
|
5
|
+
registration_headers = {
|
6
|
+
'Accept' => 'application/json',
|
7
|
+
'Content-Type' => 'application/json'
|
8
|
+
}
|
9
|
+
|
10
|
+
certifications = if certifications_jwt.nil?
|
11
|
+
[]
|
12
|
+
else
|
13
|
+
certifications_jwt.split
|
14
|
+
end
|
15
|
+
|
16
|
+
registration_body = {
|
17
|
+
'software_statement' => software_statement_jwt,
|
18
|
+
'certifications' => certifications,
|
19
|
+
'udap' => '1'
|
20
|
+
}.compact
|
21
|
+
|
22
|
+
[registration_headers, registration_body.to_json]
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.build_token_exchange_request(client_assertion_jwt, grant_type, authorization_code, redirect_uri)
|
26
|
+
token_exchange_headers = {
|
27
|
+
'Accept' => 'application/json',
|
28
|
+
'Content-Type' => 'application/x-www-form-urlencoded'
|
29
|
+
}
|
30
|
+
|
31
|
+
token_exchange_body = {
|
32
|
+
'grant_type' => grant_type,
|
33
|
+
'code' => authorization_code,
|
34
|
+
'redirect_uri' => redirect_uri,
|
35
|
+
'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
|
36
|
+
'client_assertion' => client_assertion_jwt,
|
37
|
+
'udap' => '1'
|
38
|
+
}.compact
|
39
|
+
|
40
|
+
[token_exchange_headers, URI.encode_www_form(token_exchange_body)]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module UDAPSecurityTestKit
|
2
|
+
class UDAPVersionsSupportedFieldTest < Inferno::Test
|
3
|
+
include Inferno::DSL::Assertions
|
4
|
+
|
5
|
+
title 'udap_versions_supported field'
|
6
|
+
id :udap_versions_supported_field
|
7
|
+
description %(
|
8
|
+
`udap_versions_supported` must contain a fixed array with one string
|
9
|
+
element: `["1"]`
|
10
|
+
)
|
11
|
+
|
12
|
+
input :udap_well_known_metadata_json
|
13
|
+
|
14
|
+
run do
|
15
|
+
assert_valid_json(udap_well_known_metadata_json)
|
16
|
+
config = JSON.parse(udap_well_known_metadata_json)
|
17
|
+
assert config['udap_versions_supported'] == ['1'],
|
18
|
+
"`udap_versions_supported` field must contain an array with one string element '1'"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module UDAPSecurityTestKit
|
2
|
+
class UDAPX509Certificate
|
3
|
+
attr_reader :san, :cert_private_key, :cert
|
4
|
+
|
5
|
+
def initialize(issuer_cert_pem_string, issuer_private_key_pem_string, include_san_extension: true)
|
6
|
+
issuer_private_key = OpenSSL::PKey.read(issuer_private_key_pem_string)
|
7
|
+
issuer_cert = OpenSSL::X509::Certificate.new(issuer_cert_pem_string)
|
8
|
+
|
9
|
+
@cert_private_key = OpenSSL::PKey::RSA.new 2048
|
10
|
+
cert = OpenSSL::X509::Certificate.new
|
11
|
+
|
12
|
+
# must be v3 or above to allow extensions
|
13
|
+
# x509 versions are zero-based, so '2' means version 3
|
14
|
+
cert.version = 2
|
15
|
+
|
16
|
+
# X.509 serial numbers can be up to 20 bytes (2**(8*20))
|
17
|
+
cert.serial = SecureRandom.random_number(2**32)
|
18
|
+
cert.subject = OpenSSL::X509::Name.parse '/C=US/ST=MA/L=Bedford/O=Inferno/CN=UDAP-Test-Client'
|
19
|
+
cert.issuer = issuer_cert.subject
|
20
|
+
cert.public_key = cert_private_key.public_key
|
21
|
+
cert.not_before = Time.now
|
22
|
+
cert.not_after = cert.not_before + (1 * 365 * 24 * 60 * 60) # 1 years validity
|
23
|
+
ef = OpenSSL::X509::ExtensionFactory.new
|
24
|
+
ef.subject_certificate = cert
|
25
|
+
ef.issuer_certificate = issuer_cert
|
26
|
+
|
27
|
+
if include_san_extension
|
28
|
+
# SAN must be unique for each cert
|
29
|
+
@san = "https://inferno.org/udap_security_test_kit/#{cert.serial}"
|
30
|
+
unique_uri_entry = "URI:#{@san}"
|
31
|
+
cert.add_extension(ef.create_extension('subjectAltName', unique_uri_entry, false))
|
32
|
+
end
|
33
|
+
|
34
|
+
# TODO: add in any other relevant extensions?
|
35
|
+
cert.add_extension(ef.create_extension('keyUsage', 'digitalSignature, nonRepudiation', true))
|
36
|
+
cert.add_extension(ef.create_extension('subjectKeyIdentifier', 'hash', false))
|
37
|
+
cert.sign(issuer_private_key, OpenSSL::Digest.new('SHA256'))
|
38
|
+
|
39
|
+
@cert = cert
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module UDAPSecurityTestKit
|
2
|
+
class WellKnownEndpointTest < Inferno::Test
|
3
|
+
include Inferno::DSL::Assertions
|
4
|
+
|
5
|
+
title 'UDAP Well-Known configuration is available'
|
6
|
+
id :udap_well_known_endpoint
|
7
|
+
description %(
|
8
|
+
The [UDAP Discovery IG Section 2.1 Discovery Endpoints](https://hl7.org/fhir/us/udap-security/STU1/discovery.html#discovery-of-endpoints)
|
9
|
+
states:
|
10
|
+
> Servers SHALL allow access to the following metadata URL to unregistered client applications and without
|
11
|
+
> requiring client authentication, where {baseURL} represents the base FHIR URL for the FHIR server:
|
12
|
+
> `{baseURL}/.well-known/udap`
|
13
|
+
|
14
|
+
This test ensures the discovery endpoint returns a 200 status and valid JSON body.
|
15
|
+
)
|
16
|
+
|
17
|
+
input :udap_fhir_base_url,
|
18
|
+
title: 'FHIR Server Base URL',
|
19
|
+
description: 'Base FHIR URL of FHIR Server. Discovery request will be sent to {baseURL}/.well-known/udap'
|
20
|
+
|
21
|
+
output :udap_well_known_metadata_json
|
22
|
+
makes_request :config
|
23
|
+
|
24
|
+
run do
|
25
|
+
get("#{udap_fhir_base_url.strip.chomp('/')}/.well-known/udap", name: :udap_well_known_metadata_json)
|
26
|
+
assert_response_status(200)
|
27
|
+
assert_valid_json(response[:body])
|
28
|
+
output udap_well_known_metadata_json: response[:body]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|