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,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "jose/jws"
|
|
4
|
+
require_relative "authorization_request"
|
|
5
|
+
require_relative "x509"
|
|
6
|
+
require_relative "errors"
|
|
7
|
+
|
|
8
|
+
module OpenID4VP
|
|
9
|
+
# OpenID4VP §5 / RFC 9101 (JAR): the signed JWT form of an
|
|
10
|
+
# AuthorizationRequest, sent by value (`request=`) or by reference
|
|
11
|
+
# (`request_uri`). Wraps `OpenID4VP::JOSE::JWS` with the `typ` enforcement,
|
|
12
|
+
# payload shaping and Appendix A.3.2.2 JSON-serialization header-merging
|
|
13
|
+
# rules specific to Request Objects.
|
|
14
|
+
module RequestObject
|
|
15
|
+
TYP = "oauth-authz-req+jwt"
|
|
16
|
+
DEFAULT_AUD = "https://self-issued.me/v2"
|
|
17
|
+
|
|
18
|
+
# Header-only parameters that, in JWS JSON Serialization, MUST live in
|
|
19
|
+
# each signature's protected header rather than the shared payload
|
|
20
|
+
# (Appendix A.3.2.2).
|
|
21
|
+
HEADER_ONLY_PARAMS = %w[client_id verifier_info trust_chain].freeze
|
|
22
|
+
|
|
23
|
+
# A parsed (not necessarily verified) Request Object: either from compact
|
|
24
|
+
# serialization (`compact` set, `json`/`signature_index` nil) or from one
|
|
25
|
+
# signature of a JSON-serialized object (`json`/`signature_index` set,
|
|
26
|
+
# `compact` nil).
|
|
27
|
+
# rubocop:disable-next Lint/StructNewOverride -- :compact is the interface name, not Array's method.
|
|
28
|
+
Parsed = Struct.new(:header, :payload, :compact, :request, :json, :signature_index, keyword_init: true) do
|
|
29
|
+
def x5c_certs
|
|
30
|
+
x5c = header["x5c"]
|
|
31
|
+
x5c ? OpenID4VP::X509.certs_from_x5c(x5c) : []
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def attestation_jwt
|
|
35
|
+
header["jwt"]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def kid
|
|
39
|
+
header["kid"]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def alg
|
|
43
|
+
header["alg"]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def wallet_nonce
|
|
47
|
+
payload["wallet_nonce"]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def aud
|
|
51
|
+
payload["aud"]
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
module_function
|
|
56
|
+
|
|
57
|
+
# Signs `request` (an AuthorizationRequest) as a Request Object,
|
|
58
|
+
# returning the JWS compact serialization.
|
|
59
|
+
def sign(request, key:, alg: nil, x5c: nil, kid: nil, attestation_jwt: nil, wallet_nonce: nil,
|
|
60
|
+
aud: DEFAULT_AUD, extra_header: {})
|
|
61
|
+
payload = request.to_h.merge("aud" => aud)
|
|
62
|
+
payload["wallet_nonce"] = wallet_nonce if wallet_nonce
|
|
63
|
+
header = build_header(x5c: x5c, kid: kid, attestation_jwt: attestation_jwt, extra_header: extra_header)
|
|
64
|
+
OpenID4VP::JOSE::JWS.sign(payload, key: key, alg: alg, header: header)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Parses `compact` into header/payload WITHOUT verifying the signature.
|
|
68
|
+
# Raises InvalidRequest unless the `typ` header is exactly TYP, or the
|
|
69
|
+
# payload is not a JSON object. `iss`, if present, is left in the payload
|
|
70
|
+
# but MUST be ignored by callers (OpenID4VP §5).
|
|
71
|
+
def parse(compact)
|
|
72
|
+
verified = OpenID4VP::JOSE::JWS.peek(compact)
|
|
73
|
+
check_typ!(verified.header)
|
|
74
|
+
payload = check_payload!(verified.payload)
|
|
75
|
+
Parsed.new(header: verified.header, payload: payload, compact: compact,
|
|
76
|
+
request: OpenID4VP::AuthorizationRequest.new(payload))
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Verifies `parsed` (from #parse) against `key`, returning it unchanged.
|
|
80
|
+
# Raises VerificationError on signature failure.
|
|
81
|
+
def verify(parsed, key:, algorithms: OpenID4VP::JOSE::Algorithms::SIGNING)
|
|
82
|
+
OpenID4VP::JOSE::JWS.verify(parsed.compact, key: key, algorithms: algorithms)
|
|
83
|
+
parsed
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Parses a JWS in JSON Serialization (a Hash or JSON String) into one
|
|
87
|
+
# `Parsed` per signature, per Appendix A.3.2.2: the header-only params
|
|
88
|
+
# (client_id, verifier_info, trust_chain) of each signature's protected
|
|
89
|
+
# header are merged into that signature's request params.
|
|
90
|
+
def parse_json(hash_or_string)
|
|
91
|
+
json = OpenID4VP::JOSE::JWS.parse_json(hash_or_string)
|
|
92
|
+
payload = check_payload!(json.payload)
|
|
93
|
+
json.signatures.each_with_index.map { |entry, index| parsed_from_json_entry(json, payload, entry, index) }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Verifies the signature of `parsed` (from #parse_json) against `key`,
|
|
97
|
+
# returning it unchanged. Raises VerificationError on signature failure.
|
|
98
|
+
def verify_json(parsed, key:, algorithms: OpenID4VP::JOSE::Algorithms::SIGNING)
|
|
99
|
+
OpenID4VP::JOSE::JWS.verify_json_signature(parsed.json, parsed.signature_index, key: key, algorithms: algorithms)
|
|
100
|
+
parsed
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def build_header(x5c:, kid:, attestation_jwt:, extra_header:)
|
|
104
|
+
header = { "typ" => TYP }
|
|
105
|
+
header["x5c"] = normalize_x5c(x5c) if x5c
|
|
106
|
+
header["kid"] = kid if kid
|
|
107
|
+
header["jwt"] = attestation_jwt if attestation_jwt
|
|
108
|
+
header.merge(extra_header)
|
|
109
|
+
end
|
|
110
|
+
private_class_method :build_header
|
|
111
|
+
|
|
112
|
+
# Accepts an Array of OpenSSL::X509::Certificate, DER strings, PEM
|
|
113
|
+
# strings, or already-encoded standard-base64 x5c entries. Certificate
|
|
114
|
+
# objects and DER/PEM strings are normalized via X509.x5c_from; a String
|
|
115
|
+
# that does not parse as a certificate is assumed to already be a
|
|
116
|
+
# standard-base64 x5c entry and is passed through unchanged.
|
|
117
|
+
def normalize_x5c(x5c)
|
|
118
|
+
x5c.map { |entry| entry.is_a?(::String) ? normalize_x5c_string(entry) : OpenID4VP::X509.x5c_from([entry]).first }
|
|
119
|
+
end
|
|
120
|
+
private_class_method :normalize_x5c
|
|
121
|
+
|
|
122
|
+
def normalize_x5c_string(str)
|
|
123
|
+
OpenID4VP::X509.x5c_from([str]).first
|
|
124
|
+
rescue OpenID4VP::InvalidRequest
|
|
125
|
+
str
|
|
126
|
+
end
|
|
127
|
+
private_class_method :normalize_x5c_string
|
|
128
|
+
|
|
129
|
+
def check_typ!(header)
|
|
130
|
+
return if header["typ"] == TYP
|
|
131
|
+
|
|
132
|
+
raise OpenID4VP::InvalidRequest, "Request Object typ must be #{TYP}"
|
|
133
|
+
end
|
|
134
|
+
private_class_method :check_typ!
|
|
135
|
+
|
|
136
|
+
def check_payload!(payload)
|
|
137
|
+
raise OpenID4VP::InvalidRequest, "Request Object payload must be an object" unless payload.is_a?(::Hash)
|
|
138
|
+
|
|
139
|
+
payload
|
|
140
|
+
end
|
|
141
|
+
private_class_method :check_payload!
|
|
142
|
+
|
|
143
|
+
def parsed_from_json_entry(json, payload, entry, index)
|
|
144
|
+
check_typ!(entry.protected_header)
|
|
145
|
+
header_only = entry.protected_header.slice(*HEADER_ONLY_PARAMS)
|
|
146
|
+
request_params = payload.merge(header_only)
|
|
147
|
+
Parsed.new(header: entry.protected_header, payload: payload, compact: nil,
|
|
148
|
+
request: OpenID4VP::AuthorizationRequest.new(request_params), json: json, signature_index: index)
|
|
149
|
+
end
|
|
150
|
+
private_class_method :parsed_from_json_entry
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../request_object"
|
|
4
|
+
|
|
5
|
+
module OpenID4VP
|
|
6
|
+
module RequestVerification
|
|
7
|
+
# Base class for per-Client-Identifier-Prefix wallet-side verification
|
|
8
|
+
# strategies (OpenID4VP §5.9.3). Subclasses implement
|
|
9
|
+
# `#verify!(client_id, request, parsed, dc_api:)`, returning a
|
|
10
|
+
# `VerifiedRequest`.
|
|
11
|
+
class Base
|
|
12
|
+
attr_reader :config
|
|
13
|
+
|
|
14
|
+
def initialize(config)
|
|
15
|
+
@config = config
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# client_id: OpenID4VP::ClientId. request: the effective
|
|
19
|
+
# OpenID4VP::AuthorizationRequest (parsed.request when signed).
|
|
20
|
+
# parsed: OpenID4VP::RequestObject::Parsed, or nil when unsigned.
|
|
21
|
+
def verify!(client_id, request, parsed, dc_api: false)
|
|
22
|
+
raise NotImplementedError, "#{self.class} must implement #verify!"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# Verifies the Request Object's signature against `key`, handling both
|
|
28
|
+
# compact (single-signature) and JSON-serialized (multisigned)
|
|
29
|
+
# serializations.
|
|
30
|
+
def verify_signature!(parsed, key)
|
|
31
|
+
if parsed.compact
|
|
32
|
+
OpenID4VP::RequestObject.verify(parsed, key: key, algorithms: config.request_object_signing_algs)
|
|
33
|
+
else
|
|
34
|
+
OpenID4VP::RequestObject.verify_json(parsed, key: key, algorithms: config.request_object_signing_algs)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# `keys` ordered so any key whose `kid` matches `kid` is tried first
|
|
39
|
+
# (when both are present); the rest follow, so a kid mismatch still
|
|
40
|
+
# falls back to trying every candidate key.
|
|
41
|
+
def order_by_kid(keys, kid)
|
|
42
|
+
return keys if kid.nil?
|
|
43
|
+
|
|
44
|
+
matching, rest = keys.partition { |k| k.kid == kid }
|
|
45
|
+
matching + rest
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Verifies `parsed`'s signature, trying each of `keys` in turn (see
|
|
49
|
+
# #order_by_kid) until one succeeds. Returns the key that verified the
|
|
50
|
+
# signature. Raises the last VerificationError when none match.
|
|
51
|
+
def verify_with_keys!(parsed, keys)
|
|
52
|
+
last_error = nil
|
|
53
|
+
keys.each do |key|
|
|
54
|
+
verify_signature!(parsed, key)
|
|
55
|
+
return key
|
|
56
|
+
rescue OpenID4VP::VerificationError => e
|
|
57
|
+
last_error = e
|
|
58
|
+
end
|
|
59
|
+
raise last_error || OpenID4VP::VerificationError, "no matching signing key"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenID4VP
|
|
4
|
+
module RequestVerification
|
|
5
|
+
# Wallet-side configuration for `OpenID4VP::RequestVerification::Resolver`.
|
|
6
|
+
# Immutable; keyword-initialized. `pre_registered_clients` entries are
|
|
7
|
+
# normalized on construction to Hashes with Symbol keys
|
|
8
|
+
# `{ metadata:, jwks:, redirect_uris: }`.
|
|
9
|
+
class Config
|
|
10
|
+
DEFAULT_REQUEST_OBJECT_SIGNING_ALGS = %w[ES256 ES384 ES512 RS256 PS256 EdDSA].freeze
|
|
11
|
+
DEFAULT_LEEWAY = 60
|
|
12
|
+
|
|
13
|
+
attr_reader :x509_trust_anchors, :trusted_client_ids, :attestation_issuers, :did_resolver,
|
|
14
|
+
:federation_resolver, :pre_registered_clients, :request_object_signing_algs,
|
|
15
|
+
:allow_unknown_prefix_as_pre_registered, :now, :leeway
|
|
16
|
+
|
|
17
|
+
def initialize(x509_trust_anchors: [], trusted_client_ids: [], attestation_issuers: {},
|
|
18
|
+
did_resolver: nil, federation_resolver: nil, pre_registered_clients: {},
|
|
19
|
+
request_object_signing_algs: DEFAULT_REQUEST_OBJECT_SIGNING_ALGS,
|
|
20
|
+
allow_unknown_prefix_as_pre_registered: false, now: -> { ::Time.now }, leeway: DEFAULT_LEEWAY)
|
|
21
|
+
@x509_trust_anchors = x509_trust_anchors.dup.freeze
|
|
22
|
+
@trusted_client_ids = trusted_client_ids.map(&:to_s).freeze
|
|
23
|
+
@attestation_issuers = attestation_issuers
|
|
24
|
+
@did_resolver = did_resolver
|
|
25
|
+
@federation_resolver = federation_resolver
|
|
26
|
+
@pre_registered_clients = normalize_pre_registered_clients(pre_registered_clients)
|
|
27
|
+
@request_object_signing_algs = request_object_signing_algs.dup.freeze
|
|
28
|
+
@allow_unknown_prefix_as_pre_registered = allow_unknown_prefix_as_pre_registered
|
|
29
|
+
@now = now
|
|
30
|
+
@leeway = leeway
|
|
31
|
+
freeze
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def current_time
|
|
35
|
+
@now.call
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def normalize_pre_registered_clients(clients)
|
|
41
|
+
clients.to_h { |client_id, entry| [client_id.to_s, normalize_client_entry(entry)] }.freeze
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def normalize_client_entry(entry)
|
|
45
|
+
{
|
|
46
|
+
metadata: normalize_metadata(entry[:metadata]),
|
|
47
|
+
jwks: normalize_jwks(entry[:jwks]),
|
|
48
|
+
redirect_uris: entry[:redirect_uris]&.dup&.freeze
|
|
49
|
+
}.freeze
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def normalize_metadata(metadata)
|
|
53
|
+
return nil if metadata.nil?
|
|
54
|
+
|
|
55
|
+
metadata.is_a?(::Hash) ? OpenID4VP::Metadata::VerifierMetadata.new(metadata) : metadata
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def normalize_jwks(jwks)
|
|
59
|
+
return nil if jwks.nil?
|
|
60
|
+
|
|
61
|
+
jwks.is_a?(::Hash) ? OpenID4VP::JOSE::JWKSet.from_h(jwks) : jwks
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "verified_request"
|
|
5
|
+
require_relative "../errors"
|
|
6
|
+
require_relative "../jose/jwk"
|
|
7
|
+
|
|
8
|
+
module OpenID4VP
|
|
9
|
+
module RequestVerification
|
|
10
|
+
# `decentralized_identifier` Client Identifier Prefix (OpenID4VP
|
|
11
|
+
# §5.9.3): the original Client Identifier is a DID. The request MUST be
|
|
12
|
+
# signed with a key from the DID Document's `verificationMethod`,
|
|
13
|
+
# identified by the JOSE `kid` header (`<did>#<fragment>`).
|
|
14
|
+
class DecentralizedIdentifier < Base
|
|
15
|
+
def verify!(client_id, request, parsed, dc_api: false) # rubocop:disable Lint/UnusedMethodArgument
|
|
16
|
+
raise OpenID4VP::InvalidRequest, "signed request required" if parsed.nil?
|
|
17
|
+
|
|
18
|
+
did = client_id.original
|
|
19
|
+
kid = parsed.kid
|
|
20
|
+
unless kid.is_a?(::String) && kid.start_with?("#{did}#")
|
|
21
|
+
raise OpenID4VP::InvalidRequest, "kid must be #{did}#<fragment>"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
resolver = config.did_resolver
|
|
25
|
+
raise OpenID4VP::InvalidRequest, "DID resolution unavailable" if resolver.nil?
|
|
26
|
+
|
|
27
|
+
doc = resolver.call(did)
|
|
28
|
+
signing_key = OpenID4VP::JOSE::JWK.new(public_key_jwk!(find_verification_method(doc, kid)))
|
|
29
|
+
verify_signature!(parsed, signing_key)
|
|
30
|
+
|
|
31
|
+
VerifiedRequest.new(
|
|
32
|
+
request: request, client_id: client_id, verifier_metadata: request.client_metadata,
|
|
33
|
+
signed: true, signing_key: signing_key, parsed: parsed, attestation: nil
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def find_verification_method(doc, kid)
|
|
40
|
+
fragment = kid.split("#", 2).last
|
|
41
|
+
methods = doc.is_a?(::Hash) ? doc["verificationMethod"] : nil
|
|
42
|
+
vm = methods.is_a?(::Array) ? methods.find { |m| [kid, "##{fragment}"].include?(m["id"]) } : nil
|
|
43
|
+
raise OpenID4VP::VerificationError, "verification method not found" if vm.nil?
|
|
44
|
+
|
|
45
|
+
vm
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def public_key_jwk!(verification_method)
|
|
49
|
+
jwk = verification_method["publicKeyJwk"]
|
|
50
|
+
raise OpenID4VP::VerificationError, "verification method has no publicKeyJwk" unless jwk.is_a?(::Hash)
|
|
51
|
+
|
|
52
|
+
jwk
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "verified_request"
|
|
5
|
+
require_relative "../errors"
|
|
6
|
+
require_relative "../jose/jwk_set"
|
|
7
|
+
require_relative "../metadata/verifier_metadata"
|
|
8
|
+
|
|
9
|
+
module OpenID4VP
|
|
10
|
+
module RequestVerification
|
|
11
|
+
# `openid_federation` Client Identifier Prefix (OpenID4VP §5.9.3): the
|
|
12
|
+
# original Client Identifier is an OpenID Federation Entity Identifier.
|
|
13
|
+
# Verifier metadata comes exclusively from the resolved Trust Chain; a
|
|
14
|
+
# `client_metadata` request parameter, if present, is ignored.
|
|
15
|
+
class OpenidFederation < Base
|
|
16
|
+
def verify!(client_id, request, parsed, dc_api: false) # rubocop:disable Lint/UnusedMethodArgument
|
|
17
|
+
raise OpenID4VP::InvalidRequest, "signed request required" if parsed.nil?
|
|
18
|
+
|
|
19
|
+
resolver = config.federation_resolver
|
|
20
|
+
raise OpenID4VP::InvalidRequest, "federation resolution unavailable" if resolver.nil?
|
|
21
|
+
|
|
22
|
+
res = resolver.call(client_id.original, request.trust_chain)
|
|
23
|
+
raise OpenID4VP::VerificationError, "federation entity not resolvable" unless res.is_a?(::Hash)
|
|
24
|
+
|
|
25
|
+
signing_key = verify_with_keys!(parsed, order_by_kid(jwks_from(res).keys, parsed.kid))
|
|
26
|
+
|
|
27
|
+
VerifiedRequest.new(
|
|
28
|
+
request: request, client_id: client_id, verifier_metadata: metadata_from(res),
|
|
29
|
+
signed: true, signing_key: signing_key, parsed: parsed, attestation: nil
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def fetch(res, key)
|
|
36
|
+
res[key] || res[key.to_s]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def jwks_from(res)
|
|
40
|
+
raw = fetch(res, :jwks)
|
|
41
|
+
raise OpenID4VP::InvalidRequest, "no signing keys in federation metadata" if raw.nil?
|
|
42
|
+
|
|
43
|
+
raw.is_a?(::Hash) ? OpenID4VP::JOSE::JWKSet.from_h(raw) : raw
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def metadata_from(res)
|
|
47
|
+
raw = fetch(res, :metadata) || {}
|
|
48
|
+
raw.is_a?(::Hash) ? OpenID4VP::Metadata::VerifierMetadata.new(raw) : raw
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "verified_request"
|
|
5
|
+
require_relative "../errors"
|
|
6
|
+
|
|
7
|
+
module OpenID4VP
|
|
8
|
+
module RequestVerification
|
|
9
|
+
# Pre-registered Client Identifier fallback (OpenID4VP §5.9.2): the
|
|
10
|
+
# Client Identifier is looked up in `config.pre_registered_clients`
|
|
11
|
+
# (registered out-of-band, e.g. via RFC 7591 dynamic registration).
|
|
12
|
+
class PreRegistered < Base
|
|
13
|
+
def verify!(client_id, request, parsed, dc_api: false)
|
|
14
|
+
entry = config.pre_registered_clients[client_id.to_s]
|
|
15
|
+
raise OpenID4VP::InvalidRequest, "unknown client" if entry.nil?
|
|
16
|
+
|
|
17
|
+
if request.client_metadata
|
|
18
|
+
raise OpenID4VP::InvalidClient, "client_metadata parameter must not be used with a pre-registered client_id"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
signing_key = verify_registered_signature!(parsed, entry)
|
|
22
|
+
check_redirect_uri!(request, entry, dc_api)
|
|
23
|
+
|
|
24
|
+
VerifiedRequest.new(
|
|
25
|
+
request: request, client_id: client_id, verifier_metadata: entry[:metadata],
|
|
26
|
+
signed: !parsed.nil?, signing_key: signing_key, parsed: parsed, attestation: nil
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def verify_registered_signature!(parsed, entry)
|
|
33
|
+
return nil if parsed.nil?
|
|
34
|
+
|
|
35
|
+
jwks = entry[:jwks]
|
|
36
|
+
raise OpenID4VP::InvalidRequest, "no registered signing keys" if jwks.nil? || jwks.keys.empty?
|
|
37
|
+
|
|
38
|
+
verify_with_keys!(parsed, order_by_kid(jwks.keys, parsed.kid))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def check_redirect_uri!(request, entry, dc_api)
|
|
42
|
+
redirect_uris = entry[:redirect_uris]
|
|
43
|
+
return if redirect_uris.nil? || redirect_uris.empty? || dc_api
|
|
44
|
+
|
|
45
|
+
return_uri = request.return_uri
|
|
46
|
+
return if return_uri.nil?
|
|
47
|
+
|
|
48
|
+
return if redirect_uris.include?(return_uri)
|
|
49
|
+
|
|
50
|
+
raise OpenID4VP::VerificationError, "return URI is not registered for this client"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "verified_request"
|
|
5
|
+
require_relative "../errors"
|
|
6
|
+
|
|
7
|
+
module OpenID4VP
|
|
8
|
+
module RequestVerification
|
|
9
|
+
# `redirect_uri` Client Identifier Prefix (OpenID4VP §5.9.3): the
|
|
10
|
+
# original Client Identifier is the Verifier's redirect/response URI.
|
|
11
|
+
# Requests using this prefix MUST NOT be signed; all Verifier metadata
|
|
12
|
+
# comes from the `client_metadata` request parameter.
|
|
13
|
+
class RedirectUri < Base
|
|
14
|
+
def verify!(client_id, request, parsed, dc_api: false) # rubocop:disable Lint/UnusedMethodArgument
|
|
15
|
+
raise OpenID4VP::InvalidRequest, "redirect_uri prefix requests must not be signed" if parsed
|
|
16
|
+
|
|
17
|
+
return_uri = request.return_uri
|
|
18
|
+
if return_uri && return_uri != client_id.original
|
|
19
|
+
raise OpenID4VP::InvalidRequest, "redirect_uri client_id must match the return URI"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
VerifiedRequest.new(
|
|
23
|
+
request: request, client_id: client_id, verifier_metadata: request.client_metadata,
|
|
24
|
+
signed: false, signing_key: nil, parsed: nil, attestation: nil
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "config"
|
|
4
|
+
require_relative "verified_request"
|
|
5
|
+
require_relative "redirect_uri"
|
|
6
|
+
require_relative "pre_registered"
|
|
7
|
+
require_relative "x509_san_dns"
|
|
8
|
+
require_relative "x509_hash"
|
|
9
|
+
require_relative "verifier_attestation"
|
|
10
|
+
require_relative "decentralized_identifier"
|
|
11
|
+
require_relative "openid_federation"
|
|
12
|
+
require_relative "../errors"
|
|
13
|
+
|
|
14
|
+
module OpenID4VP
|
|
15
|
+
module RequestVerification
|
|
16
|
+
# Dispatches wallet-side request verification (OpenID4VP §5.9) to the
|
|
17
|
+
# strategy matching `client_id`'s Client Identifier Prefix, after the
|
|
18
|
+
# checks common to every signed Request Object (§5.10.1): alg allow-list,
|
|
19
|
+
# client_id agreement between the Authorization Request and the Request
|
|
20
|
+
# Object, and wallet_nonce echo.
|
|
21
|
+
class Resolver
|
|
22
|
+
STRATEGIES = {
|
|
23
|
+
"openid_federation" => OpenidFederation,
|
|
24
|
+
"decentralized_identifier" => DecentralizedIdentifier,
|
|
25
|
+
"verifier_attestation" => OpenID4VP::RequestVerification::VerifierAttestation,
|
|
26
|
+
"x509_san_dns" => X509SanDns,
|
|
27
|
+
"x509_hash" => X509Hash
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
def initialize(config)
|
|
31
|
+
@config = config
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# client_id: OpenID4VP::ClientId. request: the AuthorizationRequest
|
|
35
|
+
# built from query/form parameters (used only when `parsed` is nil).
|
|
36
|
+
# parsed: OpenID4VP::RequestObject::Parsed, or nil for an unsigned
|
|
37
|
+
# request.
|
|
38
|
+
def verify(client_id:, request:, parsed: nil, dc_api: false, wallet_nonce: nil)
|
|
39
|
+
effective_request = parsed ? check_parsed!(client_id, parsed, wallet_nonce) : request
|
|
40
|
+
strategy_for(client_id).verify!(client_id, effective_request, parsed, dc_api: dc_api)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
attr_reader :config
|
|
46
|
+
|
|
47
|
+
def check_parsed!(client_id, parsed, wallet_nonce)
|
|
48
|
+
unless config.request_object_signing_algs.include?(parsed.alg)
|
|
49
|
+
raise OpenID4VP::InvalidRequest, "unsupported request object alg"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
raise OpenID4VP::InvalidRequest, "client_id mismatch" if parsed.request.client_id&.to_s != client_id.to_s
|
|
53
|
+
raise OpenID4VP::InvalidRequest, "wallet_nonce mismatch" if wallet_nonce && parsed.wallet_nonce != wallet_nonce
|
|
54
|
+
|
|
55
|
+
parsed.request
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def strategy_for(client_id)
|
|
59
|
+
prefix = client_id.prefix
|
|
60
|
+
raise OpenID4VP::InvalidRequest, "origin prefix not allowed in requests" if prefix == "origin"
|
|
61
|
+
return RedirectUri.new(config) if prefix == "redirect_uri"
|
|
62
|
+
return PreRegistered.new(config) if pre_registered_strategy?(client_id)
|
|
63
|
+
|
|
64
|
+
strategy_class = STRATEGIES.fetch(prefix) { raise OpenID4VP::InvalidRequest, "unsupported client_id prefix" }
|
|
65
|
+
strategy_class.new(config)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def pre_registered_strategy?(client_id)
|
|
69
|
+
return false unless client_id.pre_registered?
|
|
70
|
+
return true unless client_id.unknown_prefix?
|
|
71
|
+
|
|
72
|
+
config.allow_unknown_prefix_as_pre_registered
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenID4VP
|
|
4
|
+
module RequestVerification
|
|
5
|
+
# The result of `OpenID4VP::RequestVerification::Resolver#verify`: the
|
|
6
|
+
# effective AuthorizationRequest together with the Client Identifier
|
|
7
|
+
# Prefix verification outcome (OpenID4VP §5.9).
|
|
8
|
+
VerifiedRequest = Struct.new(
|
|
9
|
+
:request, :client_id, :verifier_metadata, :signed, :signing_key, :parsed, :attestation, keyword_init: true
|
|
10
|
+
)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "verified_request"
|
|
5
|
+
require_relative "../errors"
|
|
6
|
+
require_relative "../verifier_attestation"
|
|
7
|
+
|
|
8
|
+
module OpenID4VP
|
|
9
|
+
module RequestVerification
|
|
10
|
+
# `verifier_attestation` Client Identifier Prefix (OpenID4VP §5.9.3/§12):
|
|
11
|
+
# the Verifier authenticates with a Verifier Attestation JWT (carried in
|
|
12
|
+
# the `jwt` JOSE header) binding its `sub` claim to the Client Identifier
|
|
13
|
+
# and a confirmation key the request MUST be signed with.
|
|
14
|
+
#
|
|
15
|
+
# NOTE: this class name intentionally shadows the top-level
|
|
16
|
+
# `OpenID4VP::VerifierAttestation` module; always reference the module
|
|
17
|
+
# fully qualified (`OpenID4VP::VerifierAttestation`) from inside it.
|
|
18
|
+
class VerifierAttestation < Base
|
|
19
|
+
def verify!(client_id, request, parsed, dc_api: false)
|
|
20
|
+
raise OpenID4VP::InvalidRequest, "signed request required" if parsed.nil?
|
|
21
|
+
|
|
22
|
+
jwt = parsed.attestation_jwt
|
|
23
|
+
raise OpenID4VP::InvalidRequest, "verifier attestation required" if jwt.nil?
|
|
24
|
+
|
|
25
|
+
attestation = OpenID4VP::VerifierAttestation.verify(
|
|
26
|
+
jwt, trusted_issuers: config.attestation_issuers, now: config.current_time, leeway: config.leeway
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if attestation.sub != client_id.original
|
|
30
|
+
raise OpenID4VP::VerificationError, "verifier attestation sub does not match client_id"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
verify_signature!(parsed, attestation.cnf_jwk)
|
|
34
|
+
check_redirect_uris!(request, attestation, dc_api)
|
|
35
|
+
|
|
36
|
+
VerifiedRequest.new(
|
|
37
|
+
request: request, client_id: client_id, verifier_metadata: request.client_metadata,
|
|
38
|
+
signed: true, signing_key: attestation.cnf_jwk, parsed: parsed, attestation: attestation
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def check_redirect_uris!(request, attestation, dc_api)
|
|
45
|
+
redirect_uris = attestation.redirect_uris
|
|
46
|
+
return if redirect_uris.nil? || dc_api
|
|
47
|
+
|
|
48
|
+
return if redirect_uris.include?(request.return_uri)
|
|
49
|
+
|
|
50
|
+
raise OpenID4VP::VerificationError, "return URI does not match a verifier attestation redirect_uris entry"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "verified_request"
|
|
5
|
+
require_relative "../errors"
|
|
6
|
+
require_relative "../x509"
|
|
7
|
+
require_relative "../x509/chain_validator"
|
|
8
|
+
require_relative "../jose/jwk"
|
|
9
|
+
|
|
10
|
+
module OpenID4VP
|
|
11
|
+
module RequestVerification
|
|
12
|
+
# `x509_hash` Client Identifier Prefix (OpenID4VP §5.9.3): the original
|
|
13
|
+
# Client Identifier is base64url(SHA-256(DER)) of the leaf certificate
|
|
14
|
+
# carried in the request's `x5c` JOSE header.
|
|
15
|
+
class X509Hash < Base
|
|
16
|
+
def verify!(client_id, request, parsed, dc_api: false) # rubocop:disable Lint/UnusedMethodArgument
|
|
17
|
+
raise OpenID4VP::InvalidRequest, "signed request required" if parsed.nil?
|
|
18
|
+
|
|
19
|
+
certs = parsed.x5c_certs
|
|
20
|
+
raise OpenID4VP::InvalidRequest, "x5c required" if certs.empty?
|
|
21
|
+
|
|
22
|
+
leaf = OpenID4VP::X509::ChainValidator.new(
|
|
23
|
+
trust_anchors: config.x509_trust_anchors, time: config.current_time
|
|
24
|
+
).validate!(certs)
|
|
25
|
+
|
|
26
|
+
unless OpenID4VP::X509.leaf_hash_b64(leaf) == client_id.original
|
|
27
|
+
raise OpenID4VP::VerificationError, "certificate hash mismatch"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
signing_key = OpenID4VP::JOSE::JWK.from_openssl(leaf.public_key)
|
|
31
|
+
verify_signature!(parsed, signing_key)
|
|
32
|
+
|
|
33
|
+
VerifiedRequest.new(
|
|
34
|
+
request: request, client_id: client_id, verifier_metadata: request.client_metadata,
|
|
35
|
+
signed: true, signing_key: signing_key, parsed: parsed, attestation: nil
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|