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,302 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+ require_relative "client_id"
5
+ require_relative "response_mode"
6
+ require_relative "response_type"
7
+ require_relative "dcql/query"
8
+ require_relative "metadata/verifier_metadata"
9
+ require_relative "transaction_data"
10
+ require_relative "verifier_info"
11
+ require_relative "util"
12
+ require_relative "util/uri"
13
+ require_relative "authorization_request/validator"
14
+
15
+ module OpenID4VP
16
+ # OpenID4VP §5-5.3: the Authorization Request parameters. Immutable value
17
+ # object over a Hash with String keys; typed accessors lazily parse the
18
+ # structured parameters, and #validate! performs the full set of §5
19
+ # request-level checks used by both Verifier (building) and Wallet
20
+ # (validating).
21
+ class AuthorizationRequest
22
+ # Parameters whose value is a JSON-serialized object/array when carried
23
+ # as an application/x-www-form-urlencoded value (OpenID4VP §5.1).
24
+ JSON_PARAMS = %w[
25
+ dcql_query client_metadata verifier_info expected_origins trust_chain transaction_data
26
+ ].freeze
27
+
28
+ KNOWN_KEYS = %w[
29
+ response_type response_mode client_id nonce state scope redirect_uri response_uri
30
+ dcql_query client_metadata request_uri request_uri_method request transaction_data
31
+ verifier_info expected_origins wallet_nonce trust_chain aud
32
+ ].freeze
33
+
34
+ include Validator
35
+
36
+ attr_reader :params
37
+
38
+ # `params` keys are normalized to Strings; values may already be decoded
39
+ # (Hash/Array/domain objects) or JSON-serialized Strings for the members
40
+ # listed in JSON_PARAMS (decoded lazily by the typed accessors).
41
+ def initialize(params)
42
+ raise OpenID4VP::InvalidRequest, "authorization request params must be an object" unless params.is_a?(::Hash)
43
+
44
+ @params = params.transform_keys(&:to_s).freeze
45
+ @memo = {}
46
+ freeze
47
+ end
48
+
49
+ # Decodes JSON-string values of the JSON_PARAMS members (transaction_data
50
+ # may arrive as a JSON-array string or already as an Array of base64url
51
+ # strings). Symbol keys are normalized to Strings.
52
+ def self.from_params(hash)
53
+ new(hash.each_with_object({}) { |(key, value), out| out[key.to_s] = decode_param(key.to_s, value) })
54
+ end
55
+
56
+ def self.from_url(url)
57
+ from_params(OpenID4VP::Util::URI.params_from(url))
58
+ end
59
+
60
+ def self.decode_param(key, value)
61
+ return value unless value.is_a?(::String) && JSON_PARAMS.include?(key)
62
+
63
+ begin
64
+ ::JSON.parse(value)
65
+ rescue ::JSON::ParserError
66
+ raise OpenID4VP::InvalidRequest, "#{key} is not valid JSON"
67
+ end
68
+ end
69
+ private_class_method :decode_param
70
+
71
+ def [](key)
72
+ @params[key.to_s]
73
+ end
74
+
75
+ # Unrecognized params, e.g. profile-specific extensions.
76
+ def extra
77
+ @params.except(*KNOWN_KEYS)
78
+ end
79
+
80
+ def response_type
81
+ self["response_type"]
82
+ end
83
+
84
+ def response_mode
85
+ self["response_mode"] || OpenID4VP::ResponseMode.default
86
+ end
87
+
88
+ def client_id
89
+ memoize(:client_id) do
90
+ raw = self["client_id"]
91
+ next nil if raw.nil?
92
+
93
+ raw.is_a?(OpenID4VP::ClientId) ? raw : OpenID4VP::ClientId.parse(raw)
94
+ end
95
+ end
96
+
97
+ def nonce
98
+ self["nonce"]
99
+ end
100
+
101
+ def state
102
+ self["state"]
103
+ end
104
+
105
+ def scope
106
+ self["scope"]
107
+ end
108
+
109
+ def redirect_uri
110
+ self["redirect_uri"]
111
+ end
112
+
113
+ def response_uri
114
+ self["response_uri"]
115
+ end
116
+
117
+ def request_uri
118
+ self["request_uri"]
119
+ end
120
+
121
+ def request_uri_method
122
+ self["request_uri_method"]
123
+ end
124
+
125
+ def request
126
+ self["request"]
127
+ end
128
+
129
+ def wallet_nonce
130
+ self["wallet_nonce"]
131
+ end
132
+
133
+ def aud
134
+ self["aud"]
135
+ end
136
+
137
+ def trust_chain
138
+ decoded("trust_chain")
139
+ end
140
+
141
+ def dcql_query
142
+ memoize(:dcql_query) do
143
+ raw = decoded("dcql_query")
144
+ next nil if raw.nil?
145
+
146
+ raw.is_a?(OpenID4VP::DCQL::Query) ? raw : OpenID4VP::DCQL::Query.from_h(raw)
147
+ end
148
+ end
149
+
150
+ def client_metadata
151
+ memoize(:client_metadata) do
152
+ raw = decoded("client_metadata")
153
+ next nil if raw.nil?
154
+
155
+ raw.is_a?(OpenID4VP::Metadata::VerifierMetadata) ? raw : OpenID4VP::Metadata::VerifierMetadata.new(raw)
156
+ end
157
+ end
158
+
159
+ def transaction_data
160
+ memoize(:transaction_data) do
161
+ raw = decoded("transaction_data")
162
+ next nil if raw.nil?
163
+
164
+ if raw.is_a?(::Array) && raw.all?(OpenID4VP::TransactionData::Entry)
165
+ raw
166
+ else
167
+ OpenID4VP::TransactionData.parse_all(raw)
168
+ end
169
+ end
170
+ end
171
+
172
+ def verifier_info
173
+ memoize(:verifier_info) do
174
+ raw = decoded("verifier_info")
175
+ next nil if raw.nil?
176
+
177
+ if raw.is_a?(::Array) && raw.all?(OpenID4VP::VerifierInfo::Entry)
178
+ raw
179
+ else
180
+ OpenID4VP::VerifierInfo.parse_all(raw)
181
+ end
182
+ end
183
+ end
184
+
185
+ def expected_origins
186
+ memoize(:expected_origins) do
187
+ raw = decoded("expected_origins")
188
+ next nil if raw.nil?
189
+
190
+ unless raw.is_a?(::Array) && !raw.empty? && raw.all?(::String)
191
+ raise OpenID4VP::InvalidRequest, "expected_origins must be a non-empty array of strings"
192
+ end
193
+
194
+ raw
195
+ end
196
+ end
197
+
198
+ # The dcql_query in effect: dcql_query itself, or (only meaningful after
199
+ # #validate! has resolved it) the scope-resolved query. Before #validate!,
200
+ # with only a scope present, this returns nil.
201
+ def effective_dcql_query
202
+ return @memo[:effective_dcql_query] if @memo.key?(:effective_dcql_query)
203
+
204
+ self["dcql_query"].nil? ? nil : dcql_query
205
+ end
206
+
207
+ # OpenID4VP §8.2: response_uri for direct_post response modes; otherwise
208
+ # redirect_uri, falling back to the `original` of a `redirect_uri:`
209
+ # prefixed client_id (§5.9) when redirect_uri is absent.
210
+ def return_uri
211
+ if OpenID4VP::ResponseMode.direct_post?(response_mode)
212
+ response_uri
213
+ else
214
+ redirect_uri || redirect_uri_from_client_id
215
+ end
216
+ end
217
+
218
+ # The audience to bind a signed request/response to: "origin:<origin>"
219
+ # for the DC API (origin required), otherwise the client_id.
220
+ def audience(origin: nil)
221
+ if OpenID4VP::ResponseMode.dc_api?(response_mode)
222
+ raise OpenID4VP::InvalidRequest, "origin required" if origin.nil?
223
+
224
+ OpenID4VP::ClientId.origin(origin).to_s
225
+ else
226
+ cid = client_id
227
+ raise OpenID4VP::InvalidRequest, "client_id is required" if cid.nil?
228
+
229
+ cid.to_s
230
+ end
231
+ end
232
+
233
+ # A new AuthorizationRequest with the given members overridden (string or
234
+ # symbol keys are both accepted).
235
+ def with(**overrides)
236
+ self.class.new(@params.merge(overrides.transform_keys(&:to_s)))
237
+ end
238
+
239
+ def to_h
240
+ @params.each_with_object({}) { |(key, _), out| out[key] = h_value(key) }
241
+ end
242
+
243
+ def to_form_params
244
+ @params.each_with_object({}) { |(key, _), out| out[key] = form_value(key) }
245
+ end
246
+
247
+ def to_url(authorization_endpoint)
248
+ OpenID4VP::Util::URI.with_query(authorization_endpoint, to_form_params)
249
+ end
250
+
251
+ private
252
+
253
+ def memoize(key)
254
+ return @memo[key] if @memo.key?(key)
255
+
256
+ @memo[key] = yield
257
+ end
258
+
259
+ def decoded(key)
260
+ self.class.send(:decode_param, key, self[key])
261
+ end
262
+
263
+ def redirect_uri_from_client_id
264
+ cid = client_id
265
+ return nil unless cid && cid.prefix == "redirect_uri"
266
+
267
+ cid.original
268
+ end
269
+
270
+ def h_value(key)
271
+ case key
272
+ when "dcql_query"
273
+ raw = decoded(key)
274
+ raw.is_a?(OpenID4VP::DCQL::Query) ? raw.to_h : raw
275
+ when "client_metadata"
276
+ raw = decoded(key)
277
+ raw.is_a?(OpenID4VP::Metadata::VerifierMetadata) ? raw.to_h : raw
278
+ when "transaction_data"
279
+ normalize_array(decoded(key)) { |item| item.is_a?(OpenID4VP::TransactionData::Entry) ? item.raw : item }
280
+ when "verifier_info"
281
+ normalize_array(decoded(key)) { |item| item.is_a?(OpenID4VP::VerifierInfo::Entry) ? item.to_h : item }
282
+ when *JSON_PARAMS
283
+ decoded(key)
284
+ else
285
+ self[key]
286
+ end
287
+ end
288
+
289
+ def form_value(key)
290
+ value = h_value(key)
291
+ return value if value.nil? || value.is_a?(::String)
292
+
293
+ JSON_PARAMS.include?(key) ? ::JSON.generate(value) : value.to_s
294
+ end
295
+
296
+ def normalize_array(value, &)
297
+ return value unless value.is_a?(::Array)
298
+
299
+ value.map(&)
300
+ end
301
+ end
302
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "vp_token"
4
+ require_relative "response_mode"
5
+ require_relative "errors"
6
+ require_relative "util/uri"
7
+ require_relative "util/form"
8
+ require_relative "jose/jwe"
9
+ require_relative "encoded_response"
10
+
11
+ module OpenID4VP
12
+ # OpenID4VP §8: the Authorization Response carrying the VP Token, encoded
13
+ # for and decoded from any Response Mode (fragment, query,
14
+ # direct_post[.jwt], dc_api[.jwt]).
15
+ class AuthorizationResponse
16
+ Encoded = EncodedResponse
17
+ Decoded = Struct.new(:response, :encrypted, :encryption_header, :encryption_key, keyword_init: true)
18
+
19
+ EXTRA_EXCLUDED_KEYS = %w[vp_token state error error_description].freeze
20
+
21
+ attr_reader :vp_token, :state, :extra
22
+
23
+ def initialize(vp_token:, state: nil, extra: {})
24
+ @vp_token = vp_token.is_a?(VpToken) ? vp_token : VpToken.parse(vp_token)
25
+ @state = state
26
+ @extra = extra.each_with_object({}) { |(k, v), h| h[k.to_s] = v }.freeze
27
+ freeze
28
+ end
29
+
30
+ def to_params
31
+ { "vp_token" => @vp_token.to_json }.merge(state_param).merge(@extra)
32
+ end
33
+
34
+ def to_payload
35
+ { "vp_token" => @vp_token.to_h }.merge(state_param).merge(@extra)
36
+ end
37
+
38
+ # Encodes this response for `response_mode`. `redirect_uri` is required
39
+ # for the `fragment`/`query` Response Modes; `encryption` (a
40
+ # ResponseEncryption) is required for the `.jwt` Response Modes. Raises
41
+ # OpenID4VP::ConfigurationError when a required argument is missing.
42
+ def encode(response_mode, redirect_uri: nil, encryption: nil)
43
+ validate_response_mode!(response_mode)
44
+ return encode_redirect(response_mode, redirect_uri) if ResponseMode.redirect?(response_mode)
45
+ return encode_encrypted(response_mode, encryption) if ResponseMode.encrypted?(response_mode)
46
+
47
+ encode_plain(response_mode)
48
+ end
49
+
50
+ class << self
51
+ # Reverses #encode. `params` is a String-keyed Hash: form params for
52
+ # direct_post[.jwt], fragment/query params, or the dc_api[.jwt] data
53
+ # object. Raises OpenID4VP::InvalidResponse on any shape problem.
54
+ def decode(response_mode, params, decryption_keys: [])
55
+ effective_params, encrypted, header, key = resolve_params(response_mode, params, decryption_keys)
56
+ Decoded.new(
57
+ response: build_response(effective_params),
58
+ encrypted: encrypted, encryption_header: header, encryption_key: key
59
+ )
60
+ end
61
+
62
+ private
63
+
64
+ def resolve_params(response_mode, params, decryption_keys)
65
+ return [params, false, nil, nil] unless ResponseMode.encrypted?(response_mode)
66
+
67
+ if params["response"]
68
+ decrypted = decrypt_response(params["response"], decryption_keys)
69
+ [decrypted.payload, true, decrypted.header, decrypted.key]
70
+ elsif params["error"]
71
+ [params, false, nil, nil]
72
+ else
73
+ raise OpenID4VP::InvalidResponse, "missing response parameter"
74
+ end
75
+ end
76
+
77
+ def decrypt_response(compact, decryption_keys)
78
+ decrypted = OpenID4VP::JOSE::JWE.decrypt(compact, keys: decryption_keys)
79
+ unless decrypted.payload.is_a?(::Hash)
80
+ raise OpenID4VP::InvalidResponse, "encrypted response payload is not a JSON object"
81
+ end
82
+
83
+ decrypted
84
+ rescue OpenID4VP::VerificationError => e
85
+ raise OpenID4VP::InvalidResponse, "response decryption failed: #{e.message}"
86
+ end
87
+
88
+ def build_response(params)
89
+ if params["error"]
90
+ return OpenID4VP::ErrorResponse.new(
91
+ error: params["error"], error_description: params["error_description"], state: params["state"]
92
+ )
93
+ end
94
+
95
+ vp_token = params["vp_token"]
96
+ raise OpenID4VP::InvalidResponse, "missing vp_token" unless vp_token.is_a?(::String) || vp_token.is_a?(::Hash)
97
+
98
+ extra = params.except(*EXTRA_EXCLUDED_KEYS)
99
+ new(vp_token: VpToken.parse(vp_token), state: params["state"], extra: extra)
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ def state_param
106
+ @state.nil? ? {} : { "state" => @state }
107
+ end
108
+
109
+ def validate_response_mode!(response_mode)
110
+ return if ResponseMode::ALL.include?(response_mode)
111
+
112
+ raise OpenID4VP::ConfigurationError, "unsupported response_mode #{response_mode.inspect}"
113
+ end
114
+
115
+ def encode_redirect(response_mode, redirect_uri)
116
+ if redirect_uri.nil?
117
+ raise OpenID4VP::ConfigurationError, "redirect_uri is required for response_mode #{response_mode}"
118
+ end
119
+
120
+ url =
121
+ if response_mode == ResponseMode::FRAGMENT
122
+ Util::URI.with_fragment(redirect_uri, to_params)
123
+ else
124
+ Util::URI.with_query(redirect_uri, to_params)
125
+ end
126
+ Encoded.new(mode: response_mode, url: url)
127
+ end
128
+
129
+ def encode_encrypted(response_mode, encryption)
130
+ if encryption.nil?
131
+ raise OpenID4VP::ConfigurationError, "encryption is required for response_mode #{response_mode}"
132
+ end
133
+
134
+ jwe = encryption.encrypt(to_payload)
135
+ if ResponseMode.direct_post?(response_mode)
136
+ Encoded.new(mode: response_mode, form_params: { "response" => jwe },
137
+ form_body: Util::Form.encode("response" => jwe), jwe: jwe)
138
+ else
139
+ Encoded.new(mode: response_mode, data: { "response" => jwe }, jwe: jwe)
140
+ end
141
+ end
142
+
143
+ def encode_plain(response_mode)
144
+ if ResponseMode.direct_post?(response_mode)
145
+ Encoded.new(mode: response_mode, form_params: to_params, form_body: Util::Form.encode(to_params))
146
+ else
147
+ Encoded.new(mode: response_mode, data: to_payload)
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+
5
+ module OpenID4VP
6
+ class ClientId
7
+ PREFIXES = %w[
8
+ redirect_uri openid_federation decentralized_identifier
9
+ verifier_attestation x509_san_dns x509_hash origin
10
+ ].freeze
11
+ PRE_REGISTERED = "pre-registered"
12
+
13
+ SIGNED_REQUEST_REQUIRED = %w[
14
+ openid_federation decentralized_identifier verifier_attestation
15
+ x509_san_dns x509_hash
16
+ ].freeze
17
+ SIGNED_REQUEST_NOT_ALLOWED = %w[redirect_uri].freeze
18
+
19
+ def self.parse(input)
20
+ raise InvalidRequest, "client_id must be a non-empty string" unless input.is_a?(String) && !input.empty?
21
+
22
+ colon_index = input.index(":")
23
+
24
+ if colon_index.nil?
25
+ # No colon: pre-registered id
26
+ new(prefix: PRE_REGISTERED, original: input, candidate_prefix: nil)
27
+ else
28
+ potential_prefix = input[0...colon_index]
29
+ remainder = input[(colon_index + 1)..]
30
+
31
+ if PREFIXES.include?(potential_prefix)
32
+ # Known prefix
33
+ new(prefix: potential_prefix, original: remainder, candidate_prefix: potential_prefix)
34
+ else
35
+ # Unknown prefix
36
+ new(prefix: PRE_REGISTERED, original: input, candidate_prefix: potential_prefix)
37
+ end
38
+ end
39
+ end
40
+
41
+ def self.origin(origin_str)
42
+ new(prefix: "origin", original: origin_str, candidate_prefix: "origin")
43
+ end
44
+
45
+ def initialize(prefix:, original:, candidate_prefix: nil)
46
+ @prefix = prefix.freeze
47
+ @original = original.freeze
48
+ @candidate_prefix = candidate_prefix.freeze
49
+ freeze
50
+ end
51
+
52
+ attr_reader :prefix, :original, :candidate_prefix
53
+
54
+ def to_s
55
+ if prefix == PRE_REGISTERED
56
+ original
57
+ else
58
+ "#{prefix}:#{original}"
59
+ end
60
+ end
61
+
62
+ def pre_registered?
63
+ prefix == PRE_REGISTERED
64
+ end
65
+
66
+ def unknown_prefix?
67
+ pre_registered? && !candidate_prefix.nil?
68
+ end
69
+
70
+ def signed_request_required?
71
+ SIGNED_REQUEST_REQUIRED.include?(prefix)
72
+ end
73
+
74
+ def signed_request_allowed?
75
+ !SIGNED_REQUEST_NOT_ALLOWED.include?(prefix)
76
+ end
77
+
78
+ def ==(other)
79
+ other.is_a?(ClientId) && to_s == other.to_s
80
+ end
81
+
82
+ def eql?(other)
83
+ self == other
84
+ end
85
+
86
+ def hash
87
+ to_s.hash
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenID4VP
4
+ # Framework-agnostic view of a Credential held by a Wallet (or decoded from
5
+ # a Verifier's Presentation), used by `OpenID4VP::DCQL::Evaluator` on both
6
+ # sides. Immutable.
7
+ class Credential
8
+ attr_reader :format, :claims, :raw, :holder_key, :x5chain, :metadata
9
+
10
+ # format: e.g. "dc+sd-jwt", "mso_mdoc", "jwt_vc_json", "ldp_vc".
11
+ # claims: JSON-like Hash used by DCQL::ClaimsPathPointer; for mso_mdoc,
12
+ # {namespace => {element => value}}.
13
+ # raw: the issued/presented artifact (SD-JWT string, VC JWT, mdoc
14
+ # IssuerSigned CBOR, ...), or nil.
15
+ # holder_key: the credential's holder binding key material, or nil.
16
+ # x5chain: Array of OpenSSL::X509::Certificate, leaf first.
17
+ # metadata: format-specific descriptors, e.g. {"vct"=>, "doctype"=>,
18
+ # "type"=>[...], "issuer"=>}.
19
+ def initialize(format:, claims:, raw: nil, holder_key: nil, x5chain: [], metadata: {})
20
+ @format = format
21
+ @claims = deep_freeze(Marshal.load(Marshal.dump(claims)))
22
+ @raw = raw
23
+ @holder_key = holder_key
24
+ @x5chain = x5chain.dup.freeze
25
+ @metadata = metadata.dup.freeze
26
+ end
27
+
28
+ def holder_bindable?
29
+ !holder_key.nil?
30
+ end
31
+
32
+ private
33
+
34
+ # Deep-copies (via Marshal) and freezes `claims` so the Credential is
35
+ # actually immutable, without mutating/freezing the caller's own
36
+ # objects. Suitable for JSON-like data (Hash/Array/String/Integer/
37
+ # Float/true/false/nil) and mdoc byte strings.
38
+ def deep_freeze(obj)
39
+ case obj
40
+ when ::Hash
41
+ obj.each_pair do |key, value|
42
+ deep_freeze(key)
43
+ deep_freeze(value)
44
+ end
45
+ when ::Array
46
+ obj.each { |value| deep_freeze(value) }
47
+ end
48
+ obj.freeze
49
+ end
50
+ end
51
+ end