openid4vp 0.1.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 (119) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +39 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +278 -0
  5. data/lib/openid4vp/authorization_request/validator.rb +177 -0
  6. data/lib/openid4vp/authorization_request.rb +302 -0
  7. data/lib/openid4vp/authorization_response.rb +151 -0
  8. data/lib/openid4vp/client_id.rb +90 -0
  9. data/lib/openid4vp/credential.rb +51 -0
  10. data/lib/openid4vp/dc_api.rb +116 -0
  11. data/lib/openid4vp/dcql/claims_path_pointer.rb +109 -0
  12. data/lib/openid4vp/dcql/claims_query.rb +76 -0
  13. data/lib/openid4vp/dcql/credential_query.rb +178 -0
  14. data/lib/openid4vp/dcql/credential_set_query.rb +49 -0
  15. data/lib/openid4vp/dcql/evaluator.rb +228 -0
  16. data/lib/openid4vp/dcql/matcher.rb +79 -0
  17. data/lib/openid4vp/dcql/meta.rb +99 -0
  18. data/lib/openid4vp/dcql/query.rb +93 -0
  19. data/lib/openid4vp/dcql/selection.rb +45 -0
  20. data/lib/openid4vp/dcql/trusted_authorities_query.rb +48 -0
  21. data/lib/openid4vp/dcql.rb +10 -0
  22. data/lib/openid4vp/encoded_response.rb +10 -0
  23. data/lib/openid4vp/error_response.rb +82 -0
  24. data/lib/openid4vp/errors.rb +73 -0
  25. data/lib/openid4vp/formats/base.rb +74 -0
  26. data/lib/openid4vp/formats/context.rb +56 -0
  27. data/lib/openid4vp/formats/jwt_vc_json.rb +274 -0
  28. data/lib/openid4vp/formats/ldp_vc.rb +160 -0
  29. data/lib/openid4vp/formats/mso_mdoc.rb +204 -0
  30. data/lib/openid4vp/formats/registry.rb +53 -0
  31. data/lib/openid4vp/formats/sd_jwt_vc.rb +132 -0
  32. data/lib/openid4vp/formats/verified_presentation.rb +45 -0
  33. data/lib/openid4vp/formats.rb +12 -0
  34. data/lib/openid4vp/jose/algorithms.rb +46 -0
  35. data/lib/openid4vp/jose/jwe.rb +184 -0
  36. data/lib/openid4vp/jose/jwk.rb +253 -0
  37. data/lib/openid4vp/jose/jwk_set.rb +57 -0
  38. data/lib/openid4vp/jose/jws.rb +225 -0
  39. data/lib/openid4vp/jose.rb +8 -0
  40. data/lib/openid4vp/mdoc/builder.rb +132 -0
  41. data/lib/openid4vp/mdoc/cbor_values.rb +32 -0
  42. data/lib/openid4vp/mdoc/cose_key.rb +71 -0
  43. data/lib/openid4vp/mdoc/device_authentication.rb +32 -0
  44. data/lib/openid4vp/mdoc/device_response.rb +77 -0
  45. data/lib/openid4vp/mdoc/device_signed.rb +111 -0
  46. data/lib/openid4vp/mdoc/document.rb +44 -0
  47. data/lib/openid4vp/mdoc/handover.rb +51 -0
  48. data/lib/openid4vp/mdoc/issuer_signed.rb +157 -0
  49. data/lib/openid4vp/mdoc/mso.rb +133 -0
  50. data/lib/openid4vp/mdoc/session_transcript.rb +49 -0
  51. data/lib/openid4vp/mdoc/sign1_decoder.rb +60 -0
  52. data/lib/openid4vp/mdoc/verifier.rb +169 -0
  53. data/lib/openid4vp/mdoc.rb +9 -0
  54. data/lib/openid4vp/metadata/verifier_metadata.rb +109 -0
  55. data/lib/openid4vp/metadata/vp_formats.rb +112 -0
  56. data/lib/openid4vp/metadata/wallet_metadata.rb +139 -0
  57. data/lib/openid4vp/metadata.rb +10 -0
  58. data/lib/openid4vp/request_object.rb +152 -0
  59. data/lib/openid4vp/request_verification/base.rb +63 -0
  60. data/lib/openid4vp/request_verification/config.rb +65 -0
  61. data/lib/openid4vp/request_verification/decentralized_identifier.rb +56 -0
  62. data/lib/openid4vp/request_verification/openid_federation.rb +52 -0
  63. data/lib/openid4vp/request_verification/pre_registered.rb +54 -0
  64. data/lib/openid4vp/request_verification/redirect_uri.rb +29 -0
  65. data/lib/openid4vp/request_verification/resolver.rb +76 -0
  66. data/lib/openid4vp/request_verification/verified_request.rb +12 -0
  67. data/lib/openid4vp/request_verification/verifier_attestation.rb +54 -0
  68. data/lib/openid4vp/request_verification/x509_hash.rb +40 -0
  69. data/lib/openid4vp/request_verification/x509_san_dns.rb +60 -0
  70. data/lib/openid4vp/request_verification.rb +10 -0
  71. data/lib/openid4vp/response_encryption.rb +51 -0
  72. data/lib/openid4vp/response_endpoint_result.rb +33 -0
  73. data/lib/openid4vp/response_mode.rb +44 -0
  74. data/lib/openid4vp/response_type.rb +7 -0
  75. data/lib/openid4vp/sd_jwt/digest.rb +30 -0
  76. data/lib/openid4vp/sd_jwt/disclosure.rb +104 -0
  77. data/lib/openid4vp/sd_jwt/issuer.rb +132 -0
  78. data/lib/openid4vp/sd_jwt/kb_jwt.rb +82 -0
  79. data/lib/openid4vp/sd_jwt/parser.rb +48 -0
  80. data/lib/openid4vp/sd_jwt/presentation.rb +333 -0
  81. data/lib/openid4vp/sd_jwt/reconstruct.rb +132 -0
  82. data/lib/openid4vp/sd_jwt.rb +10 -0
  83. data/lib/openid4vp/transaction_data/registry.rb +42 -0
  84. data/lib/openid4vp/transaction_data/sd_jwt_hashes.rb +124 -0
  85. data/lib/openid4vp/transaction_data.rb +138 -0
  86. data/lib/openid4vp/util/base64url.rb +44 -0
  87. data/lib/openid4vp/util/form.rb +24 -0
  88. data/lib/openid4vp/util/random.rb +29 -0
  89. data/lib/openid4vp/util/uri.rb +48 -0
  90. data/lib/openid4vp/util.rb +23 -0
  91. data/lib/openid4vp/verifier/config.rb +216 -0
  92. data/lib/openid4vp/verifier/created_request.rb +25 -0
  93. data/lib/openid4vp/verifier/http_result.rb +34 -0
  94. data/lib/openid4vp/verifier/memory_session_store.rb +102 -0
  95. data/lib/openid4vp/verifier/request_builder.rb +216 -0
  96. data/lib/openid4vp/verifier/request_signer.rb +59 -0
  97. data/lib/openid4vp/verifier/request_uri_endpoint.rb +151 -0
  98. data/lib/openid4vp/verifier/response_endpoint.rb +197 -0
  99. data/lib/openid4vp/verifier/response_validator.rb +258 -0
  100. data/lib/openid4vp/verifier/session.rb +109 -0
  101. data/lib/openid4vp/verifier/session_store.rb +48 -0
  102. data/lib/openid4vp/verifier/verified_response.rb +53 -0
  103. data/lib/openid4vp/verifier.rb +155 -0
  104. data/lib/openid4vp/verifier_attestation.rb +130 -0
  105. data/lib/openid4vp/verifier_info.rb +102 -0
  106. data/lib/openid4vp/version.rb +5 -0
  107. data/lib/openid4vp/vp_token.rb +81 -0
  108. data/lib/openid4vp/wallet/config.rb +49 -0
  109. data/lib/openid4vp/wallet/http_client.rb +73 -0
  110. data/lib/openid4vp/wallet/presenter.rb +90 -0
  111. data/lib/openid4vp/wallet/processed_request.rb +14 -0
  112. data/lib/openid4vp/wallet/request_processor.rb +237 -0
  113. data/lib/openid4vp/wallet/response_builder.rb +44 -0
  114. data/lib/openid4vp/wallet/submission.rb +68 -0
  115. data/lib/openid4vp/wallet.rb +142 -0
  116. data/lib/openid4vp/x509/chain_validator.rb +42 -0
  117. data/lib/openid4vp/x509.rb +103 -0
  118. data/lib/openid4vp.rb +18 -0
  119. metadata +233 -0
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cose"
4
+ require_relative "../mdoc"
5
+ require_relative "../errors"
6
+ require_relative "../util"
7
+ require_relative "../jose/jwk"
8
+ require_relative "cose_key"
9
+ require_relative "mso"
10
+ require_relative "device_authentication"
11
+
12
+ module OpenID4VP
13
+ module Mdoc
14
+ # Verifies a parsed OpenID4VP::Mdoc::Document (OpenID4VP §B.2, ISO
15
+ # 18013-5 §9.3.1): the IssuerAuth signature over the MSO, the MSO's
16
+ # validity window and per-element digests, and the DeviceSignature over
17
+ # the SessionTranscript-bound DeviceAuthentication structure.
18
+ module Verifier
19
+ Result = Struct.new(
20
+ :doc_type, :claims, :mso, :issuer_certs, :device_key, :issuer_alg, :device_alg, keyword_init: true
21
+ )
22
+
23
+ # COSE alg identifiers (protected header label 1 values) this module
24
+ # accepts for IssuerAuth/DeviceSignature: ES256/ES384/ES512, the
25
+ # ECDSA algs this gem itself issues and verifies (mirrors
26
+ # CoseKey::CURVE_FOR_ALG; EdDSA mdocs are not supported here). Any
27
+ # other alg is rejected before we ever
28
+ # call cose-ruby's verify: cose-ruby's ECDSA#valid_key? does not check
29
+ # that the key's curve matches the alg (only that the key carries no
30
+ # alg, or the same alg), so an IssuerAuth/DeviceSignature whose
31
+ # protected alg doesn't match the actual key's curve (e.g. alg -35
32
+ # ES384 with a P-256 leaf key) would otherwise reach
33
+ # OpenSSL::SignatureAlgorithm::Base#verify_key=, which raises
34
+ # OpenSSL::SignatureAlgorithm::VerifyKeyError -- a raw, undocumented
35
+ # error type -- instead of a clean OpenID4VP::Error.
36
+ ALLOWED_ALGS = [-7, -35, -36].freeze
37
+
38
+ module_function
39
+
40
+ # document: an OpenID4VP::Mdoc::Document. session_transcript: the
41
+ # SessionTranscript array (e.g. from SessionTranscript.for_request).
42
+ # issuer_cert_validator: a callable(chain_certs) -> leaf cert, e.g.
43
+ # ->(chain) { OpenID4VP::X509::ChainValidator.new(trust_anchors: [...]).validate!(chain) }.
44
+ #
45
+ # Every Credential-side failure (untrusted/invalid issuer chain, bad
46
+ # IssuerAuth signature, malformed/expired MSO, tampered disclosed
47
+ # element) raises OpenID4VP::PresentationInvalid. Only a failure to
48
+ # prove possession of the mdoc device key (a missing/invalid
49
+ # DeviceSignature) raises OpenID4VP::HolderBindingInvalid. Callers can
50
+ # rely on this distinction instead of inspecting error messages.
51
+ def verify(document, session_transcript:, issuer_cert_validator:, now: ::Time.now)
52
+ issuer_signed = document.issuer_signed
53
+ issuer_auth = issuer_signed.issuer_auth
54
+
55
+ chain = issuer_signed.x5chain
56
+ raise OpenID4VP::PresentationInvalid, "mdoc: IssuerAuth is missing an x5chain" if chain.empty?
57
+
58
+ leaf = validate_issuer_chain!(issuer_cert_validator, chain)
59
+ verify_issuer_signature!(issuer_auth, leaf)
60
+
61
+ mso = parse_mso!(issuer_auth.payload)
62
+ validate_doc_type!(mso, document.doc_type)
63
+ validate_validity!(mso, now)
64
+ claims = verify_disclosed_items!(issuer_signed, mso)
65
+ verify_device_signature!(document, mso, session_transcript)
66
+
67
+ Result.new(
68
+ doc_type: document.doc_type, claims: claims, mso: mso, issuer_certs: chain,
69
+ device_key: mso.device_key, issuer_alg: issuer_auth.headers.alg,
70
+ device_alg: document.device_signed.device_auth.fetch("deviceSignature").headers.alg
71
+ )
72
+ end
73
+
74
+ def validate_issuer_chain!(issuer_cert_validator, chain)
75
+ issuer_cert_validator.call(chain)
76
+ rescue OpenID4VP::VerificationError => e
77
+ raise OpenID4VP::PresentationInvalid, "mdoc: untrusted issuer certificate chain: #{e.message}"
78
+ end
79
+ private_class_method :validate_issuer_chain!
80
+
81
+ def verify_issuer_signature!(issuer_auth, leaf)
82
+ check_allowed_alg!(issuer_auth.headers.alg, OpenID4VP::PresentationInvalid, "IssuerAuth")
83
+ key = CoseKey.with_kid(::COSE::Key::EC2.from_pkey(leaf.public_key), issuer_auth.headers.kid)
84
+ issuer_auth.verify(key)
85
+ rescue ::COSE::Error, ::OpenSSL::SignatureAlgorithm::Error, ::OpenSSL::OpenSSLError => e
86
+ raise OpenID4VP::PresentationInvalid, "mdoc: issuer signature invalid: #{e.message}"
87
+ end
88
+ private_class_method :verify_issuer_signature!
89
+
90
+ # Raises `error_class` (OpenID4VP::PresentationInvalid for IssuerAuth,
91
+ # OpenID4VP::HolderBindingInvalid for DeviceSignature) unless `alg` is
92
+ # one of ALLOWED_ALGS. See ALLOWED_ALGS for why this check must run
93
+ # before we call cose-ruby's verify.
94
+ def check_allowed_alg!(alg, error_class, label)
95
+ return if ALLOWED_ALGS.include?(alg)
96
+
97
+ raise error_class, "mdoc: unsupported #{label} alg #{alg.inspect}"
98
+ end
99
+ private_class_method :check_allowed_alg!
100
+
101
+ def parse_mso!(issuer_auth_payload)
102
+ MSO.parse(issuer_auth_payload)
103
+ rescue OpenID4VP::InvalidResponse => e
104
+ raise OpenID4VP::PresentationInvalid, "mdoc: invalid MSO: #{e.message}"
105
+ end
106
+ private_class_method :parse_mso!
107
+
108
+ def validate_doc_type!(mso, doc_type)
109
+ return if mso.doc_type == doc_type
110
+
111
+ raise OpenID4VP::PresentationInvalid, "mdoc: MSO docType #{mso.doc_type.inspect} does not match " \
112
+ "Document docType #{doc_type.inspect}"
113
+ end
114
+ private_class_method :validate_doc_type!
115
+
116
+ def validate_validity!(mso, now)
117
+ valid_from = mso.validity_info.fetch("validFrom")
118
+ valid_until = mso.validity_info.fetch("validUntil")
119
+ raise OpenID4VP::PresentationInvalid, "mdoc: MSO is not yet valid" if now < valid_from
120
+ raise OpenID4VP::PresentationInvalid, "mdoc: MSO has expired" if now > valid_until
121
+ end
122
+ private_class_method :validate_validity!
123
+
124
+ def verify_disclosed_items!(issuer_signed, mso)
125
+ issuer_signed.name_spaces.to_h do |namespace, items|
126
+ namespace_digests = mso.value_digests[namespace] || {}
127
+ claims = items.to_h do |item|
128
+ [item.element_identifier, verified_element_value(item, namespace, namespace_digests, mso)]
129
+ end
130
+ [namespace, claims]
131
+ end
132
+ end
133
+ private_class_method :verify_disclosed_items!
134
+
135
+ def verified_element_value(item, namespace, namespace_digests, mso)
136
+ expected = namespace_digests[item.digest_id]
137
+ if expected.nil?
138
+ raise OpenID4VP::PresentationInvalid, "mdoc: unknown digest_id #{item.digest_id} in namespace #{namespace}"
139
+ end
140
+
141
+ actual = item.digest(mso.digest_algorithm)
142
+ unless OpenID4VP::Util.secure_compare(actual, expected.b)
143
+ raise OpenID4VP::PresentationInvalid,
144
+ "mdoc: digest mismatch for #{namespace}/#{item.element_identifier}"
145
+ end
146
+
147
+ item.element_value
148
+ end
149
+ private_class_method :verified_element_value
150
+
151
+ def verify_device_signature!(document, mso, session_transcript)
152
+ device_signed = document.device_signed
153
+ device_signature = device_signed.device_auth.fetch("deviceSignature") do
154
+ raise OpenID4VP::HolderBindingInvalid, "mdoc: device signature invalid: missing deviceSignature"
155
+ end
156
+ check_allowed_alg!(device_signature.headers.alg, OpenID4VP::HolderBindingInvalid, "DeviceSignature")
157
+ device_authentication_bytes = DeviceAuthentication.bytes(
158
+ session_transcript: session_transcript, doc_type: document.doc_type,
159
+ name_spaces_bytes: device_signed.name_spaces_bytes
160
+ )
161
+ key = CoseKey.with_kid(CoseKey.from_jwk(mso.device_key), device_signature.headers.kid)
162
+ device_signature.verify(key, nil, detached_payload: device_authentication_bytes)
163
+ rescue ::COSE::Error, ::OpenSSL::SignatureAlgorithm::Error, ::OpenSSL::OpenSSLError => e
164
+ raise OpenID4VP::HolderBindingInvalid, "mdoc: device signature invalid: #{e.message}"
165
+ end
166
+ private_class_method :verify_device_signature!
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenID4VP
4
+ # ISO mdoc (ISO/IEC 18013-5) structures used to bind a DeviceResponse to an
5
+ # OpenID4VP request (OpenID4VP §B.2.6): OpenID4VP::Mdoc::Handover and
6
+ # OpenID4VP::Mdoc::SessionTranscript.
7
+ module Mdoc
8
+ end
9
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../metadata"
4
+ require_relative "../errors"
5
+ require_relative "../jose/jwk_set"
6
+ require_relative "vp_formats"
7
+
8
+ module OpenID4VP
9
+ module Metadata
10
+ # OpenID4VP §5.1/§11: the Verifier's client_metadata, e.g. the value of
11
+ # the client_metadata Authorization Request parameter or of a Verifier's
12
+ # RFC 7591 client registration. Wraps a Hash with String keys; unknown
13
+ # members are preserved as-is in #to_h.
14
+ class VerifierMetadata
15
+ DEFAULT_ENCRYPTED_RESPONSE_ENC_VALUES_SUPPORTED = %w[A128GCM].freeze
16
+
17
+ def initialize(hash)
18
+ raise OpenID4VP::InvalidRequest, "client_metadata must be an object" unless hash.is_a?(::Hash)
19
+
20
+ @hash = hash.dup.freeze
21
+ @memo = {}
22
+ freeze
23
+ end
24
+
25
+ def self.from_h(hash)
26
+ new(hash)
27
+ end
28
+
29
+ def to_h
30
+ @hash.dup
31
+ end
32
+
33
+ # OpenID4VP §5.1: the Verifier's jwks, or nil when absent. Raises
34
+ # InvalidRequest when it is not an object with a "keys" array, when a
35
+ # key is malformed, or when any key is missing its kid.
36
+ def jwks
37
+ memoize(:jwks) { build_jwks }
38
+ end
39
+
40
+ # OpenID4VP §5.1: defaults to ["A128GCM"] when absent.
41
+ def encrypted_response_enc_values_supported
42
+ memoize(:encrypted_response_enc_values_supported) { build_encrypted_response_enc_values_supported }
43
+ end
44
+
45
+ def vp_formats_supported
46
+ memoize(:vp_formats_supported) { build_vp_formats_supported }
47
+ end
48
+
49
+ # A new VerifierMetadata with the given members overridden (string or
50
+ # symbol keys are both accepted).
51
+ def with(**overrides)
52
+ self.class.new(@hash.merge(overrides.transform_keys(&:to_s)))
53
+ end
54
+
55
+ # Picks a key from #jwks usable for encrypting the Authorization
56
+ # Response, or nil when there is no jwks.
57
+ def encryption_key(supported_algs: nil)
58
+ jwks&.select_encryption_key(supported_algs: supported_algs)
59
+ end
60
+
61
+ private
62
+
63
+ def memoize(key)
64
+ return @memo[key] if @memo.key?(key)
65
+
66
+ @memo[key] = yield
67
+ end
68
+
69
+ def build_jwks
70
+ raw = @hash["jwks"]
71
+ return nil if raw.nil?
72
+
73
+ unless raw.is_a?(::Hash) && raw["keys"].is_a?(::Array)
74
+ raise OpenID4VP::InvalidRequest, "jwks must be an object with a keys array"
75
+ end
76
+
77
+ jwk_set = begin
78
+ OpenID4VP::JOSE::JWKSet.from_h(raw)
79
+ rescue OpenID4VP::Error
80
+ raise
81
+ rescue StandardError => e
82
+ raise OpenID4VP::InvalidRequest, "jwks: malformed key (#{e.message})"
83
+ end
84
+
85
+ jwk_set.require_kids!
86
+ jwk_set
87
+ end
88
+
89
+ def build_encrypted_response_enc_values_supported
90
+ value = @hash["encrypted_response_enc_values_supported"]
91
+ return DEFAULT_ENCRYPTED_RESPONSE_ENC_VALUES_SUPPORTED if value.nil?
92
+
93
+ unless value.is_a?(::Array) && !value.empty? && value.all?(::String)
94
+ raise OpenID4VP::InvalidRequest,
95
+ "encrypted_response_enc_values_supported must be a non-empty array of strings"
96
+ end
97
+
98
+ value
99
+ end
100
+
101
+ def build_vp_formats_supported
102
+ value = @hash["vp_formats_supported"]
103
+ return nil if value.nil?
104
+
105
+ VpFormats.from_h(value)
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../metadata"
4
+ require_relative "../errors"
5
+
6
+ module OpenID4VP
7
+ module Metadata
8
+ # OpenID4VP §11.1/§10.1: a vp_formats_supported object, mapping
9
+ # Credential Format Identifiers to format-specific parameters (Appendix
10
+ # B). Wraps a Hash with String keys; unknown formats and unknown
11
+ # per-format parameters are preserved as-is.
12
+ class VpFormats
13
+ KNOWN_PARAMS = {
14
+ "jwt_vc_json" => %w[alg_values],
15
+ "ldp_vc" => %w[proof_type_values cryptosuite_values],
16
+ "mso_mdoc" => %w[issuerauth_alg_values deviceauth_alg_values],
17
+ "dc+sd-jwt" => %w[sd-jwt_alg_values kb-jwt_alg_values]
18
+ }.freeze
19
+
20
+ def initialize(hash)
21
+ raise OpenID4VP::InvalidRequest, "vp_formats_supported must be an object" unless hash.is_a?(::Hash)
22
+
23
+ @hash = hash.dup.freeze
24
+ freeze
25
+ end
26
+
27
+ def self.from_h(hash)
28
+ new(hash)
29
+ end
30
+
31
+ def to_h
32
+ @hash.dup
33
+ end
34
+
35
+ def formats
36
+ @hash.keys
37
+ end
38
+
39
+ def supports?(format)
40
+ @hash.key?(format)
41
+ end
42
+
43
+ def params_for(format)
44
+ @hash[format]
45
+ end
46
+
47
+ def empty?
48
+ @hash.empty?
49
+ end
50
+
51
+ # Raises InvalidRequest unless every value is an object (a Hash), and
52
+ # every known parameter (per KNOWN_PARAMS) present in it is a
53
+ # non-empty array.
54
+ def validate!
55
+ @hash.each do |format, params|
56
+ unless params.is_a?(::Hash)
57
+ raise OpenID4VP::InvalidRequest, "vp_formats_supported[#{format.inspect}] must be an object"
58
+ end
59
+
60
+ (KNOWN_PARAMS[format] || []).each do |param|
61
+ next unless params.key?(param)
62
+
63
+ value = params[param]
64
+ next if value.is_a?(::Array) && !value.empty?
65
+
66
+ raise OpenID4VP::InvalidRequest,
67
+ "vp_formats_supported[#{format.inspect}][#{param.inspect}] must be a non-empty array"
68
+ end
69
+ end
70
+ self
71
+ end
72
+
73
+ # Common formats between self and other; for each known parameter
74
+ # present on both sides, keeps the array intersection (dropping the
75
+ # format entirely if any common known parameter intersects to empty).
76
+ # A known parameter present on only one side is kept as-is. Formats
77
+ # not listed in KNOWN_PARAMS are intersected by name only.
78
+ def intersect(other)
79
+ result = (formats & other.formats).each_with_object({}) do |format, acc|
80
+ merged = intersect_format(format, params_for(format) || {}, other.params_for(format) || {})
81
+ acc[format] = merged unless merged.nil?
82
+ end
83
+ self.class.new(result)
84
+ end
85
+
86
+ private
87
+
88
+ def intersect_format(format, self_params, other_params)
89
+ known = KNOWN_PARAMS[format]
90
+ return {} unless known
91
+
92
+ merged = {}
93
+ known.each do |param|
94
+ in_self = self_params.key?(param)
95
+ in_other = other_params.key?(param)
96
+
97
+ if in_self && in_other
98
+ common = self_params[param] & other_params[param]
99
+ return nil if common.empty?
100
+
101
+ merged[param] = common
102
+ elsif in_self
103
+ merged[param] = self_params[param]
104
+ elsif in_other
105
+ merged[param] = other_params[param]
106
+ end
107
+ end
108
+ merged
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../metadata"
4
+ require_relative "../errors"
5
+ require_relative "../jose/jwk_set"
6
+ require_relative "../client_id"
7
+ require_relative "vp_formats"
8
+
9
+ module OpenID4VP
10
+ module Metadata
11
+ # OpenID4VP §10: the Wallet's (Authorization Server) metadata, e.g. as
12
+ # obtained via RFC 8414 or pre-agreed out of band (§13.1.2). Wraps a Hash
13
+ # with String keys; unknown members are preserved as-is in #to_h.
14
+ class WalletMetadata
15
+ DEFAULT_CLIENT_ID_PREFIXES_SUPPORTED = [OpenID4VP::ClientId::PRE_REGISTERED].freeze
16
+
17
+ def initialize(hash)
18
+ raise OpenID4VP::InvalidRequest, "wallet metadata must be an object" unless hash.is_a?(::Hash)
19
+
20
+ @hash = hash.dup.freeze
21
+ @memo = {}
22
+ freeze
23
+ end
24
+
25
+ def self.from_h(hash)
26
+ new(hash)
27
+ end
28
+
29
+ # OpenID4VP §13.1.2: the non-normative set of static configuration
30
+ # values bound to the openid4vp: custom URL scheme.
31
+ def self.static_openid4vp
32
+ new(
33
+ "authorization_endpoint" => "openid4vp:",
34
+ "response_types_supported" => %w[vp_token],
35
+ "vp_formats_supported" => {
36
+ "dc+sd-jwt" => {
37
+ "sd-jwt_alg_values" => %w[ES256],
38
+ "kb-jwt_alg_values" => %w[ES256]
39
+ },
40
+ "mso_mdoc" => {}
41
+ },
42
+ "request_object_signing_alg_values_supported" => %w[ES256]
43
+ )
44
+ end
45
+
46
+ def to_h
47
+ @hash.dup
48
+ end
49
+
50
+ # OpenID4VP §10.1: required. Raises InvalidRequest when absent.
51
+ def vp_formats_supported
52
+ memoize(:vp_formats_supported) { build_vp_formats_supported }
53
+ end
54
+
55
+ # OpenID4VP §10.1: defaults to ["pre-registered"] when absent.
56
+ def client_id_prefixes_supported
57
+ memoize(:client_id_prefixes_supported) { build_client_id_prefixes_supported }
58
+ end
59
+
60
+ def authorization_endpoint
61
+ @hash["authorization_endpoint"]
62
+ end
63
+
64
+ def response_types_supported
65
+ @hash["response_types_supported"]
66
+ end
67
+
68
+ def response_modes_supported
69
+ @hash["response_modes_supported"]
70
+ end
71
+
72
+ def request_object_signing_alg_values_supported
73
+ @hash["request_object_signing_alg_values_supported"]
74
+ end
75
+
76
+ def authorization_encryption_alg_values_supported
77
+ @hash["authorization_encryption_alg_values_supported"]
78
+ end
79
+
80
+ def authorization_encryption_enc_values_supported
81
+ @hash["authorization_encryption_enc_values_supported"]
82
+ end
83
+
84
+ def jwks
85
+ memoize(:jwks) { build_jwks }
86
+ end
87
+
88
+ # Whether this Wallet supports the Client Identifier Prefix of
89
+ # client_id (or "pre-registered" when client_id has none).
90
+ def supports_prefix?(client_id)
91
+ prefix = client_id.pre_registered? ? OpenID4VP::ClientId::PRE_REGISTERED : client_id.prefix
92
+ client_id_prefixes_supported.include?(prefix)
93
+ end
94
+
95
+ private
96
+
97
+ def memoize(key)
98
+ return @memo[key] if @memo.key?(key)
99
+
100
+ @memo[key] = yield
101
+ end
102
+
103
+ def build_vp_formats_supported
104
+ value = @hash["vp_formats_supported"]
105
+ raise OpenID4VP::InvalidRequest, "vp_formats_supported is required" if value.nil?
106
+
107
+ VpFormats.from_h(value)
108
+ end
109
+
110
+ def build_client_id_prefixes_supported
111
+ value = @hash["client_id_prefixes_supported"]
112
+ return DEFAULT_CLIENT_ID_PREFIXES_SUPPORTED if value.nil?
113
+
114
+ unless value.is_a?(::Array) && !value.empty? && value.all?(::String)
115
+ raise OpenID4VP::InvalidRequest, "client_id_prefixes_supported must be a non-empty array of strings"
116
+ end
117
+
118
+ value
119
+ end
120
+
121
+ def build_jwks
122
+ raw = @hash["jwks"]
123
+ return nil if raw.nil?
124
+
125
+ unless raw.is_a?(::Hash) && raw["keys"].is_a?(::Array)
126
+ raise OpenID4VP::InvalidRequest, "jwks must be an object with a keys array"
127
+ end
128
+
129
+ begin
130
+ OpenID4VP::JOSE::JWKSet.from_h(raw)
131
+ rescue OpenID4VP::Error
132
+ raise
133
+ rescue StandardError => e
134
+ raise OpenID4VP::InvalidRequest, "jwks: malformed key (#{e.message})"
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenID4VP
4
+ # Verifier and Wallet metadata (OpenID4VP §5.1, §10, §11): the
5
+ # vp_formats_supported object (VpFormats), the Verifier's client_metadata
6
+ # (VerifierMetadata) and the Wallet's authorization server metadata
7
+ # (WalletMetadata).
8
+ module Metadata
9
+ end
10
+ end