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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +39 -0
- data/LICENSE.txt +21 -0
- data/README.md +278 -0
- data/lib/openid4vp/authorization_request/validator.rb +177 -0
- data/lib/openid4vp/authorization_request.rb +302 -0
- data/lib/openid4vp/authorization_response.rb +151 -0
- data/lib/openid4vp/client_id.rb +90 -0
- data/lib/openid4vp/credential.rb +51 -0
- data/lib/openid4vp/dc_api.rb +116 -0
- data/lib/openid4vp/dcql/claims_path_pointer.rb +109 -0
- data/lib/openid4vp/dcql/claims_query.rb +76 -0
- data/lib/openid4vp/dcql/credential_query.rb +178 -0
- data/lib/openid4vp/dcql/credential_set_query.rb +49 -0
- data/lib/openid4vp/dcql/evaluator.rb +228 -0
- data/lib/openid4vp/dcql/matcher.rb +79 -0
- data/lib/openid4vp/dcql/meta.rb +99 -0
- data/lib/openid4vp/dcql/query.rb +93 -0
- data/lib/openid4vp/dcql/selection.rb +45 -0
- data/lib/openid4vp/dcql/trusted_authorities_query.rb +48 -0
- data/lib/openid4vp/dcql.rb +10 -0
- data/lib/openid4vp/encoded_response.rb +10 -0
- data/lib/openid4vp/error_response.rb +82 -0
- data/lib/openid4vp/errors.rb +73 -0
- data/lib/openid4vp/formats/base.rb +74 -0
- data/lib/openid4vp/formats/context.rb +56 -0
- data/lib/openid4vp/formats/jwt_vc_json.rb +274 -0
- data/lib/openid4vp/formats/ldp_vc.rb +160 -0
- data/lib/openid4vp/formats/mso_mdoc.rb +204 -0
- data/lib/openid4vp/formats/registry.rb +53 -0
- data/lib/openid4vp/formats/sd_jwt_vc.rb +132 -0
- data/lib/openid4vp/formats/verified_presentation.rb +45 -0
- data/lib/openid4vp/formats.rb +12 -0
- data/lib/openid4vp/jose/algorithms.rb +46 -0
- data/lib/openid4vp/jose/jwe.rb +184 -0
- data/lib/openid4vp/jose/jwk.rb +253 -0
- data/lib/openid4vp/jose/jwk_set.rb +57 -0
- data/lib/openid4vp/jose/jws.rb +225 -0
- data/lib/openid4vp/jose.rb +8 -0
- data/lib/openid4vp/mdoc/builder.rb +132 -0
- data/lib/openid4vp/mdoc/cbor_values.rb +32 -0
- data/lib/openid4vp/mdoc/cose_key.rb +71 -0
- data/lib/openid4vp/mdoc/device_authentication.rb +32 -0
- data/lib/openid4vp/mdoc/device_response.rb +77 -0
- data/lib/openid4vp/mdoc/device_signed.rb +111 -0
- data/lib/openid4vp/mdoc/document.rb +44 -0
- data/lib/openid4vp/mdoc/handover.rb +51 -0
- data/lib/openid4vp/mdoc/issuer_signed.rb +157 -0
- data/lib/openid4vp/mdoc/mso.rb +133 -0
- data/lib/openid4vp/mdoc/session_transcript.rb +49 -0
- data/lib/openid4vp/mdoc/sign1_decoder.rb +60 -0
- data/lib/openid4vp/mdoc/verifier.rb +169 -0
- data/lib/openid4vp/mdoc.rb +9 -0
- data/lib/openid4vp/metadata/verifier_metadata.rb +109 -0
- data/lib/openid4vp/metadata/vp_formats.rb +112 -0
- data/lib/openid4vp/metadata/wallet_metadata.rb +139 -0
- data/lib/openid4vp/metadata.rb +10 -0
- data/lib/openid4vp/request_object.rb +152 -0
- data/lib/openid4vp/request_verification/base.rb +63 -0
- data/lib/openid4vp/request_verification/config.rb +65 -0
- data/lib/openid4vp/request_verification/decentralized_identifier.rb +56 -0
- data/lib/openid4vp/request_verification/openid_federation.rb +52 -0
- data/lib/openid4vp/request_verification/pre_registered.rb +54 -0
- data/lib/openid4vp/request_verification/redirect_uri.rb +29 -0
- data/lib/openid4vp/request_verification/resolver.rb +76 -0
- data/lib/openid4vp/request_verification/verified_request.rb +12 -0
- data/lib/openid4vp/request_verification/verifier_attestation.rb +54 -0
- data/lib/openid4vp/request_verification/x509_hash.rb +40 -0
- data/lib/openid4vp/request_verification/x509_san_dns.rb +60 -0
- data/lib/openid4vp/request_verification.rb +10 -0
- data/lib/openid4vp/response_encryption.rb +51 -0
- data/lib/openid4vp/response_endpoint_result.rb +33 -0
- data/lib/openid4vp/response_mode.rb +44 -0
- data/lib/openid4vp/response_type.rb +7 -0
- data/lib/openid4vp/sd_jwt/digest.rb +30 -0
- data/lib/openid4vp/sd_jwt/disclosure.rb +104 -0
- data/lib/openid4vp/sd_jwt/issuer.rb +132 -0
- data/lib/openid4vp/sd_jwt/kb_jwt.rb +82 -0
- data/lib/openid4vp/sd_jwt/parser.rb +48 -0
- data/lib/openid4vp/sd_jwt/presentation.rb +333 -0
- data/lib/openid4vp/sd_jwt/reconstruct.rb +132 -0
- data/lib/openid4vp/sd_jwt.rb +10 -0
- data/lib/openid4vp/transaction_data/registry.rb +42 -0
- data/lib/openid4vp/transaction_data/sd_jwt_hashes.rb +124 -0
- data/lib/openid4vp/transaction_data.rb +138 -0
- data/lib/openid4vp/util/base64url.rb +44 -0
- data/lib/openid4vp/util/form.rb +24 -0
- data/lib/openid4vp/util/random.rb +29 -0
- data/lib/openid4vp/util/uri.rb +48 -0
- data/lib/openid4vp/util.rb +23 -0
- data/lib/openid4vp/verifier/config.rb +216 -0
- data/lib/openid4vp/verifier/created_request.rb +25 -0
- data/lib/openid4vp/verifier/http_result.rb +34 -0
- data/lib/openid4vp/verifier/memory_session_store.rb +102 -0
- data/lib/openid4vp/verifier/request_builder.rb +216 -0
- data/lib/openid4vp/verifier/request_signer.rb +59 -0
- data/lib/openid4vp/verifier/request_uri_endpoint.rb +151 -0
- data/lib/openid4vp/verifier/response_endpoint.rb +197 -0
- data/lib/openid4vp/verifier/response_validator.rb +258 -0
- data/lib/openid4vp/verifier/session.rb +109 -0
- data/lib/openid4vp/verifier/session_store.rb +48 -0
- data/lib/openid4vp/verifier/verified_response.rb +53 -0
- data/lib/openid4vp/verifier.rb +155 -0
- data/lib/openid4vp/verifier_attestation.rb +130 -0
- data/lib/openid4vp/verifier_info.rb +102 -0
- data/lib/openid4vp/version.rb +5 -0
- data/lib/openid4vp/vp_token.rb +81 -0
- data/lib/openid4vp/wallet/config.rb +49 -0
- data/lib/openid4vp/wallet/http_client.rb +73 -0
- data/lib/openid4vp/wallet/presenter.rb +90 -0
- data/lib/openid4vp/wallet/processed_request.rb +14 -0
- data/lib/openid4vp/wallet/request_processor.rb +237 -0
- data/lib/openid4vp/wallet/response_builder.rb +44 -0
- data/lib/openid4vp/wallet/submission.rb +68 -0
- data/lib/openid4vp/wallet.rb +142 -0
- data/lib/openid4vp/x509/chain_validator.rb +42 -0
- data/lib/openid4vp/x509.rb +103 -0
- data/lib/openid4vp.rb +18 -0
- 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
|