udap_security_test_kit 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/lib/udap_security_test_kit/authorization_code_authentication_group.rb +44 -0
  4. data/lib/udap_security_test_kit/authorization_code_group.rb +103 -0
  5. data/lib/udap_security_test_kit/authorization_code_received_test.rb +31 -0
  6. data/lib/udap_security_test_kit/authorization_code_redirect_test.rb +74 -0
  7. data/lib/udap_security_test_kit/authorization_code_token_exchange_test.rb +103 -0
  8. data/lib/udap_security_test_kit/authorization_endpoint_field_test.rb +43 -0
  9. data/lib/udap_security_test_kit/certs/InfernoCA.key +52 -0
  10. data/lib/udap_security_test_kit/certs/InfernoCA.pem +35 -0
  11. data/lib/udap_security_test_kit/certs/TestClient.pem +32 -0
  12. data/lib/udap_security_test_kit/certs/TestClientPrivateKey.key +28 -0
  13. data/lib/udap_security_test_kit/client_credentials_authentication_group.rb +40 -0
  14. data/lib/udap_security_test_kit/client_credentials_group.rb +105 -0
  15. data/lib/udap_security_test_kit/client_credentials_token_exchange_test.rb +130 -0
  16. data/lib/udap_security_test_kit/common_assertions.rb +16 -0
  17. data/lib/udap_security_test_kit/default_cert_file_loader.rb +27 -0
  18. data/lib/udap_security_test_kit/discovery_group.rb +90 -0
  19. data/lib/udap_security_test_kit/dynamic_client_registration_group.rb +129 -0
  20. data/lib/udap_security_test_kit/generate_client_certs_test.rb +60 -0
  21. data/lib/udap_security_test_kit/grant_types_supported_field_test.rb +53 -0
  22. data/lib/udap_security_test_kit/reg_endpoint_jwt_signing_alg_values_supported_field_test.rb +29 -0
  23. data/lib/udap_security_test_kit/registration_endpoint_field_test.rb +30 -0
  24. data/lib/udap_security_test_kit/registration_failure_invalid_contents_test.rb +68 -0
  25. data/lib/udap_security_test_kit/registration_failure_invalid_jwt_signature_test.rb +70 -0
  26. data/lib/udap_security_test_kit/registration_success_contents_test.rb +64 -0
  27. data/lib/udap_security_test_kit/registration_success_test.rb +68 -0
  28. data/lib/udap_security_test_kit/scopes_supported_field_test.rb +26 -0
  29. data/lib/udap_security_test_kit/signed_metadata_contents_test.rb +89 -0
  30. data/lib/udap_security_test_kit/signed_metadata_field_test.rb +31 -0
  31. data/lib/udap_security_test_kit/signed_metadata_trust_verification_test.rb +54 -0
  32. data/lib/udap_security_test_kit/software_statement_builder.rb +32 -0
  33. data/lib/udap_security_test_kit/token_endpoint_auth_methods_supported_field_test.rb +22 -0
  34. data/lib/udap_security_test_kit/token_endpoint_auth_signing_alg_values_supported_field_test.rb +32 -0
  35. data/lib/udap_security_test_kit/token_endpoint_field_test.rb +30 -0
  36. data/lib/udap_security_test_kit/token_exchange_response_body_test.rb +30 -0
  37. data/lib/udap_security_test_kit/token_exchange_response_headers_test.rb +30 -0
  38. data/lib/udap_security_test_kit/udap_auth_extensions_required_field_test.rb +38 -0
  39. data/lib/udap_security_test_kit/udap_auth_extensions_supported_field_test.rb +31 -0
  40. data/lib/udap_security_test_kit/udap_certifications_required_field_test.rb +45 -0
  41. data/lib/udap_security_test_kit/udap_certifications_supported_field_test.rb +33 -0
  42. data/lib/udap_security_test_kit/udap_client_assertion_payload_builder.rb +15 -0
  43. data/lib/udap_security_test_kit/udap_jwt_builder.rb +30 -0
  44. data/lib/udap_security_test_kit/udap_jwt_validator.rb +71 -0
  45. data/lib/udap_security_test_kit/udap_profiles_supported_field_test.rb +47 -0
  46. data/lib/udap_security_test_kit/udap_request_builder.rb +43 -0
  47. data/lib/udap_security_test_kit/udap_versions_supported_field_test.rb +21 -0
  48. data/lib/udap_security_test_kit/udap_x509_certificate.rb +42 -0
  49. data/lib/udap_security_test_kit/version.rb +3 -0
  50. data/lib/udap_security_test_kit/well_known_endpoint_test.rb +31 -0
  51. data/lib/udap_security_test_kit.rb +63 -0
  52. metadata +124 -0
@@ -0,0 +1,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
@@ -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,3 @@
1
+ module UDAPSecurityTestKit
2
+ VERSION = '0.9.0'.freeze
3
+ 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