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,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