udap_security_test_kit 0.9.0

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