@ixo/credentials-kit 0.0.0-stage → 0.1.1
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.
- package/README.md +469 -2
- package/dist/chunk-2SF56NEJ.js +1053 -0
- package/dist/chunk-2SF56NEJ.js.map +1 -0
- package/dist/chunk-46VRPQPD.js +176 -0
- package/dist/chunk-46VRPQPD.js.map +1 -0
- package/dist/chunk-AQ3IINHC.js +138 -0
- package/dist/chunk-AQ3IINHC.js.map +1 -0
- package/dist/chunk-FSKCDHZ7.js +613 -0
- package/dist/chunk-FSKCDHZ7.js.map +1 -0
- package/dist/chunk-I6GBYFIJ.js +89 -0
- package/dist/chunk-I6GBYFIJ.js.map +1 -0
- package/dist/chunk-JBUX6V2Q.js +210 -0
- package/dist/chunk-JBUX6V2Q.js.map +1 -0
- package/dist/chunk-Q3MZNMCK.js +183 -0
- package/dist/chunk-Q3MZNMCK.js.map +1 -0
- package/dist/chunk-RKKRO4ZK.js +133 -0
- package/dist/chunk-RKKRO4ZK.js.map +1 -0
- package/dist/chunk-STMMFNVW.js +391 -0
- package/dist/chunk-STMMFNVW.js.map +1 -0
- package/dist/chunk-VXKONZK4.js +189 -0
- package/dist/chunk-VXKONZK4.js.map +1 -0
- package/dist/chunk-WIEGZFUO.js +337 -0
- package/dist/chunk-WIEGZFUO.js.map +1 -0
- package/dist/chunk-WND2LZ4O.js +66 -0
- package/dist/chunk-WND2LZ4O.js.map +1 -0
- package/dist/chunk-XREZY53L.js +260 -0
- package/dist/chunk-XREZY53L.js.map +1 -0
- package/dist/chunk-ZZY4WGQ6.js +13 -0
- package/dist/chunk-ZZY4WGQ6.js.map +1 -0
- package/dist/did.d.ts +182 -0
- package/dist/did.js +6 -0
- package/dist/did.js.map +1 -0
- package/dist/display.d.ts +117 -0
- package/dist/display.js +5 -0
- package/dist/display.js.map +1 -0
- package/dist/errors-BKQmNpXX.d.ts +11 -0
- package/dist/holder.d.ts +85 -0
- package/dist/holder.js +8 -0
- package/dist/holder.js.map +1 -0
- package/dist/http-BhuJ_-RZ.d.ts +25 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/jose.d.ts +134 -0
- package/dist/jose.js +4 -0
- package/dist/jose.js.map +1 -0
- package/dist/oid4vci.d.ts +428 -0
- package/dist/oid4vci.js +11 -0
- package/dist/oid4vci.js.map +1 -0
- package/dist/oid4vp.d.ts +416 -0
- package/dist/oid4vp.js +11 -0
- package/dist/oid4vp.js.map +1 -0
- package/dist/profile.d.ts +218 -0
- package/dist/profile.js +4 -0
- package/dist/profile.js.map +1 -0
- package/dist/sdjwt.d.ts +133 -0
- package/dist/sdjwt.js +5 -0
- package/dist/sdjwt.js.map +1 -0
- package/dist/sender.d.ts +8 -0
- package/dist/sender.js +7 -0
- package/dist/sender.js.map +1 -0
- package/dist/status.d.ts +108 -0
- package/dist/status.js +7 -0
- package/dist/status.js.map +1 -0
- package/dist/util-DOTEZgTt.d.ts +3 -0
- package/dist/verify.d.ts +144 -0
- package/dist/verify.js +9 -0
- package/dist/verify.js.map +1 -0
- package/dist/webhook.d.ts +62 -0
- package/dist/webhook.js +7 -0
- package/dist/webhook.js.map +1 -0
- package/package.json +168 -4
|
@@ -0,0 +1,1053 @@
|
|
|
1
|
+
import { REQUEST_OBJECT_TYP, CLIENT_ID_PREFIX } from './chunk-I6GBYFIJ.js';
|
|
2
|
+
import { verifyPresentation } from './chunk-STMMFNVW.js';
|
|
3
|
+
import { parseSdJwt, SdJwtError, selectDisclosures, serializeSdJwt, sdHash, processDisclosures } from './chunk-XREZY53L.js';
|
|
4
|
+
import { didFromKid, ResolverUnavailable, findKey } from './chunk-WIEGZFUO.js';
|
|
5
|
+
import { isFetchableUrl, request } from './chunk-Q3MZNMCK.js';
|
|
6
|
+
import { CredentialsKitError, isRecord, toSeconds, clockMs, signJws, decodeJws, verifyEd25519, isEd25519JwsAlg, CLOCK_SKEW_S } from './chunk-VXKONZK4.js';
|
|
7
|
+
import { randomToken, utf8, base64urlDecode, concatBytes, fromUtf8, base64urlEncode } from './chunk-AQ3IINHC.js';
|
|
8
|
+
|
|
9
|
+
// src/internal/jwe.ts
|
|
10
|
+
var JWE_ALG = "ECDH-ES";
|
|
11
|
+
var JWE_ENCS = ["A256GCM", "A128GCM"];
|
|
12
|
+
var JWE_ENC = "A256GCM";
|
|
13
|
+
function isJweEnc(v) {
|
|
14
|
+
return v === "A256GCM" || v === "A128GCM";
|
|
15
|
+
}
|
|
16
|
+
function keyBitsOf(enc) {
|
|
17
|
+
return enc === "A128GCM" ? 128 : 256;
|
|
18
|
+
}
|
|
19
|
+
var MAX_JWE_LENGTH = 256 * 1024;
|
|
20
|
+
var JweError = class extends Error {
|
|
21
|
+
constructor(kind) {
|
|
22
|
+
super(`JWE ${kind}`);
|
|
23
|
+
this.kind = kind;
|
|
24
|
+
this.name = "JweError";
|
|
25
|
+
}
|
|
26
|
+
kind;
|
|
27
|
+
};
|
|
28
|
+
function uint32be(n) {
|
|
29
|
+
const out = new Uint8Array(4);
|
|
30
|
+
new DataView(out.buffer).setUint32(0, n, false);
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
function lengthPrefixed(bytes) {
|
|
34
|
+
return concatBytes(uint32be(bytes.length), bytes);
|
|
35
|
+
}
|
|
36
|
+
async function concatKdf(z, keyBits, algorithmId, apu = new Uint8Array(0), apv = new Uint8Array(0)) {
|
|
37
|
+
const otherInfo = concatBytes(
|
|
38
|
+
lengthPrefixed(utf8(algorithmId)),
|
|
39
|
+
lengthPrefixed(apu),
|
|
40
|
+
lengthPrefixed(apv),
|
|
41
|
+
uint32be(keyBits)
|
|
42
|
+
);
|
|
43
|
+
const rounds = Math.ceil(keyBits / 256);
|
|
44
|
+
const parts = [];
|
|
45
|
+
for (let i = 1; i <= rounds; i++) {
|
|
46
|
+
const input = concatBytes(uint32be(i), z, otherInfo);
|
|
47
|
+
parts.push(new Uint8Array(await crypto.subtle.digest("SHA-256", input)));
|
|
48
|
+
}
|
|
49
|
+
return concatBytes(...parts).slice(0, keyBits / 8);
|
|
50
|
+
}
|
|
51
|
+
function isP256PublicJwk(v) {
|
|
52
|
+
return isRecord(v) && v.kty === "EC" && v.crv === "P-256" && typeof v.x === "string" && typeof v.y === "string" && !("d" in v);
|
|
53
|
+
}
|
|
54
|
+
function publicPart(jwk) {
|
|
55
|
+
return { kty: "EC", crv: "P-256", x: jwk.x, y: jwk.y };
|
|
56
|
+
}
|
|
57
|
+
async function importPublic(jwk) {
|
|
58
|
+
try {
|
|
59
|
+
return await crypto.subtle.importKey(
|
|
60
|
+
"jwk",
|
|
61
|
+
publicPart(jwk),
|
|
62
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
63
|
+
true,
|
|
64
|
+
[]
|
|
65
|
+
);
|
|
66
|
+
} catch {
|
|
67
|
+
throw new JweError("bad_key");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function importPrivate(jwk) {
|
|
71
|
+
try {
|
|
72
|
+
return await crypto.subtle.importKey(
|
|
73
|
+
"jwk",
|
|
74
|
+
{ ...publicPart(jwk), d: jwk.d },
|
|
75
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
76
|
+
false,
|
|
77
|
+
["deriveBits"]
|
|
78
|
+
);
|
|
79
|
+
} catch {
|
|
80
|
+
throw new JweError("bad_key");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function sharedSecret(priv, pub) {
|
|
84
|
+
return new Uint8Array(
|
|
85
|
+
await crypto.subtle.deriveBits({ name: "ECDH", public: pub }, priv, 256)
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
async function generateP256KeyPair(kid) {
|
|
89
|
+
const pair = await crypto.subtle.generateKey(
|
|
90
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
91
|
+
true,
|
|
92
|
+
["deriveBits"]
|
|
93
|
+
);
|
|
94
|
+
const priv = await crypto.subtle.exportKey("jwk", pair.privateKey);
|
|
95
|
+
if (!priv.x || !priv.y || !priv.d) throw new JweError("bad_key");
|
|
96
|
+
const publicJwk = {
|
|
97
|
+
kty: "EC",
|
|
98
|
+
crv: "P-256",
|
|
99
|
+
x: priv.x,
|
|
100
|
+
y: priv.y,
|
|
101
|
+
kid,
|
|
102
|
+
use: "enc",
|
|
103
|
+
alg: "ECDH-ES"
|
|
104
|
+
};
|
|
105
|
+
return { publicJwk, privateJwk: { ...publicJwk, d: priv.d } };
|
|
106
|
+
}
|
|
107
|
+
async function encryptJwe(payload, recipient, enc = JWE_ENC) {
|
|
108
|
+
if (!isJweEnc(enc)) throw new JweError("unsupported");
|
|
109
|
+
const recipientKey = await importPublic(recipient);
|
|
110
|
+
const eph = await crypto.subtle.generateKey(
|
|
111
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
112
|
+
true,
|
|
113
|
+
["deriveBits"]
|
|
114
|
+
);
|
|
115
|
+
const epk = await crypto.subtle.exportKey("jwk", eph.publicKey);
|
|
116
|
+
const header = {
|
|
117
|
+
alg: JWE_ALG,
|
|
118
|
+
enc,
|
|
119
|
+
...recipient.kid !== void 0 ? { kid: recipient.kid } : {},
|
|
120
|
+
epk: { kty: "EC", crv: "P-256", x: epk.x, y: epk.y }
|
|
121
|
+
};
|
|
122
|
+
const z = await sharedSecret(eph.privateKey, recipientKey);
|
|
123
|
+
const cek = await concatKdf(z, keyBitsOf(enc), enc);
|
|
124
|
+
const protectedHeader = base64urlEncode(utf8(JSON.stringify(header)));
|
|
125
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
126
|
+
const key = await crypto.subtle.importKey("raw", cek, "AES-GCM", false, [
|
|
127
|
+
"encrypt"
|
|
128
|
+
]);
|
|
129
|
+
const sealed = new Uint8Array(
|
|
130
|
+
await crypto.subtle.encrypt(
|
|
131
|
+
{
|
|
132
|
+
name: "AES-GCM",
|
|
133
|
+
iv,
|
|
134
|
+
additionalData: utf8(protectedHeader),
|
|
135
|
+
tagLength: 128
|
|
136
|
+
},
|
|
137
|
+
key,
|
|
138
|
+
utf8(JSON.stringify(payload))
|
|
139
|
+
)
|
|
140
|
+
);
|
|
141
|
+
const ciphertext = sealed.slice(0, sealed.length - 16);
|
|
142
|
+
const tag = sealed.slice(sealed.length - 16);
|
|
143
|
+
return [
|
|
144
|
+
protectedHeader,
|
|
145
|
+
"",
|
|
146
|
+
base64urlEncode(iv),
|
|
147
|
+
base64urlEncode(ciphertext),
|
|
148
|
+
base64urlEncode(tag)
|
|
149
|
+
].join(".");
|
|
150
|
+
}
|
|
151
|
+
function decodeJweHeader(compact) {
|
|
152
|
+
const first = compact.split(".")[0] ?? "";
|
|
153
|
+
try {
|
|
154
|
+
const header = JSON.parse(fromUtf8(base64urlDecode(first)));
|
|
155
|
+
if (!isRecord(header)) throw new JweError("malformed");
|
|
156
|
+
return header;
|
|
157
|
+
} catch {
|
|
158
|
+
throw new JweError("malformed");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async function decryptJwe(compact, recipient, accept = JWE_ENCS) {
|
|
162
|
+
if (typeof compact !== "string" || compact.length > MAX_JWE_LENGTH) {
|
|
163
|
+
throw new JweError("malformed");
|
|
164
|
+
}
|
|
165
|
+
const parts = compact.split(".");
|
|
166
|
+
if (parts.length !== 5) throw new JweError("malformed");
|
|
167
|
+
const [h, encryptedKey, ivB64, ctB64, tagB64] = parts;
|
|
168
|
+
if (encryptedKey !== "") throw new JweError("unsupported");
|
|
169
|
+
const header = decodeJweHeader(compact);
|
|
170
|
+
const enc = header.enc;
|
|
171
|
+
if (header.alg !== JWE_ALG || !isJweEnc(enc) || !accept.includes(enc)) {
|
|
172
|
+
throw new JweError("unsupported");
|
|
173
|
+
}
|
|
174
|
+
if (header.zip !== void 0 || header.crit !== void 0) {
|
|
175
|
+
throw new JweError("unsupported");
|
|
176
|
+
}
|
|
177
|
+
if (recipient.kid !== void 0 && header.kid !== void 0 && header.kid !== recipient.kid) {
|
|
178
|
+
throw new JweError("bad_key");
|
|
179
|
+
}
|
|
180
|
+
if (!isP256PublicJwk(header.epk)) throw new JweError("malformed");
|
|
181
|
+
let iv;
|
|
182
|
+
let ciphertext;
|
|
183
|
+
let tag;
|
|
184
|
+
let apu;
|
|
185
|
+
let apv;
|
|
186
|
+
try {
|
|
187
|
+
iv = base64urlDecode(ivB64);
|
|
188
|
+
ciphertext = base64urlDecode(ctB64);
|
|
189
|
+
tag = base64urlDecode(tagB64);
|
|
190
|
+
apu = typeof header.apu === "string" ? base64urlDecode(header.apu) : new Uint8Array(0);
|
|
191
|
+
apv = typeof header.apv === "string" ? base64urlDecode(header.apv) : new Uint8Array(0);
|
|
192
|
+
} catch {
|
|
193
|
+
throw new JweError("malformed");
|
|
194
|
+
}
|
|
195
|
+
if (iv.length !== 12 || tag.length !== 16) throw new JweError("malformed");
|
|
196
|
+
const epk = await importPublic(header.epk);
|
|
197
|
+
const priv = await importPrivate(recipient);
|
|
198
|
+
const z = await sharedSecret(priv, epk);
|
|
199
|
+
const cek = await concatKdf(z, keyBitsOf(enc), enc, apu, apv);
|
|
200
|
+
const key = await crypto.subtle.importKey("raw", cek, "AES-GCM", false, [
|
|
201
|
+
"decrypt"
|
|
202
|
+
]);
|
|
203
|
+
let plain;
|
|
204
|
+
try {
|
|
205
|
+
plain = new Uint8Array(
|
|
206
|
+
await crypto.subtle.decrypt(
|
|
207
|
+
{ name: "AES-GCM", iv, additionalData: utf8(h), tagLength: 128 },
|
|
208
|
+
key,
|
|
209
|
+
concatBytes(ciphertext, tag)
|
|
210
|
+
)
|
|
211
|
+
);
|
|
212
|
+
} catch {
|
|
213
|
+
throw new JweError("decrypt_failed");
|
|
214
|
+
}
|
|
215
|
+
let payload;
|
|
216
|
+
try {
|
|
217
|
+
payload = JSON.parse(fromUtf8(plain));
|
|
218
|
+
} catch {
|
|
219
|
+
throw new JweError("malformed");
|
|
220
|
+
}
|
|
221
|
+
if (!isRecord(payload)) throw new JweError("malformed");
|
|
222
|
+
return payload;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/oid4vp.ts
|
|
226
|
+
var SELF_ISSUED_AUDIENCE = "https://self-issued.me/v2";
|
|
227
|
+
var RESPONSE_ENCRYPTION_ENC = "A256GCM";
|
|
228
|
+
var RESPONSE_ENCRYPTION_ENCS = JWE_ENCS;
|
|
229
|
+
var DEFAULT_RESPONSE_ENCRYPTION_ENC = "A128GCM";
|
|
230
|
+
var MAX_CREDENTIAL_QUERIES = 16;
|
|
231
|
+
var MAX_CLAIMS_PER_QUERY = 64;
|
|
232
|
+
var MAX_REQUEST_OBJECT_LENGTH = 64 * 1024;
|
|
233
|
+
var MAX_RESPONSE_BODY_LENGTH = 256 * 1024;
|
|
234
|
+
var MAX_REQUEST_LIFETIME_SECONDS = 24 * 3600;
|
|
235
|
+
var OID4VP_MESSAGES = {
|
|
236
|
+
malformed_request: "The presentation request is not well formed.",
|
|
237
|
+
wrong_type: "The request object is not typed oauth-authz-req+jwt.",
|
|
238
|
+
unsupported_client_id: "The request does not use a decentralized_identifier client_id.",
|
|
239
|
+
unknown_verifier: "The verifier's DID could not be found.",
|
|
240
|
+
unknown_key: "The request is not signed with a key of the verifier's DID.",
|
|
241
|
+
bad_signature: "The request signature does not verify.",
|
|
242
|
+
expired: "The request has expired.",
|
|
243
|
+
unsupported_request: "The request uses a feature this profile does not support.",
|
|
244
|
+
unsupported_response_mode: "The response mode is not direct_post or direct_post.jwt.",
|
|
245
|
+
insecure_response_uri: "The response URI is not an https address on the public internet.",
|
|
246
|
+
unsupported_query: "The DCQL query uses a feature this profile does not support.",
|
|
247
|
+
unsupported_encryption: "The response encryption parameters are not supported.",
|
|
248
|
+
malformed_response: "The presentation response is not well formed.",
|
|
249
|
+
decryption_failed: "The presentation response could not be decrypted.",
|
|
250
|
+
wallet_error: "The wallet answered with an error.",
|
|
251
|
+
state_mismatch: "The response state does not match the request."
|
|
252
|
+
};
|
|
253
|
+
var Oid4vpError = class extends CredentialsKitError {
|
|
254
|
+
/** For `wallet_error`: the OAuth error code the wallet sent (e.g. `access_denied`). */
|
|
255
|
+
walletError;
|
|
256
|
+
/**
|
|
257
|
+
* For `unsupported_request`: the feature refused (`credential_sets`, `claim_sets`,
|
|
258
|
+
* `trusted_authorities`, `nested_path`, `format`, `registered_claim`, `transaction_data`,
|
|
259
|
+
* `scope`, `response_type`, `request_uri_method`, `request`, `too_many_queries`,
|
|
260
|
+
* `too_many_claims`).
|
|
261
|
+
*/
|
|
262
|
+
feature;
|
|
263
|
+
/**
|
|
264
|
+
* Set only when the request object's signature verified and its response URI is usable: the
|
|
265
|
+
* wallet may tell the verifier with `sendAuthorizationError({ target: error.errorTarget, … })`
|
|
266
|
+
* (OpenID4VP §8.5). Never set for a request that did not verify.
|
|
267
|
+
*/
|
|
268
|
+
errorTarget;
|
|
269
|
+
constructor(code, walletError, extra = {}) {
|
|
270
|
+
super(code, OID4VP_MESSAGES[code]);
|
|
271
|
+
if (walletError !== void 0) this.walletError = walletError;
|
|
272
|
+
if (extra.feature !== void 0) this.feature = extra.feature;
|
|
273
|
+
if (extra.errorTarget !== void 0) this.errorTarget = extra.errorTarget;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
function unsupported(feature) {
|
|
277
|
+
throw new Oid4vpError("unsupported_request", void 0, { feature });
|
|
278
|
+
}
|
|
279
|
+
function malformed() {
|
|
280
|
+
throw new Oid4vpError("malformed_request");
|
|
281
|
+
}
|
|
282
|
+
var QUERY_ID = /^[A-Za-z0-9_-]+$/;
|
|
283
|
+
var STRUCTURAL_NAMES = /* @__PURE__ */ new Set(["_sd", "_sd_alg", "..."]);
|
|
284
|
+
var NOT_REQUESTABLE = /* @__PURE__ */ new Set([
|
|
285
|
+
"nbf",
|
|
286
|
+
"exp",
|
|
287
|
+
"vct",
|
|
288
|
+
"vct#integrity",
|
|
289
|
+
"aka_vcts",
|
|
290
|
+
"cnf",
|
|
291
|
+
"status"
|
|
292
|
+
]);
|
|
293
|
+
function isPathElement(x) {
|
|
294
|
+
return typeof x === "string" || x === null || typeof x === "number" && Number.isSafeInteger(x) && x >= 0;
|
|
295
|
+
}
|
|
296
|
+
function validateClaim(v) {
|
|
297
|
+
if (!isRecord(v)) malformed();
|
|
298
|
+
if (v.id !== void 0 && (typeof v.id !== "string" || !QUERY_ID.test(v.id)))
|
|
299
|
+
malformed();
|
|
300
|
+
const path = v.path;
|
|
301
|
+
if (!Array.isArray(path) || path.length === 0 || !path.every(isPathElement))
|
|
302
|
+
malformed();
|
|
303
|
+
if (path.length !== 1 || typeof path[0] !== "string")
|
|
304
|
+
unsupported("nested_path");
|
|
305
|
+
const name = path[0];
|
|
306
|
+
if (name.length === 0 || STRUCTURAL_NAMES.has(name)) malformed();
|
|
307
|
+
if (NOT_REQUESTABLE.has(name)) unsupported("registered_claim");
|
|
308
|
+
let values;
|
|
309
|
+
if (v.values !== void 0) {
|
|
310
|
+
if (!Array.isArray(v.values) || v.values.length === 0) malformed();
|
|
311
|
+
for (const x of v.values) {
|
|
312
|
+
const ok = typeof x === "string" || typeof x === "boolean" || typeof x === "number" && Number.isInteger(x);
|
|
313
|
+
if (!ok) malformed();
|
|
314
|
+
}
|
|
315
|
+
values = v.values;
|
|
316
|
+
}
|
|
317
|
+
if (name === "iss" && values && !values.every((x) => typeof x === "string"))
|
|
318
|
+
malformed();
|
|
319
|
+
return {
|
|
320
|
+
...typeof v.id === "string" ? { id: v.id } : {},
|
|
321
|
+
path: [name],
|
|
322
|
+
...values ? { values } : {}
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function validateCredentialQuery(v) {
|
|
326
|
+
if (!isRecord(v)) malformed();
|
|
327
|
+
if (typeof v.id !== "string" || !QUERY_ID.test(v.id)) malformed();
|
|
328
|
+
if (typeof v.format !== "string") malformed();
|
|
329
|
+
if (v.multiple !== void 0 && typeof v.multiple !== "boolean") malformed();
|
|
330
|
+
if (v.require_cryptographic_holder_binding !== void 0 && typeof v.require_cryptographic_holder_binding !== "boolean")
|
|
331
|
+
malformed();
|
|
332
|
+
const meta = v.meta;
|
|
333
|
+
if (v.format !== "dc+sd-jwt") unsupported("format");
|
|
334
|
+
if (!isRecord(meta) || !Array.isArray(meta.vct_values) || meta.vct_values.length === 0 || !meta.vct_values.every((x) => typeof x === "string" && x.length > 0)) {
|
|
335
|
+
malformed();
|
|
336
|
+
}
|
|
337
|
+
if (v.claim_sets !== void 0) unsupported("claim_sets");
|
|
338
|
+
if (v.trusted_authorities !== void 0) unsupported("trusted_authorities");
|
|
339
|
+
let claims;
|
|
340
|
+
if (v.claims !== void 0) {
|
|
341
|
+
if (!Array.isArray(v.claims) || v.claims.length === 0) malformed();
|
|
342
|
+
if (v.claims.length > MAX_CLAIMS_PER_QUERY) unsupported("too_many_claims");
|
|
343
|
+
const seen = /* @__PURE__ */ new Set();
|
|
344
|
+
claims = [];
|
|
345
|
+
for (const raw of v.claims) {
|
|
346
|
+
const claim = validateClaim(raw);
|
|
347
|
+
if (seen.has(claim.path[0])) continue;
|
|
348
|
+
seen.add(claim.path[0]);
|
|
349
|
+
claims.push(claim);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
id: v.id,
|
|
354
|
+
format: "dc+sd-jwt",
|
|
355
|
+
meta: { vct_values: [...meta.vct_values] },
|
|
356
|
+
...claims ? { claims } : {},
|
|
357
|
+
...typeof v.require_cryptographic_holder_binding === "boolean" ? {
|
|
358
|
+
require_cryptographic_holder_binding: v.require_cryptographic_holder_binding
|
|
359
|
+
} : {},
|
|
360
|
+
...typeof v.multiple === "boolean" ? { multiple: v.multiple } : {}
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
function validateDcql(dcql) {
|
|
364
|
+
if (!isRecord(dcql)) malformed();
|
|
365
|
+
const creds = dcql.credentials;
|
|
366
|
+
if (!Array.isArray(creds) || creds.length === 0) malformed();
|
|
367
|
+
if (creds.length > MAX_CREDENTIAL_QUERIES) unsupported("too_many_queries");
|
|
368
|
+
const credentials = creds.map(validateCredentialQuery);
|
|
369
|
+
const ids = credentials.map((c) => c.id);
|
|
370
|
+
if (new Set(ids).size !== ids.length) malformed();
|
|
371
|
+
if (dcql.credential_sets !== void 0) unsupported("credential_sets");
|
|
372
|
+
return { credentials };
|
|
373
|
+
}
|
|
374
|
+
function issuerFilter(query) {
|
|
375
|
+
const iss = query.claims?.find((c) => c.path[0] === "iss");
|
|
376
|
+
return iss?.values;
|
|
377
|
+
}
|
|
378
|
+
function requestedClaims(query) {
|
|
379
|
+
return (query.claims ?? []).filter((c) => c.path[0] !== "iss");
|
|
380
|
+
}
|
|
381
|
+
function summarizeDcql(dcql) {
|
|
382
|
+
return validateDcql(dcql).credentials.map((q) => {
|
|
383
|
+
const issuers = issuerFilter(q);
|
|
384
|
+
return {
|
|
385
|
+
id: q.id,
|
|
386
|
+
vctValues: [...q.meta.vct_values],
|
|
387
|
+
claims: requestedClaims(q).map((c) => c.path[0]),
|
|
388
|
+
...issuers ? { issuers: [...issuers] } : {}
|
|
389
|
+
};
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
function valueMatches(value, values) {
|
|
393
|
+
return values === void 0 || values.some((v) => v === value);
|
|
394
|
+
}
|
|
395
|
+
function matchCredential(query, parsed) {
|
|
396
|
+
let q;
|
|
397
|
+
try {
|
|
398
|
+
q = validateCredentialQuery(query);
|
|
399
|
+
} catch {
|
|
400
|
+
return { ok: false, reason: "unsupported_query" };
|
|
401
|
+
}
|
|
402
|
+
const typ = parsed.header.typ;
|
|
403
|
+
if (typ !== "dc+sd-jwt" && typ !== "vc+sd-jwt")
|
|
404
|
+
return { ok: false, reason: "wrong_format" };
|
|
405
|
+
const { payload } = parsed;
|
|
406
|
+
if (typeof payload.vct !== "string" || !q.meta.vct_values.includes(payload.vct)) {
|
|
407
|
+
return { ok: false, reason: "wrong_type" };
|
|
408
|
+
}
|
|
409
|
+
const issuers = issuerFilter(q);
|
|
410
|
+
if (issuers && (typeof payload.iss !== "string" || !issuers.includes(payload.iss))) {
|
|
411
|
+
return { ok: false, reason: "untrusted_issuer" };
|
|
412
|
+
}
|
|
413
|
+
const cnf = payload.cnf;
|
|
414
|
+
if (!isRecord(cnf) || !isRecord(cnf.jwk))
|
|
415
|
+
return { ok: false, reason: "no_holder_binding" };
|
|
416
|
+
const topDigests = new Set(
|
|
417
|
+
Array.isArray(payload._sd) ? payload._sd : []
|
|
418
|
+
);
|
|
419
|
+
const byName = new Map(
|
|
420
|
+
parsed.disclosures.filter((d) => d.name !== null && topDigests.has(d.digest)).map((d) => [d.name, d])
|
|
421
|
+
);
|
|
422
|
+
const disclose = [];
|
|
423
|
+
for (const claim of requestedClaims(q)) {
|
|
424
|
+
const name = claim.path[0];
|
|
425
|
+
const sd = byName.get(name);
|
|
426
|
+
if (sd) {
|
|
427
|
+
if (!valueMatches(sd.value, claim.values))
|
|
428
|
+
return { ok: false, reason: "value_mismatch" };
|
|
429
|
+
disclose.push(name);
|
|
430
|
+
} else if (Object.hasOwn(payload, name)) {
|
|
431
|
+
if (!valueMatches(payload[name], claim.values)) {
|
|
432
|
+
return { ok: false, reason: "value_mismatch" };
|
|
433
|
+
}
|
|
434
|
+
} else {
|
|
435
|
+
return { ok: false, reason: "missing_claim" };
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return { ok: true, disclose };
|
|
439
|
+
}
|
|
440
|
+
function generateResponseKeyPair() {
|
|
441
|
+
return generateP256KeyPair(`enc-${randomToken(9)}`);
|
|
442
|
+
}
|
|
443
|
+
var URL_SAFE = /^[A-Za-z0-9._~-]+$/;
|
|
444
|
+
async function createAuthorizationRequest(input) {
|
|
445
|
+
if (didFromKid(input.signer.kid) !== input.clientDid || !input.signer.kid.includes("#")) {
|
|
446
|
+
throw new Oid4vpError("unknown_key");
|
|
447
|
+
}
|
|
448
|
+
if (!isFetchableUrl(input.responseUri, {
|
|
449
|
+
allowHttp: input.allowHttp ?? false,
|
|
450
|
+
allowLocal: input.allowLocal ?? false
|
|
451
|
+
})) {
|
|
452
|
+
throw new Oid4vpError("insecure_response_uri");
|
|
453
|
+
}
|
|
454
|
+
if (!URL_SAFE.test(input.nonce) || input.nonce.length < 16) malformed();
|
|
455
|
+
if (typeof input.state !== "string" || !URL_SAFE.test(input.state))
|
|
456
|
+
malformed();
|
|
457
|
+
const dcql = validateDcql(input.dcql);
|
|
458
|
+
const iat = toSeconds(clockMs(input.now));
|
|
459
|
+
if (!Number.isSafeInteger(input.exp) || input.exp <= iat) malformed();
|
|
460
|
+
const enc = input.responseEncryptionJwk;
|
|
461
|
+
if (enc !== void 0 && (!isP256PublicJwk(enc) || typeof enc.kid !== "string")) {
|
|
462
|
+
throw new Oid4vpError("unsupported_encryption");
|
|
463
|
+
}
|
|
464
|
+
const clientMetadata = {
|
|
465
|
+
vp_formats_supported: {
|
|
466
|
+
"dc+sd-jwt": {
|
|
467
|
+
// Fully-specified name first (RFC 9864), then the JWS name most wallets still match.
|
|
468
|
+
"sd-jwt_alg_values": ["Ed25519", "EdDSA"],
|
|
469
|
+
"kb-jwt_alg_values": ["Ed25519", "EdDSA"]
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
...enc ? {
|
|
473
|
+
jwks: { keys: [{ ...enc, use: "enc", alg: "ECDH-ES" }] },
|
|
474
|
+
encrypted_response_enc_values_supported: [
|
|
475
|
+
...RESPONSE_ENCRYPTION_ENCS
|
|
476
|
+
]
|
|
477
|
+
} : {}
|
|
478
|
+
};
|
|
479
|
+
return signJws(
|
|
480
|
+
{ alg: "EdDSA", typ: REQUEST_OBJECT_TYP, kid: input.signer.kid },
|
|
481
|
+
{
|
|
482
|
+
aud: SELF_ISSUED_AUDIENCE,
|
|
483
|
+
client_id: `${CLIENT_ID_PREFIX}${input.clientDid}`,
|
|
484
|
+
response_type: "vp_token",
|
|
485
|
+
response_mode: enc ? "direct_post.jwt" : "direct_post",
|
|
486
|
+
response_uri: input.responseUri,
|
|
487
|
+
nonce: input.nonce,
|
|
488
|
+
state: input.state,
|
|
489
|
+
dcql_query: dcql,
|
|
490
|
+
client_metadata: clientMetadata,
|
|
491
|
+
iat,
|
|
492
|
+
exp: input.exp
|
|
493
|
+
},
|
|
494
|
+
input.signer
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
var MAX_WALLET_LINK_LENGTH = 4096;
|
|
498
|
+
function buildWalletLink(input) {
|
|
499
|
+
const scheme = input.scheme ?? "openid4vp://";
|
|
500
|
+
if (typeof input.clientId !== "string" || input.clientId.length === 0)
|
|
501
|
+
throw new TypeError("clientId is required");
|
|
502
|
+
let requestUri;
|
|
503
|
+
try {
|
|
504
|
+
requestUri = new URL(input.requestUri);
|
|
505
|
+
} catch {
|
|
506
|
+
throw new TypeError("requestUri must be an absolute URL");
|
|
507
|
+
}
|
|
508
|
+
if (requestUri.protocol !== "https:" && requestUri.protocol !== "http:")
|
|
509
|
+
throw new TypeError("requestUri must be an http(s) URL");
|
|
510
|
+
let base;
|
|
511
|
+
if (/^[a-z][a-z0-9+.-]*:(?:\/\/)?$/i.test(scheme)) {
|
|
512
|
+
const name = scheme.slice(0, scheme.indexOf(":")).toLowerCase();
|
|
513
|
+
if (["javascript", "data", "vbscript", "file", "blob", "http"].includes(name))
|
|
514
|
+
throw new TypeError("scheme must be a custom scheme or an https URL");
|
|
515
|
+
base = scheme;
|
|
516
|
+
} else {
|
|
517
|
+
let u;
|
|
518
|
+
try {
|
|
519
|
+
u = new URL(scheme);
|
|
520
|
+
} catch {
|
|
521
|
+
throw new TypeError("scheme must be a custom scheme or an https URL");
|
|
522
|
+
}
|
|
523
|
+
if (u.protocol !== "https:" || u.search || u.hash)
|
|
524
|
+
throw new TypeError("scheme must be a custom scheme or an https URL");
|
|
525
|
+
base = u.href;
|
|
526
|
+
}
|
|
527
|
+
const params = new URLSearchParams({
|
|
528
|
+
client_id: input.clientId,
|
|
529
|
+
request_uri: input.requestUri
|
|
530
|
+
});
|
|
531
|
+
return `${base}?${params.toString()}`;
|
|
532
|
+
}
|
|
533
|
+
function parseWalletLink(link, opts = {}) {
|
|
534
|
+
if (typeof link !== "string" || link.length > MAX_WALLET_LINK_LENGTH)
|
|
535
|
+
malformed();
|
|
536
|
+
const q = link.indexOf("?");
|
|
537
|
+
if (q < 0) malformed();
|
|
538
|
+
const query = link.slice(q + 1).split("#")[0] ?? "";
|
|
539
|
+
const params = new URLSearchParams(query);
|
|
540
|
+
if (params.has("request")) unsupported("request");
|
|
541
|
+
const method = params.getAll("request_uri_method");
|
|
542
|
+
if (method.length > 1 || method.length === 1 && method[0] !== "get") {
|
|
543
|
+
if (method[0] === "post") unsupported("request_uri_method");
|
|
544
|
+
malformed();
|
|
545
|
+
}
|
|
546
|
+
const clientIds = params.getAll("client_id");
|
|
547
|
+
const requestUris = params.getAll("request_uri");
|
|
548
|
+
if (clientIds.length !== 1 || requestUris.length !== 1) malformed();
|
|
549
|
+
const clientId = clientIds[0];
|
|
550
|
+
const requestUri = requestUris[0];
|
|
551
|
+
if (clientId.length === 0) malformed();
|
|
552
|
+
if (!isFetchableUrl(requestUri, {
|
|
553
|
+
allowHttp: opts.allowHttp ?? false,
|
|
554
|
+
allowLocal: opts.allowLocal ?? false
|
|
555
|
+
}))
|
|
556
|
+
malformed();
|
|
557
|
+
return { clientId, requestUri };
|
|
558
|
+
}
|
|
559
|
+
function checkHeader(header, payload, expectedClientId) {
|
|
560
|
+
if (header.typ !== REQUEST_OBJECT_TYP) throw new Oid4vpError("wrong_type");
|
|
561
|
+
if (!isEd25519JwsAlg(header.alg)) throw new Oid4vpError("bad_signature");
|
|
562
|
+
const clientId = payload.client_id;
|
|
563
|
+
if (typeof clientId !== "string" || !clientId.startsWith(CLIENT_ID_PREFIX)) {
|
|
564
|
+
throw new Oid4vpError("unsupported_client_id");
|
|
565
|
+
}
|
|
566
|
+
if (expectedClientId !== void 0 && clientId !== expectedClientId)
|
|
567
|
+
malformed();
|
|
568
|
+
const clientDid = clientId.slice(CLIENT_ID_PREFIX.length);
|
|
569
|
+
if (!/^did:[a-z0-9]+:\S+$/.test(clientDid) || clientDid.includes("#")) {
|
|
570
|
+
throw new Oid4vpError("unsupported_client_id");
|
|
571
|
+
}
|
|
572
|
+
const kid = typeof header.kid === "string" ? header.kid : "";
|
|
573
|
+
const absoluteKid = kid.startsWith("#") ? `${clientDid}${kid}` : kid;
|
|
574
|
+
if (didFromKid(absoluteKid) !== clientDid || !absoluteKid.includes("#")) {
|
|
575
|
+
throw new Oid4vpError("unknown_key");
|
|
576
|
+
}
|
|
577
|
+
return { clientId, clientDid, kid: absoluteKid };
|
|
578
|
+
}
|
|
579
|
+
function checkTimes(payload, opts) {
|
|
580
|
+
const { exp, iat, aud } = payload;
|
|
581
|
+
if (exp !== void 0 && (typeof exp !== "number" || !Number.isFinite(exp)))
|
|
582
|
+
malformed();
|
|
583
|
+
if (iat !== void 0 && (typeof iat !== "number" || !Number.isFinite(iat)))
|
|
584
|
+
malformed();
|
|
585
|
+
if (exp === void 0 && opts.requireExpiry) malformed();
|
|
586
|
+
if (aud !== void 0 && aud !== SELF_ISSUED_AUDIENCE) malformed();
|
|
587
|
+
if (opts.nowS === null) return;
|
|
588
|
+
const nowS = opts.nowS;
|
|
589
|
+
if (typeof exp === "number" && nowS > exp + CLOCK_SKEW_S)
|
|
590
|
+
throw new Oid4vpError("expired");
|
|
591
|
+
if (typeof iat === "number" && iat > nowS + CLOCK_SKEW_S) malformed();
|
|
592
|
+
if (typeof exp === "number") {
|
|
593
|
+
const from = typeof iat === "number" ? iat : nowS;
|
|
594
|
+
if (exp - from > MAX_REQUEST_LIFETIME_SECONDS + CLOCK_SKEW_S) malformed();
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
function chooseEncryption(meta) {
|
|
598
|
+
if (!isRecord(meta)) return null;
|
|
599
|
+
const encsRaw = meta.encrypted_response_enc_values_supported;
|
|
600
|
+
const encs = encsRaw === void 0 ? [DEFAULT_RESPONSE_ENCRYPTION_ENC] : Array.isArray(encsRaw) ? encsRaw : [];
|
|
601
|
+
const enc = RESPONSE_ENCRYPTION_ENCS.find((e) => encs.includes(e));
|
|
602
|
+
const keys = isRecord(meta.jwks) ? meta.jwks.keys : void 0;
|
|
603
|
+
const key = Array.isArray(keys) ? keys.slice(0, 16).find(
|
|
604
|
+
(k) => isP256PublicJwk(k) && k.alg === "ECDH-ES" && (k.kid === void 0 || typeof k.kid === "string") && (k.use === void 0 || k.use === "enc")
|
|
605
|
+
) : void 0;
|
|
606
|
+
if (!enc || !isJweEnc(enc) || !key) return null;
|
|
607
|
+
return {
|
|
608
|
+
jwk: {
|
|
609
|
+
kty: "EC",
|
|
610
|
+
crv: "P-256",
|
|
611
|
+
x: key.x,
|
|
612
|
+
y: key.y,
|
|
613
|
+
...typeof key.kid === "string" ? { kid: key.kid } : {},
|
|
614
|
+
use: "enc",
|
|
615
|
+
alg: "ECDH-ES"
|
|
616
|
+
},
|
|
617
|
+
enc
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
function parsePayload(jwt, head, payload, opts) {
|
|
621
|
+
const mode = payload.response_mode;
|
|
622
|
+
if (mode !== "direct_post" && mode !== "direct_post.jwt") {
|
|
623
|
+
throw new Oid4vpError("unsupported_response_mode");
|
|
624
|
+
}
|
|
625
|
+
const responseUri = payload.response_uri;
|
|
626
|
+
if (typeof responseUri !== "string") malformed();
|
|
627
|
+
if (!isFetchableUrl(responseUri, {
|
|
628
|
+
allowHttp: opts.allowHttp ?? false,
|
|
629
|
+
allowLocal: opts.allowLocal ?? false
|
|
630
|
+
})) {
|
|
631
|
+
throw new Oid4vpError("insecure_response_uri");
|
|
632
|
+
}
|
|
633
|
+
const state = payload.state;
|
|
634
|
+
if (state !== void 0 && (typeof state !== "string" || !URL_SAFE.test(state)))
|
|
635
|
+
malformed();
|
|
636
|
+
const encryption = mode === "direct_post.jwt" ? chooseEncryption(payload.client_metadata) : null;
|
|
637
|
+
const target = {
|
|
638
|
+
clientId: head.clientId,
|
|
639
|
+
responseUri,
|
|
640
|
+
responseMode: mode,
|
|
641
|
+
...state !== void 0 ? { state } : {},
|
|
642
|
+
...encryption ? {
|
|
643
|
+
responseEncryptionJwk: encryption.jwk,
|
|
644
|
+
responseEncryptionEnc: encryption.enc
|
|
645
|
+
} : {}
|
|
646
|
+
};
|
|
647
|
+
const refuse = (code, feature) => new Oid4vpError(code, void 0, {
|
|
648
|
+
...feature !== void 0 ? { feature } : {},
|
|
649
|
+
errorTarget: target
|
|
650
|
+
});
|
|
651
|
+
if (mode === "direct_post.jwt" && !encryption)
|
|
652
|
+
throw refuse("unsupported_encryption");
|
|
653
|
+
if (payload.redirect_uri !== void 0) throw refuse("malformed_request");
|
|
654
|
+
if (payload.response_type !== "vp_token")
|
|
655
|
+
throw refuse("unsupported_request", "response_type");
|
|
656
|
+
if (payload.transaction_data !== void 0)
|
|
657
|
+
throw refuse("unsupported_request", "transaction_data");
|
|
658
|
+
if (payload.scope !== void 0) throw refuse("unsupported_request", "scope");
|
|
659
|
+
const nonce = payload.nonce;
|
|
660
|
+
if (typeof nonce !== "string" || !URL_SAFE.test(nonce))
|
|
661
|
+
throw refuse("malformed_request");
|
|
662
|
+
let dcql;
|
|
663
|
+
try {
|
|
664
|
+
dcql = validateDcql(payload.dcql_query);
|
|
665
|
+
} catch (e) {
|
|
666
|
+
if (e instanceof Oid4vpError) throw refuse(e.code, e.feature);
|
|
667
|
+
throw e;
|
|
668
|
+
}
|
|
669
|
+
return {
|
|
670
|
+
jwt,
|
|
671
|
+
clientId: head.clientId,
|
|
672
|
+
clientDid: head.clientDid,
|
|
673
|
+
kid: head.kid,
|
|
674
|
+
responseUri,
|
|
675
|
+
responseMode: mode,
|
|
676
|
+
nonce,
|
|
677
|
+
...state !== void 0 ? { state } : {},
|
|
678
|
+
dcql,
|
|
679
|
+
...encryption ? {
|
|
680
|
+
responseEncryptionJwk: encryption.jwk,
|
|
681
|
+
responseEncryptionEnc: encryption.enc
|
|
682
|
+
} : {},
|
|
683
|
+
...typeof payload.exp === "number" ? { exp: payload.exp } : {},
|
|
684
|
+
...typeof payload.iat === "number" ? { iat: payload.iat } : {}
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
async function verifyAuthorizationRequest(jwt, opts) {
|
|
688
|
+
if (typeof jwt !== "string" || jwt.length > MAX_REQUEST_OBJECT_LENGTH) {
|
|
689
|
+
malformed();
|
|
690
|
+
}
|
|
691
|
+
let jws;
|
|
692
|
+
try {
|
|
693
|
+
jws = decodeJws(jwt.trim());
|
|
694
|
+
} catch {
|
|
695
|
+
malformed();
|
|
696
|
+
}
|
|
697
|
+
const parseOpts = {
|
|
698
|
+
allowHttp: opts.allowHttp ?? false,
|
|
699
|
+
allowLocal: opts.allowLocal ?? false,
|
|
700
|
+
nowS: toSeconds(clockMs(opts.now)),
|
|
701
|
+
requireExpiry: opts.requireExpiry ?? false
|
|
702
|
+
};
|
|
703
|
+
const head = checkHeader(jws.header, jws.payload, opts.expectedClientId);
|
|
704
|
+
checkTimes(jws.payload, parseOpts);
|
|
705
|
+
let doc;
|
|
706
|
+
try {
|
|
707
|
+
doc = await opts.resolver(head.clientDid);
|
|
708
|
+
} catch (e) {
|
|
709
|
+
if (e instanceof ResolverUnavailable) throw e;
|
|
710
|
+
throw new Oid4vpError("unknown_verifier");
|
|
711
|
+
}
|
|
712
|
+
if (!doc) throw new Oid4vpError("unknown_verifier");
|
|
713
|
+
const key = findKey(doc, head.kid, "authentication") ?? findKey(doc, head.kid, "assertionMethod");
|
|
714
|
+
if (!key) throw new Oid4vpError("unknown_key");
|
|
715
|
+
if (!verifyEd25519(jws.signature, utf8(jws.signingInput), key.publicKey)) {
|
|
716
|
+
throw new Oid4vpError("bad_signature");
|
|
717
|
+
}
|
|
718
|
+
return parsePayload(jwt.trim(), head, jws.payload, parseOpts);
|
|
719
|
+
}
|
|
720
|
+
function decodeOwnAuthorizationRequest(jwt, opts = {}) {
|
|
721
|
+
let jws;
|
|
722
|
+
try {
|
|
723
|
+
jws = decodeJws(jwt.trim());
|
|
724
|
+
} catch {
|
|
725
|
+
malformed();
|
|
726
|
+
}
|
|
727
|
+
const parseOpts = {
|
|
728
|
+
allowHttp: opts.allowHttp ?? false,
|
|
729
|
+
allowLocal: opts.allowLocal ?? false,
|
|
730
|
+
nowS: null,
|
|
731
|
+
requireExpiry: false
|
|
732
|
+
};
|
|
733
|
+
const head = checkHeader(jws.header, jws.payload, void 0);
|
|
734
|
+
checkTimes(jws.payload, parseOpts);
|
|
735
|
+
return parsePayload(jwt.trim(), head, jws.payload, parseOpts);
|
|
736
|
+
}
|
|
737
|
+
async function createPresentation(input) {
|
|
738
|
+
const parsed = parseSdJwt(input.sdjwt.trim());
|
|
739
|
+
if (parsed.kbJwt !== void 0) throw new SdJwtError("malformed");
|
|
740
|
+
const raws = selectDisclosures(parsed, input.disclose);
|
|
741
|
+
const base = serializeSdJwt(parsed.jwt, raws);
|
|
742
|
+
const kb = await signJws(
|
|
743
|
+
{ alg: "EdDSA", typ: "kb+jwt" },
|
|
744
|
+
{
|
|
745
|
+
iat: toSeconds(clockMs(input.now)),
|
|
746
|
+
aud: input.audience,
|
|
747
|
+
nonce: input.nonce,
|
|
748
|
+
sd_hash: sdHash(base)
|
|
749
|
+
},
|
|
750
|
+
input.signer
|
|
751
|
+
);
|
|
752
|
+
return base + kb;
|
|
753
|
+
}
|
|
754
|
+
function checkVpToken(vpToken, dcql) {
|
|
755
|
+
if (!isRecord(vpToken)) throw new Oid4vpError("malformed_response");
|
|
756
|
+
const ids = dcql ? new Set(dcql.credentials.map((c) => c.id)) : null;
|
|
757
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
758
|
+
for (const [id, list] of Object.entries(vpToken)) {
|
|
759
|
+
if (ids && !ids.has(id)) throw new Oid4vpError("malformed_response");
|
|
760
|
+
if (!Array.isArray(list) || list.length !== 1 || !list.every((p) => typeof p === "string" && p.length > 0)) {
|
|
761
|
+
throw new Oid4vpError("malformed_response");
|
|
762
|
+
}
|
|
763
|
+
out[id] = [...list];
|
|
764
|
+
}
|
|
765
|
+
if (Object.keys(out).length === 0)
|
|
766
|
+
throw new Oid4vpError("malformed_response");
|
|
767
|
+
return out;
|
|
768
|
+
}
|
|
769
|
+
async function formBody(target, members) {
|
|
770
|
+
const params = new URLSearchParams();
|
|
771
|
+
const withState = {
|
|
772
|
+
...members,
|
|
773
|
+
...target.state !== void 0 ? { state: target.state } : {}
|
|
774
|
+
};
|
|
775
|
+
if (target.responseMode === "direct_post.jwt" && target.responseEncryptionJwk) {
|
|
776
|
+
params.set(
|
|
777
|
+
"response",
|
|
778
|
+
await encryptJwe(
|
|
779
|
+
withState,
|
|
780
|
+
target.responseEncryptionJwk,
|
|
781
|
+
target.responseEncryptionEnc ?? DEFAULT_RESPONSE_ENCRYPTION_ENC
|
|
782
|
+
)
|
|
783
|
+
);
|
|
784
|
+
} else {
|
|
785
|
+
for (const [k, v] of Object.entries(withState))
|
|
786
|
+
params.set(k, typeof v === "string" ? v : JSON.stringify(v));
|
|
787
|
+
}
|
|
788
|
+
return params.toString();
|
|
789
|
+
}
|
|
790
|
+
async function createAuthorizationResponse(input) {
|
|
791
|
+
const { request: request2 } = input;
|
|
792
|
+
const vpToken = checkVpToken(input.vpToken, request2.dcql);
|
|
793
|
+
if (request2.responseMode === "direct_post.jwt" && !request2.responseEncryptionJwk)
|
|
794
|
+
throw new Oid4vpError("unsupported_encryption");
|
|
795
|
+
return {
|
|
796
|
+
url: request2.responseUri,
|
|
797
|
+
body: await formBody(request2, { vp_token: vpToken }),
|
|
798
|
+
contentType: "application/x-www-form-urlencoded"
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
var AUTHORIZATION_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
802
|
+
"invalid_request",
|
|
803
|
+
"access_denied",
|
|
804
|
+
"invalid_client",
|
|
805
|
+
"invalid_scope",
|
|
806
|
+
"vp_formats_not_supported",
|
|
807
|
+
"invalid_request_uri_method",
|
|
808
|
+
"invalid_transaction_data",
|
|
809
|
+
"wallet_unavailable"
|
|
810
|
+
]);
|
|
811
|
+
var ERROR_DESCRIPTION = /^[\x20-\x21\x23-\x5b\x5d-\x7e]{1,300}$/;
|
|
812
|
+
function authorizationErrorFor(e) {
|
|
813
|
+
if (!(e instanceof Oid4vpError) || !e.errorTarget) return null;
|
|
814
|
+
if (e.code === "unsupported_request") {
|
|
815
|
+
if (e.feature === "transaction_data") return "invalid_transaction_data";
|
|
816
|
+
if (e.feature === "format") return "vp_formats_not_supported";
|
|
817
|
+
if (e.feature === "scope") return "invalid_scope";
|
|
818
|
+
}
|
|
819
|
+
return "invalid_request";
|
|
820
|
+
}
|
|
821
|
+
async function createAuthorizationErrorResponse(input) {
|
|
822
|
+
if (!AUTHORIZATION_ERROR_CODES.has(input.error))
|
|
823
|
+
throw new TypeError("error is not an OpenID4VP error code");
|
|
824
|
+
if (input.errorDescription !== void 0 && !ERROR_DESCRIPTION.test(input.errorDescription))
|
|
825
|
+
throw new TypeError(
|
|
826
|
+
'errorDescription must be printable ASCII without " and \\ (at most 300)'
|
|
827
|
+
);
|
|
828
|
+
return {
|
|
829
|
+
url: input.target.responseUri,
|
|
830
|
+
body: await formBody(input.target, {
|
|
831
|
+
error: input.error,
|
|
832
|
+
...input.errorDescription !== void 0 ? { error_description: input.errorDescription } : {}
|
|
833
|
+
}),
|
|
834
|
+
contentType: "application/x-www-form-urlencoded"
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
async function sendAuthorizationError(input) {
|
|
838
|
+
const policy = {
|
|
839
|
+
allowHttp: input.allowHttp ?? false,
|
|
840
|
+
allowLocal: input.allowLocal ?? false
|
|
841
|
+
};
|
|
842
|
+
if (!isFetchableUrl(input.target.responseUri, policy))
|
|
843
|
+
throw new Oid4vpError("insecure_response_uri");
|
|
844
|
+
const res = await createAuthorizationErrorResponse(input);
|
|
845
|
+
let answer;
|
|
846
|
+
try {
|
|
847
|
+
answer = await request(input.fetch, res.url, {
|
|
848
|
+
method: "POST",
|
|
849
|
+
headers: {
|
|
850
|
+
"content-type": res.contentType,
|
|
851
|
+
accept: "application/json"
|
|
852
|
+
},
|
|
853
|
+
body: res.body,
|
|
854
|
+
maxBytes: 16 * 1024,
|
|
855
|
+
...input.timeoutMs !== void 0 ? { timeoutMs: input.timeoutMs } : {}
|
|
856
|
+
});
|
|
857
|
+
} catch {
|
|
858
|
+
return { delivered: false, status: null };
|
|
859
|
+
}
|
|
860
|
+
let redirectUri;
|
|
861
|
+
if (answer.status === 200) {
|
|
862
|
+
try {
|
|
863
|
+
const json = JSON.parse(answer.text);
|
|
864
|
+
if (isRecord(json) && typeof json.redirect_uri === "string" && // A fragment is normal here (§8.2 example: `#response_code=…`).
|
|
865
|
+
isFetchableUrl(json.redirect_uri.split("#")[0] ?? "", policy))
|
|
866
|
+
redirectUri = json.redirect_uri;
|
|
867
|
+
} catch {
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return {
|
|
871
|
+
delivered: answer.status === 200,
|
|
872
|
+
status: answer.status,
|
|
873
|
+
...redirectUri !== void 0 ? { redirectUri } : {}
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
async function parseAuthorizationResponse(body, opts = {}) {
|
|
877
|
+
if (opts.request?.responseMode === "direct_post.jwt" && !opts.decryptionKey)
|
|
878
|
+
throw new TypeError(
|
|
879
|
+
"decryptionKey is required for a direct_post.jwt request"
|
|
880
|
+
);
|
|
881
|
+
if (typeof body === "string" && body.length > MAX_RESPONSE_BODY_LENGTH) {
|
|
882
|
+
throw new Oid4vpError("malformed_response");
|
|
883
|
+
}
|
|
884
|
+
const params = typeof body === "string" ? new URLSearchParams(body) : body;
|
|
885
|
+
const error = params.get("error");
|
|
886
|
+
if (error !== null) {
|
|
887
|
+
throw new Oid4vpError("wallet_error", walletErrorCode(error));
|
|
888
|
+
}
|
|
889
|
+
let vpToken;
|
|
890
|
+
let state;
|
|
891
|
+
const response = params.get("response");
|
|
892
|
+
if (opts.decryptionKey) {
|
|
893
|
+
if (response === null || params.has("vp_token")) {
|
|
894
|
+
throw new Oid4vpError("malformed_response");
|
|
895
|
+
}
|
|
896
|
+
let payload;
|
|
897
|
+
try {
|
|
898
|
+
payload = await decryptJwe(response, opts.decryptionKey);
|
|
899
|
+
} catch (e) {
|
|
900
|
+
if (e instanceof JweError && e.kind === "malformed") {
|
|
901
|
+
throw new Oid4vpError("malformed_response");
|
|
902
|
+
}
|
|
903
|
+
throw new Oid4vpError("decryption_failed");
|
|
904
|
+
}
|
|
905
|
+
if (typeof payload.error === "string") {
|
|
906
|
+
throw new Oid4vpError("wallet_error", walletErrorCode(payload.error));
|
|
907
|
+
}
|
|
908
|
+
vpToken = typeof payload.vp_token === "string" ? parseJson(payload.vp_token) : payload.vp_token;
|
|
909
|
+
state = payload.state;
|
|
910
|
+
} else {
|
|
911
|
+
if (response !== null) throw new Oid4vpError("decryption_failed");
|
|
912
|
+
const raw = params.get("vp_token");
|
|
913
|
+
if (raw === null) throw new Oid4vpError("malformed_response");
|
|
914
|
+
vpToken = parseJson(raw);
|
|
915
|
+
state = params.get("state") ?? void 0;
|
|
916
|
+
}
|
|
917
|
+
if (state !== void 0 && typeof state !== "string") {
|
|
918
|
+
throw new Oid4vpError("malformed_response");
|
|
919
|
+
}
|
|
920
|
+
return {
|
|
921
|
+
vpToken: checkVpToken(vpToken, null),
|
|
922
|
+
...typeof state === "string" ? { state } : {}
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
function walletErrorCode(error) {
|
|
926
|
+
return /^[A-Za-z0-9_.-]{1,64}$/.test(error) ? error : "invalid";
|
|
927
|
+
}
|
|
928
|
+
function parseJson(s) {
|
|
929
|
+
try {
|
|
930
|
+
return JSON.parse(s);
|
|
931
|
+
} catch {
|
|
932
|
+
throw new Oid4vpError("malformed_response");
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
async function verifyAuthorizationResponse(input) {
|
|
936
|
+
const request2 = typeof input.request === "string" ? (
|
|
937
|
+
// Your own request: its response URI was checked when you created it.
|
|
938
|
+
decodeOwnAuthorizationRequest(input.request, {
|
|
939
|
+
allowHttp: true,
|
|
940
|
+
allowLocal: true
|
|
941
|
+
})
|
|
942
|
+
) : input.request;
|
|
943
|
+
if (input.state !== request2.state) {
|
|
944
|
+
throw new Oid4vpError("state_mismatch");
|
|
945
|
+
}
|
|
946
|
+
const results = /* @__PURE__ */ Object.create(null);
|
|
947
|
+
const unbound = (code) => ({
|
|
948
|
+
ok: false,
|
|
949
|
+
code,
|
|
950
|
+
holderAuthenticated: false,
|
|
951
|
+
keyBindingValid: false
|
|
952
|
+
});
|
|
953
|
+
let everySubjectExpected = true;
|
|
954
|
+
const queryIds = new Set(request2.dcql.credentials.map((q) => q.id));
|
|
955
|
+
for (const id of Object.keys(input.vpToken)) {
|
|
956
|
+
if (!queryIds.has(id)) results[id] = unbound("unexpected_presentation");
|
|
957
|
+
}
|
|
958
|
+
for (const query of request2.dcql.credentials) {
|
|
959
|
+
const policy = input.policyFor(query.id);
|
|
960
|
+
const subjectExpected = policy.expectedSubject !== void 0;
|
|
961
|
+
if (!subjectExpected) everySubjectExpected = false;
|
|
962
|
+
const list = input.vpToken[query.id];
|
|
963
|
+
if (list?.length !== 1 || typeof list[0] !== "string") {
|
|
964
|
+
results[query.id] = unbound("missing_presentation");
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
const presentation = list[0];
|
|
968
|
+
const out = await verifyPresentation(
|
|
969
|
+
presentation,
|
|
970
|
+
{
|
|
971
|
+
...policy,
|
|
972
|
+
audience: request2.clientId,
|
|
973
|
+
nonce: request2.nonce,
|
|
974
|
+
requireKeyBinding: true
|
|
975
|
+
},
|
|
976
|
+
input.deps
|
|
977
|
+
);
|
|
978
|
+
if (!out.ok) {
|
|
979
|
+
results[query.id] = {
|
|
980
|
+
ok: false,
|
|
981
|
+
code: out.code,
|
|
982
|
+
holderAuthenticated: out.holderAuthenticated,
|
|
983
|
+
keyBindingValid: out.keyBindingValid
|
|
984
|
+
};
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
const failQuery = (code) => ({
|
|
988
|
+
ok: false,
|
|
989
|
+
code,
|
|
990
|
+
holderAuthenticated: subjectExpected,
|
|
991
|
+
keyBindingValid: true
|
|
992
|
+
});
|
|
993
|
+
if (!query.meta.vct_values.includes(out.vct)) {
|
|
994
|
+
results[query.id] = failQuery("wrong_type");
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
const issuers = issuerFilter(query);
|
|
998
|
+
if (issuers && !issuers.includes(out.issuer)) {
|
|
999
|
+
results[query.id] = failQuery("untrusted_issuer");
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
const visible = visibleClaims(out, presentation, requestedClaims(query));
|
|
1003
|
+
let failure = null;
|
|
1004
|
+
for (const claim of requestedClaims(query)) {
|
|
1005
|
+
const name = claim.path[0];
|
|
1006
|
+
if (!Object.hasOwn(visible, name)) {
|
|
1007
|
+
failure = failQuery("missing_claim");
|
|
1008
|
+
break;
|
|
1009
|
+
}
|
|
1010
|
+
if (!valueMatches(visible[name], claim.values)) {
|
|
1011
|
+
failure = failQuery("value_mismatch");
|
|
1012
|
+
break;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
results[query.id] = failure ?? out;
|
|
1016
|
+
}
|
|
1017
|
+
const verifiedOnes = Object.values(results).filter(
|
|
1018
|
+
(r) => r.ok
|
|
1019
|
+
);
|
|
1020
|
+
const first = verifiedOnes[0];
|
|
1021
|
+
if (first && verifiedOnes.some(
|
|
1022
|
+
(r) => r.subject !== first.subject || r.holderKey?.x !== first.holderKey?.x
|
|
1023
|
+
)) {
|
|
1024
|
+
for (const [id, r] of Object.entries(results)) {
|
|
1025
|
+
if (r.ok) results[id] = unbound("wrong_subject");
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
const all = Object.values(results);
|
|
1029
|
+
return {
|
|
1030
|
+
ok: all.every((r) => r.ok),
|
|
1031
|
+
results,
|
|
1032
|
+
holderAuthenticated: everySubjectExpected && all.every((r) => r.ok || r.holderAuthenticated),
|
|
1033
|
+
keyBindingValid: all.every((r) => r.ok || r.keyBindingValid)
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
var SIGNED_REGISTERED_CLAIMS = ["sub", "iat"];
|
|
1037
|
+
function visibleClaims(out, presentation, claims) {
|
|
1038
|
+
const wanted = SIGNED_REGISTERED_CLAIMS.filter(
|
|
1039
|
+
(name) => claims.some((c) => c.path[0] === name)
|
|
1040
|
+
);
|
|
1041
|
+
if (wanted.length === 0) return out.claims;
|
|
1042
|
+
const visible = { ...out.claims, sub: out.subject };
|
|
1043
|
+
if (wanted.includes("iat")) {
|
|
1044
|
+
const parsed = parseSdJwt(presentation.trim());
|
|
1045
|
+
const all = processDisclosures(parsed.payload, parsed.disclosures).claims;
|
|
1046
|
+
if (typeof all.iat === "number") visible.iat = all.iat;
|
|
1047
|
+
}
|
|
1048
|
+
return visible;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
export { DEFAULT_RESPONSE_ENCRYPTION_ENC, MAX_CLAIMS_PER_QUERY, MAX_CREDENTIAL_QUERIES, MAX_REQUEST_LIFETIME_SECONDS, MAX_REQUEST_OBJECT_LENGTH, MAX_RESPONSE_BODY_LENGTH, Oid4vpError, RESPONSE_ENCRYPTION_ENC, RESPONSE_ENCRYPTION_ENCS, SELF_ISSUED_AUDIENCE, authorizationErrorFor, buildWalletLink, createAuthorizationErrorResponse, createAuthorizationRequest, createAuthorizationResponse, createPresentation, decodeOwnAuthorizationRequest, generateResponseKeyPair, matchCredential, parseAuthorizationResponse, parseWalletLink, sendAuthorizationError, summarizeDcql, validateDcql, verifyAuthorizationRequest, verifyAuthorizationResponse };
|
|
1052
|
+
//# sourceMappingURL=chunk-2SF56NEJ.js.map
|
|
1053
|
+
//# sourceMappingURL=chunk-2SF56NEJ.js.map
|