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,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
|