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