@atrib/verify 0.3.7 → 0.5.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.
- package/README.md +95 -24
- package/dist/ap2-vi-evidence.d.ts +47 -0
- package/dist/ap2-vi-evidence.d.ts.map +1 -1
- package/dist/ap2-vi-evidence.js +713 -9
- package/dist/ap2-vi-evidence.js.map +1 -1
- package/dist/handoff.d.ts +94 -0
- package/dist/handoff.d.ts.map +1 -0
- package/dist/handoff.js +371 -0
- package/dist/handoff.js.map +1 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -6
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/verifier.d.ts +14 -1
- package/dist/verifier.d.ts.map +1 -1
- package/dist/verifier.js +29 -1
- package/dist/verifier.js.map +1 -1
- package/dist/verify-record.d.ts +30 -8
- package/dist/verify-record.d.ts.map +1 -1
- package/dist/verify-record.js +75 -16
- package/dist/verify-record.js.map +1 -1
- package/package.json +4 -2
package/dist/ap2-vi-evidence.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import canonicalize from 'canonicalize';
|
|
3
3
|
import { createPublicKey, verify as verifyEcdsa } from 'node:crypto';
|
|
4
4
|
import { base64urlDecode, base64urlEncode, sha256 } from '@atrib/mcp';
|
|
5
|
+
import { SDJwtInstance } from '@sd-jwt/core';
|
|
6
|
+
import { SDJwtVcInstance } from '@sd-jwt/sd-jwt-vc';
|
|
5
7
|
import { createLocalJWKSet, createRemoteJWKSet, customFetch, decodeJwt, decodeProtectedHeader, jwtVerify, } from 'jose';
|
|
6
8
|
const textEncoder = new TextEncoder();
|
|
7
9
|
const textDecoder = new TextDecoder();
|
|
@@ -14,6 +16,9 @@ function isString(value) {
|
|
|
14
16
|
function isNumber(value) {
|
|
15
17
|
return typeof value === 'number' && Number.isFinite(value);
|
|
16
18
|
}
|
|
19
|
+
function isSafeInteger(value) {
|
|
20
|
+
return typeof value === 'number' && Number.isSafeInteger(value);
|
|
21
|
+
}
|
|
17
22
|
function isJsonWebKeySet(value) {
|
|
18
23
|
return (isRecord(value) && Array.isArray(value['keys']) && value['keys'].every((key) => isRecord(key)));
|
|
19
24
|
}
|
|
@@ -26,18 +31,27 @@ function decodeBase64urlJson(value) {
|
|
|
26
31
|
function signatureCheck(status, reason) {
|
|
27
32
|
return reason === undefined ? { status } : { status, reason };
|
|
28
33
|
}
|
|
29
|
-
function
|
|
30
|
-
|
|
34
|
+
function sdJwtConformanceCheck(status, profile, reason) {
|
|
35
|
+
return reason === undefined ? { status, profile } : { status, profile, reason };
|
|
36
|
+
}
|
|
37
|
+
function collectDelegatePayloadDigestList(payload) {
|
|
38
|
+
const digests = [];
|
|
31
39
|
const delegatePayload = payload['delegate_payload'];
|
|
32
40
|
if (!Array.isArray(delegatePayload))
|
|
33
41
|
return digests;
|
|
34
42
|
for (const item of delegatePayload) {
|
|
35
43
|
if (isRecord(item) && isString(item['...'])) {
|
|
36
|
-
digests.
|
|
44
|
+
digests.push(item['...']);
|
|
37
45
|
}
|
|
38
46
|
}
|
|
39
47
|
return digests;
|
|
40
48
|
}
|
|
49
|
+
function collectDelegatePayloadDigests(payload) {
|
|
50
|
+
return new Set(collectDelegatePayloadDigestList(payload));
|
|
51
|
+
}
|
|
52
|
+
function hasDuplicateStrings(values) {
|
|
53
|
+
return new Set(values).size !== values.length;
|
|
54
|
+
}
|
|
41
55
|
function disclosureValue(decoded) {
|
|
42
56
|
if (!Array.isArray(decoded))
|
|
43
57
|
return undefined;
|
|
@@ -65,7 +79,10 @@ function parseCredential(input) {
|
|
|
65
79
|
const value = disclosureValue(decodeBase64urlJson(encoded));
|
|
66
80
|
return { encoded, digest: hashAscii(encoded), value };
|
|
67
81
|
});
|
|
68
|
-
const
|
|
82
|
+
const mandateDisclosures = disclosures.flatMap((disclosure) => isRecord(disclosure.value) && isString(disclosure.value['vct'])
|
|
83
|
+
? [{ digest: disclosure.digest, value: disclosure.value }]
|
|
84
|
+
: []);
|
|
85
|
+
const mandateValues = mandateDisclosures.map((mandate) => mandate.value);
|
|
69
86
|
return {
|
|
70
87
|
input,
|
|
71
88
|
jwt,
|
|
@@ -74,6 +91,7 @@ function parseCredential(input) {
|
|
|
74
91
|
signingInput: `${parts[0]}.${parts[1]}`,
|
|
75
92
|
signature: base64urlDecode(parts[2]),
|
|
76
93
|
disclosures,
|
|
94
|
+
mandateDisclosures,
|
|
77
95
|
mandateValues,
|
|
78
96
|
};
|
|
79
97
|
}
|
|
@@ -97,22 +115,473 @@ function sameJson(left, right) {
|
|
|
97
115
|
const rightCanonical = canonicalJson(right);
|
|
98
116
|
return leftCanonical !== null && leftCanonical === rightCanonical;
|
|
99
117
|
}
|
|
118
|
+
function getConstraintObjects(mandate) {
|
|
119
|
+
const constraints = mandate['constraints'];
|
|
120
|
+
if (!Array.isArray(constraints))
|
|
121
|
+
return [];
|
|
122
|
+
return constraints.filter(isRecord);
|
|
123
|
+
}
|
|
124
|
+
function normalizeConstraintType(value) {
|
|
125
|
+
return value.startsWith('mandate.') ? value.slice('mandate.'.length) : value;
|
|
126
|
+
}
|
|
127
|
+
function makeConstraintCheck(constraint, domain, status, reason) {
|
|
128
|
+
const type = isString(constraint['type']) ? constraint['type'] : `${domain}.unknown`;
|
|
129
|
+
return reason === undefined ? { type, domain, status } : { type, domain, status, reason };
|
|
130
|
+
}
|
|
131
|
+
function resolveDisclosureRefs(value, disclosures) {
|
|
132
|
+
if (Array.isArray(value))
|
|
133
|
+
return value.map((item) => resolveDisclosureRefs(item, disclosures));
|
|
134
|
+
if (!isRecord(value))
|
|
135
|
+
return value;
|
|
136
|
+
const digest = value['...'];
|
|
137
|
+
if (isString(digest) && Object.keys(value).length === 1) {
|
|
138
|
+
return disclosures.has(digest)
|
|
139
|
+
? resolveDisclosureRefs(disclosures.get(digest), disclosures)
|
|
140
|
+
: value;
|
|
141
|
+
}
|
|
142
|
+
const resolved = {};
|
|
143
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
144
|
+
resolved[key] = resolveDisclosureRefs(nested, disclosures);
|
|
145
|
+
}
|
|
146
|
+
return resolved;
|
|
147
|
+
}
|
|
148
|
+
function disclosureMap(parsedCredentials) {
|
|
149
|
+
const disclosures = new Map();
|
|
150
|
+
for (const parsed of parsedCredentials) {
|
|
151
|
+
for (const disclosure of parsed.disclosures) {
|
|
152
|
+
disclosures.set(disclosure.digest, disclosure.value);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return disclosures;
|
|
156
|
+
}
|
|
157
|
+
function comparableFields(left, right, fields) {
|
|
158
|
+
return fields.filter((field) => isString(left[field]) && isString(right[field]));
|
|
159
|
+
}
|
|
160
|
+
function entityMatches(left, right, fields) {
|
|
161
|
+
if (!isRecord(left) || !isRecord(right))
|
|
162
|
+
return false;
|
|
163
|
+
const fieldsToCompare = comparableFields(left, right, fields);
|
|
164
|
+
return (fieldsToCompare.length > 0 && fieldsToCompare.every((field) => left[field] === right[field]));
|
|
165
|
+
}
|
|
166
|
+
function anyEntityMatches(allowed, actual, fields) {
|
|
167
|
+
if (!Array.isArray(allowed))
|
|
168
|
+
return null;
|
|
169
|
+
if (!isRecord(actual))
|
|
170
|
+
return null;
|
|
171
|
+
const allowedRecords = allowed.filter(isRecord);
|
|
172
|
+
if (allowedRecords.length === 0)
|
|
173
|
+
return null;
|
|
174
|
+
return allowedRecords.some((candidate) => entityMatches(candidate, actual, fields));
|
|
175
|
+
}
|
|
176
|
+
function paymentAmount(target) {
|
|
177
|
+
const amount = target['payment_amount'];
|
|
178
|
+
if (!isRecord(amount) || !isSafeInteger(amount['amount']) || !isString(amount['currency'])) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
return { amount: amount['amount'], currency: amount['currency'] };
|
|
182
|
+
}
|
|
183
|
+
function optionalIntegerBound(value) {
|
|
184
|
+
if (value === undefined)
|
|
185
|
+
return undefined;
|
|
186
|
+
return isSafeInteger(value) ? value : null;
|
|
187
|
+
}
|
|
188
|
+
function compareIsoDate(value, lower, upper) {
|
|
189
|
+
const time = Date.parse(value);
|
|
190
|
+
if (!Number.isFinite(time))
|
|
191
|
+
return null;
|
|
192
|
+
if (isString(lower)) {
|
|
193
|
+
const lowerTime = Date.parse(lower);
|
|
194
|
+
if (!Number.isFinite(lowerTime) || time < lowerTime)
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
if (isString(upper)) {
|
|
198
|
+
const upperTime = Date.parse(upper);
|
|
199
|
+
if (!Number.isFinite(upperTime) || time > upperTime)
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
function decodeCompactJwtPayload(value) {
|
|
205
|
+
try {
|
|
206
|
+
const payload = decodeJwt(value);
|
|
207
|
+
return isRecord(payload) ? payload : undefined;
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function resolveCheckoutPayload(explicitPayload, closedCheckoutMandates) {
|
|
214
|
+
if (isRecord(explicitPayload))
|
|
215
|
+
return explicitPayload;
|
|
216
|
+
for (const mandate of closedCheckoutMandates) {
|
|
217
|
+
const payload = mandate['checkout_payload'];
|
|
218
|
+
if (isRecord(payload))
|
|
219
|
+
return payload;
|
|
220
|
+
const checkoutJwt = mandate['checkout_jwt'];
|
|
221
|
+
if (isString(checkoutJwt)) {
|
|
222
|
+
const decoded = decodeCompactJwtPayload(checkoutJwt);
|
|
223
|
+
if (decoded)
|
|
224
|
+
return decoded;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
function checkoutLineItemId(value) {
|
|
230
|
+
const product = value['product'];
|
|
231
|
+
if (isRecord(product) && isString(product['id']))
|
|
232
|
+
return product['id'];
|
|
233
|
+
if (isRecord(product) && isString(product['sku']))
|
|
234
|
+
return product['sku'];
|
|
235
|
+
if (isString(value['sku']))
|
|
236
|
+
return value['sku'];
|
|
237
|
+
return isString(value['id']) ? value['id'] : null;
|
|
238
|
+
}
|
|
239
|
+
function checkoutLineItems(checkoutPayload) {
|
|
240
|
+
const lineItems = checkoutPayload['line_items'];
|
|
241
|
+
if (Array.isArray(lineItems))
|
|
242
|
+
return lineItems;
|
|
243
|
+
const cart = checkoutPayload['cart'];
|
|
244
|
+
if (isRecord(cart) && Array.isArray(cart['items']))
|
|
245
|
+
return cart['items'];
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
function checkoutLineItemQuantities(checkoutPayload) {
|
|
249
|
+
const items = checkoutLineItems(checkoutPayload);
|
|
250
|
+
if (items === null)
|
|
251
|
+
return null;
|
|
252
|
+
const quantities = new Map();
|
|
253
|
+
for (const item of items) {
|
|
254
|
+
if (!isRecord(item))
|
|
255
|
+
return null;
|
|
256
|
+
const id = checkoutLineItemId(item);
|
|
257
|
+
const quantity = item['quantity'];
|
|
258
|
+
if (id === null || !isSafeInteger(quantity) || quantity < 0)
|
|
259
|
+
return null;
|
|
260
|
+
quantities.set(id, (quantities.get(id) ?? 0) + quantity);
|
|
261
|
+
}
|
|
262
|
+
return quantities;
|
|
263
|
+
}
|
|
264
|
+
function maxLineItemFlow(requirements, checkoutQuantities) {
|
|
265
|
+
const source = 'source';
|
|
266
|
+
const sink = 'sink';
|
|
267
|
+
const graph = new Map();
|
|
268
|
+
const addEdge = (from, to, capacity) => {
|
|
269
|
+
if (!graph.has(from))
|
|
270
|
+
graph.set(from, new Map());
|
|
271
|
+
if (!graph.has(to))
|
|
272
|
+
graph.set(to, new Map());
|
|
273
|
+
graph.get(from).set(to, (graph.get(from).get(to) ?? 0) + capacity);
|
|
274
|
+
graph.get(to).set(from, graph.get(to).get(from) ?? 0);
|
|
275
|
+
};
|
|
276
|
+
requirements.forEach((requirement, index) => {
|
|
277
|
+
const reqNode = `req:${index}`;
|
|
278
|
+
addEdge(source, reqNode, requirement.quantity);
|
|
279
|
+
for (const itemId of requirement.acceptableIds) {
|
|
280
|
+
if (checkoutQuantities.has(itemId))
|
|
281
|
+
addEdge(reqNode, `item:${itemId}`, Number.MAX_SAFE_INTEGER);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
for (const [itemId, quantity] of checkoutQuantities) {
|
|
285
|
+
addEdge(`item:${itemId}`, sink, quantity);
|
|
286
|
+
}
|
|
287
|
+
let flow = 0;
|
|
288
|
+
for (;;) {
|
|
289
|
+
const parent = new Map();
|
|
290
|
+
const queue = [source];
|
|
291
|
+
parent.set(source, '');
|
|
292
|
+
for (let i = 0; i < queue.length && !parent.has(sink); i += 1) {
|
|
293
|
+
const node = queue[i];
|
|
294
|
+
for (const [next, capacity] of graph.get(node) ?? []) {
|
|
295
|
+
if (capacity > 0 && !parent.has(next)) {
|
|
296
|
+
parent.set(next, node);
|
|
297
|
+
queue.push(next);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (!parent.has(sink))
|
|
302
|
+
return flow;
|
|
303
|
+
let increment = Number.MAX_SAFE_INTEGER;
|
|
304
|
+
for (let node = sink; node !== source; node = parent.get(node)) {
|
|
305
|
+
const prev = parent.get(node);
|
|
306
|
+
increment = Math.min(increment, graph.get(prev).get(node));
|
|
307
|
+
}
|
|
308
|
+
for (let node = sink; node !== source; node = parent.get(node)) {
|
|
309
|
+
const prev = parent.get(node);
|
|
310
|
+
graph.get(prev).set(node, graph.get(prev).get(node) - increment);
|
|
311
|
+
graph.get(node).set(prev, graph.get(node).get(prev) + increment);
|
|
312
|
+
}
|
|
313
|
+
flow += increment;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function evaluatePaymentConstraint(constraint, closedPaymentMandates, nowSeconds, checkoutPaymentBindingOk, openCheckoutMandateDigests = []) {
|
|
317
|
+
const type = isString(constraint['type']) ? normalizeConstraintType(constraint['type']) : '';
|
|
318
|
+
if (closedPaymentMandates.length === 0) {
|
|
319
|
+
return makeConstraintCheck(constraint, 'payment', 'unresolved', 'closed_payment_missing');
|
|
320
|
+
}
|
|
321
|
+
if (type === 'payment.amount_range') {
|
|
322
|
+
const min = optionalIntegerBound(constraint['min']);
|
|
323
|
+
const max = optionalIntegerBound(constraint['max']);
|
|
324
|
+
if (min === null || max === null) {
|
|
325
|
+
return makeConstraintCheck(constraint, 'payment', 'unresolved', 'amount_range');
|
|
326
|
+
}
|
|
327
|
+
for (const mandate of closedPaymentMandates) {
|
|
328
|
+
const amount = paymentAmount(mandate);
|
|
329
|
+
if (amount === null) {
|
|
330
|
+
return makeConstraintCheck(constraint, 'payment', 'unresolved', 'payment_amount');
|
|
331
|
+
}
|
|
332
|
+
if (isString(constraint['currency']) && constraint['currency'] !== amount.currency) {
|
|
333
|
+
return makeConstraintCheck(constraint, 'payment', 'failed', 'currency');
|
|
334
|
+
}
|
|
335
|
+
if (min !== undefined && amount.amount < min) {
|
|
336
|
+
return makeConstraintCheck(constraint, 'payment', 'failed', 'min');
|
|
337
|
+
}
|
|
338
|
+
if (max !== undefined && amount.amount > max) {
|
|
339
|
+
return makeConstraintCheck(constraint, 'payment', 'failed', 'max');
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return makeConstraintCheck(constraint, 'payment', 'passed');
|
|
343
|
+
}
|
|
344
|
+
if (type === 'payment.allowed_payees') {
|
|
345
|
+
for (const mandate of closedPaymentMandates) {
|
|
346
|
+
const ok = anyEntityMatches(constraint['allowed'], mandate['payee'], [
|
|
347
|
+
'id',
|
|
348
|
+
'website',
|
|
349
|
+
'name',
|
|
350
|
+
]);
|
|
351
|
+
if (ok === null)
|
|
352
|
+
return makeConstraintCheck(constraint, 'payment', 'unresolved', 'allowed');
|
|
353
|
+
if (!ok)
|
|
354
|
+
return makeConstraintCheck(constraint, 'payment', 'failed', 'payee');
|
|
355
|
+
}
|
|
356
|
+
return makeConstraintCheck(constraint, 'payment', 'passed');
|
|
357
|
+
}
|
|
358
|
+
if (type === 'payment.allowed_payment_instruments') {
|
|
359
|
+
for (const mandate of closedPaymentMandates) {
|
|
360
|
+
const ok = anyEntityMatches(constraint['allowed'], mandate['payment_instrument'], [
|
|
361
|
+
'id',
|
|
362
|
+
'type',
|
|
363
|
+
'description',
|
|
364
|
+
]);
|
|
365
|
+
if (ok === null)
|
|
366
|
+
return makeConstraintCheck(constraint, 'payment', 'unresolved', 'allowed');
|
|
367
|
+
if (!ok)
|
|
368
|
+
return makeConstraintCheck(constraint, 'payment', 'failed', 'payment_instrument');
|
|
369
|
+
}
|
|
370
|
+
return makeConstraintCheck(constraint, 'payment', 'passed');
|
|
371
|
+
}
|
|
372
|
+
if (type === 'payment.allowed_pisps') {
|
|
373
|
+
for (const mandate of closedPaymentMandates) {
|
|
374
|
+
const ok = anyEntityMatches(constraint['allowed'], mandate['pisp'], [
|
|
375
|
+
'domain_name',
|
|
376
|
+
'brand_name',
|
|
377
|
+
'legal_name',
|
|
378
|
+
]);
|
|
379
|
+
if (ok === null)
|
|
380
|
+
return makeConstraintCheck(constraint, 'payment', 'unresolved', 'allowed');
|
|
381
|
+
if (!ok)
|
|
382
|
+
return makeConstraintCheck(constraint, 'payment', 'failed', 'pisp');
|
|
383
|
+
}
|
|
384
|
+
return makeConstraintCheck(constraint, 'payment', 'passed');
|
|
385
|
+
}
|
|
386
|
+
if (type === 'payment.reference') {
|
|
387
|
+
const referencedCheckoutDigest = constraint['conditional_transaction_id'];
|
|
388
|
+
if (!isString(referencedCheckoutDigest)) {
|
|
389
|
+
return makeConstraintCheck(constraint, 'payment', 'unresolved', 'conditional_transaction_id');
|
|
390
|
+
}
|
|
391
|
+
if (openCheckoutMandateDigests.length === 0) {
|
|
392
|
+
return makeConstraintCheck(constraint, 'payment', 'unresolved', 'open_checkout_digest');
|
|
393
|
+
}
|
|
394
|
+
if (!openCheckoutMandateDigests.includes(referencedCheckoutDigest)) {
|
|
395
|
+
return makeConstraintCheck(constraint, 'payment', 'failed', 'open_checkout_digest');
|
|
396
|
+
}
|
|
397
|
+
if (checkoutPaymentBindingOk === false) {
|
|
398
|
+
return makeConstraintCheck(constraint, 'payment', 'failed', 'checkout_payment_binding');
|
|
399
|
+
}
|
|
400
|
+
if (checkoutPaymentBindingOk === null || checkoutPaymentBindingOk === undefined) {
|
|
401
|
+
return makeConstraintCheck(constraint, 'payment', 'unresolved', 'checkout_payment_binding');
|
|
402
|
+
}
|
|
403
|
+
return makeConstraintCheck(constraint, 'payment', 'passed');
|
|
404
|
+
}
|
|
405
|
+
if (type === 'payment.execution_date') {
|
|
406
|
+
const immediateExecutionDate = new Date(nowSeconds * 1000).toISOString();
|
|
407
|
+
for (const mandate of closedPaymentMandates) {
|
|
408
|
+
const executionDate = isString(mandate['execution_date'])
|
|
409
|
+
? mandate['execution_date']
|
|
410
|
+
: immediateExecutionDate;
|
|
411
|
+
const ok = compareIsoDate(executionDate, constraint['not_before'], constraint['not_after']);
|
|
412
|
+
if (ok === null)
|
|
413
|
+
return makeConstraintCheck(constraint, 'payment', 'unresolved', 'date');
|
|
414
|
+
if (!ok)
|
|
415
|
+
return makeConstraintCheck(constraint, 'payment', 'failed', 'execution_date');
|
|
416
|
+
}
|
|
417
|
+
return makeConstraintCheck(constraint, 'payment', 'passed');
|
|
418
|
+
}
|
|
419
|
+
return makeConstraintCheck(constraint, 'payment', 'unsupported');
|
|
420
|
+
}
|
|
421
|
+
function evaluateCheckoutConstraint(constraint, checkoutPayload, closedPaymentMandates) {
|
|
422
|
+
const type = isString(constraint['type']) ? normalizeConstraintType(constraint['type']) : '';
|
|
423
|
+
if (type === 'checkout.allowed_merchants') {
|
|
424
|
+
const merchant = checkoutPayload?.['merchant'] ??
|
|
425
|
+
closedPaymentMandates.find((mandate) => mandate['payee'] !== undefined)?.['payee'];
|
|
426
|
+
const ok = anyEntityMatches(constraint['allowed'], merchant, ['id', 'website', 'name']);
|
|
427
|
+
if (ok === null)
|
|
428
|
+
return makeConstraintCheck(constraint, 'checkout', 'unresolved', 'merchant');
|
|
429
|
+
if (!ok)
|
|
430
|
+
return makeConstraintCheck(constraint, 'checkout', 'failed', 'merchant');
|
|
431
|
+
return makeConstraintCheck(constraint, 'checkout', 'passed');
|
|
432
|
+
}
|
|
433
|
+
if (type === 'checkout.line_items') {
|
|
434
|
+
if (!checkoutPayload) {
|
|
435
|
+
return makeConstraintCheck(constraint, 'checkout', 'unresolved', 'checkout_payload_missing');
|
|
436
|
+
}
|
|
437
|
+
const checkoutQuantities = checkoutLineItemQuantities(checkoutPayload);
|
|
438
|
+
const items = constraint['items'];
|
|
439
|
+
if (checkoutQuantities === null || !Array.isArray(items)) {
|
|
440
|
+
return makeConstraintCheck(constraint, 'checkout', 'unresolved', 'line_items');
|
|
441
|
+
}
|
|
442
|
+
const requirements = [];
|
|
443
|
+
for (const item of items) {
|
|
444
|
+
if (!isRecord(item)) {
|
|
445
|
+
return makeConstraintCheck(constraint, 'checkout', 'unresolved', 'requirement');
|
|
446
|
+
}
|
|
447
|
+
const quantity = item['quantity'];
|
|
448
|
+
if (!isSafeInteger(quantity) || quantity < 0) {
|
|
449
|
+
return makeConstraintCheck(constraint, 'checkout', 'unresolved', 'requirement');
|
|
450
|
+
}
|
|
451
|
+
const acceptableItems = item['acceptable_items'];
|
|
452
|
+
if (!Array.isArray(acceptableItems)) {
|
|
453
|
+
return makeConstraintCheck(constraint, 'checkout', 'unresolved', 'acceptable_items');
|
|
454
|
+
}
|
|
455
|
+
const acceptableIds = new Set();
|
|
456
|
+
for (const acceptable of acceptableItems) {
|
|
457
|
+
if (isRecord(acceptable) && isString(acceptable['id']))
|
|
458
|
+
acceptableIds.add(acceptable['id']);
|
|
459
|
+
}
|
|
460
|
+
if (acceptableIds.size === 0) {
|
|
461
|
+
return makeConstraintCheck(constraint, 'checkout', 'unresolved', 'acceptable_items');
|
|
462
|
+
}
|
|
463
|
+
requirements.push({ quantity, acceptableIds });
|
|
464
|
+
}
|
|
465
|
+
const requiredQuantity = requirements.reduce((sum, item) => sum + item.quantity, 0);
|
|
466
|
+
const checkoutQuantity = [...checkoutQuantities.values()].reduce((sum, quantity) => sum + quantity, 0);
|
|
467
|
+
const maxFlow = maxLineItemFlow(requirements, checkoutQuantities);
|
|
468
|
+
if (maxFlow !== requiredQuantity || requiredQuantity !== checkoutQuantity) {
|
|
469
|
+
return makeConstraintCheck(constraint, 'checkout', 'failed', 'line_items');
|
|
470
|
+
}
|
|
471
|
+
return makeConstraintCheck(constraint, 'checkout', 'passed');
|
|
472
|
+
}
|
|
473
|
+
return makeConstraintCheck(constraint, 'checkout', 'unsupported');
|
|
474
|
+
}
|
|
475
|
+
function summarizeConstraintChecks(checks) {
|
|
476
|
+
if (checks.length === 0)
|
|
477
|
+
return 'not_applicable';
|
|
478
|
+
if (checks.some((check) => check.status === 'failed'))
|
|
479
|
+
return 'failed';
|
|
480
|
+
if (checks.some((check) => check.status === 'unresolved' || check.status === 'unsupported')) {
|
|
481
|
+
return 'unresolved';
|
|
482
|
+
}
|
|
483
|
+
return 'passed';
|
|
484
|
+
}
|
|
485
|
+
export function evaluateAp2ViConstraints(input, disclosures = new Map()) {
|
|
486
|
+
const openCheckoutMandates = (input.openCheckoutMandates ?? [])
|
|
487
|
+
.filter(isRecord)
|
|
488
|
+
.map((mandate) => resolveDisclosureRefs(mandate, disclosures))
|
|
489
|
+
.filter(isRecord);
|
|
490
|
+
const closedCheckoutMandates = (input.closedCheckoutMandates ?? [])
|
|
491
|
+
.filter(isRecord)
|
|
492
|
+
.map((mandate) => resolveDisclosureRefs(mandate, disclosures))
|
|
493
|
+
.filter(isRecord);
|
|
494
|
+
const openPaymentMandates = (input.openPaymentMandates ?? [])
|
|
495
|
+
.filter(isRecord)
|
|
496
|
+
.map((mandate) => resolveDisclosureRefs(mandate, disclosures))
|
|
497
|
+
.filter(isRecord);
|
|
498
|
+
const closedPaymentMandates = (input.closedPaymentMandates ?? [])
|
|
499
|
+
.filter(isRecord)
|
|
500
|
+
.map((mandate) => resolveDisclosureRefs(mandate, disclosures))
|
|
501
|
+
.filter(isRecord);
|
|
502
|
+
const resolvedCheckoutPayload = resolveDisclosureRefs(input.checkoutPayload, disclosures);
|
|
503
|
+
const checkoutPayload = resolveCheckoutPayload(resolvedCheckoutPayload, closedCheckoutMandates);
|
|
504
|
+
const nowSeconds = input.nowSeconds ?? Math.floor(Date.now() / 1000);
|
|
505
|
+
const checkoutPaymentBindingOk = input.checkoutPaymentBindingOk;
|
|
506
|
+
const openCheckoutMandateDigests = input.openCheckoutMandateDigests ?? [];
|
|
507
|
+
const checks = [];
|
|
508
|
+
for (const mandate of openCheckoutMandates) {
|
|
509
|
+
for (const constraint of getConstraintObjects(mandate)) {
|
|
510
|
+
const resolved = resolveDisclosureRefs(constraint, disclosures);
|
|
511
|
+
checks.push(evaluateCheckoutConstraint(isRecord(resolved) ? resolved : constraint, checkoutPayload, closedPaymentMandates));
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
for (const mandate of openPaymentMandates) {
|
|
515
|
+
for (const constraint of getConstraintObjects(mandate)) {
|
|
516
|
+
const resolved = resolveDisclosureRefs(constraint, disclosures);
|
|
517
|
+
checks.push(evaluatePaymentConstraint(isRecord(resolved) ? resolved : constraint, closedPaymentMandates, nowSeconds, checkoutPaymentBindingOk, openCheckoutMandateDigests));
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return { status: summarizeConstraintChecks(checks), checks };
|
|
521
|
+
}
|
|
100
522
|
function normalizeJwks(value) {
|
|
101
523
|
const keys = Array.isArray(value) ? value : value.keys;
|
|
524
|
+
if (keys.length === 0)
|
|
525
|
+
throw new Error('jwks_empty');
|
|
526
|
+
const seenKids = new Set();
|
|
527
|
+
for (const key of keys)
|
|
528
|
+
validateReceiptJwk(key, seenKids);
|
|
102
529
|
return { keys: keys };
|
|
103
530
|
}
|
|
104
|
-
function
|
|
531
|
+
function verifyEs256Signature(data, signature, publicJwk) {
|
|
105
532
|
const key = createPublicKey({ key: publicJwk, format: 'jwk' });
|
|
106
|
-
return verifyEcdsa('sha256', Buffer.from(
|
|
533
|
+
return verifyEcdsa('sha256', Buffer.from(data, 'ascii'), { key, dsaEncoding: 'ieee-p1363' }, Buffer.from(signature));
|
|
534
|
+
}
|
|
535
|
+
function verifyJwtSignature(parsed, publicJwk) {
|
|
536
|
+
return verifyEs256Signature(parsed.signingInput, parsed.signature, publicJwk);
|
|
107
537
|
}
|
|
108
538
|
function findIssuerKey(kid, keys) {
|
|
109
539
|
if (kid === null)
|
|
110
540
|
return keys[0];
|
|
111
541
|
return keys.find((key) => getKid(key) === kid);
|
|
112
542
|
}
|
|
543
|
+
function validateReceiptJwk(key, seenKids) {
|
|
544
|
+
const record = key;
|
|
545
|
+
const kid = getKid(record);
|
|
546
|
+
if (kid !== null) {
|
|
547
|
+
if (seenKids.has(kid))
|
|
548
|
+
throw new Error('jwks_duplicate_kid');
|
|
549
|
+
seenKids.add(kid);
|
|
550
|
+
}
|
|
551
|
+
if (record['kty'] !== 'EC' || record['crv'] !== 'P-256') {
|
|
552
|
+
throw new Error('jwks_key_unsupported');
|
|
553
|
+
}
|
|
554
|
+
const alg = record['alg'];
|
|
555
|
+
if (alg !== undefined && alg !== 'ES256') {
|
|
556
|
+
throw new Error('jwks_key_unsupported');
|
|
557
|
+
}
|
|
558
|
+
const use = record['use'];
|
|
559
|
+
if (use !== undefined && use !== 'sig') {
|
|
560
|
+
throw new Error('jwks_key_unsupported');
|
|
561
|
+
}
|
|
562
|
+
const keyOps = record['key_ops'];
|
|
563
|
+
if (keyOps !== undefined &&
|
|
564
|
+
(!Array.isArray(keyOps) || !keyOps.every(isString) || !keyOps.includes('verify'))) {
|
|
565
|
+
throw new Error('jwks_key_unsupported');
|
|
566
|
+
}
|
|
567
|
+
}
|
|
113
568
|
function receiptJwtError(kind, code) {
|
|
114
569
|
return `ap2_${kind}_receipt_jwt_${code}`;
|
|
115
570
|
}
|
|
571
|
+
function normalizeReceiptJwtErrorCode(code) {
|
|
572
|
+
if (code === 'fetch_unavailable' ||
|
|
573
|
+
code === 'metadata_fetch_failed' ||
|
|
574
|
+
code === 'metadata_invalid' ||
|
|
575
|
+
code === 'metadata_jwks_missing' ||
|
|
576
|
+
code === 'key_source_missing' ||
|
|
577
|
+
code === 'jwks_empty' ||
|
|
578
|
+
code === 'jwks_duplicate_kid' ||
|
|
579
|
+
code === 'jwks_key_unsupported' ||
|
|
580
|
+
code === 'iat_in_future') {
|
|
581
|
+
return code === 'fetch_unavailable' ? 'metadata_fetch_unavailable' : code;
|
|
582
|
+
}
|
|
583
|
+
return 'invalid';
|
|
584
|
+
}
|
|
116
585
|
function addReceiptJwtFailure(verification, kind, code, policy) {
|
|
117
586
|
const error = receiptJwtError(kind, code);
|
|
118
587
|
verification.check.error = error;
|
|
@@ -189,6 +658,23 @@ function disclosureDigestsOk(parsed) {
|
|
|
189
658
|
const delegateDigests = collectDelegatePayloadDigests(parsed.payload);
|
|
190
659
|
return parsed.disclosures.every((disclosure) => sdDigests.has(disclosure.digest) || delegateDigests.has(disclosure.digest));
|
|
191
660
|
}
|
|
661
|
+
function credentialStructuralErrors(parsed) {
|
|
662
|
+
const errors = [];
|
|
663
|
+
if (hasDuplicateStrings(parsed.disclosures.map((disclosure) => disclosure.digest))) {
|
|
664
|
+
errors.push('vi_disclosure_duplicate');
|
|
665
|
+
}
|
|
666
|
+
const sdAlg = parsed.payload['_sd_alg'];
|
|
667
|
+
if (sdAlg !== undefined && sdAlg !== 'sha-256') {
|
|
668
|
+
errors.push('vi_sd_alg_unsupported');
|
|
669
|
+
}
|
|
670
|
+
const sd = parsed.payload['_sd'];
|
|
671
|
+
const sdDigests = Array.isArray(sd) ? sd.filter(isString) : [];
|
|
672
|
+
const delegateDigests = collectDelegatePayloadDigestList(parsed.payload);
|
|
673
|
+
if (hasDuplicateStrings(sdDigests) || hasDuplicateStrings(delegateDigests)) {
|
|
674
|
+
errors.push('vi_disclosure_digest_duplicate');
|
|
675
|
+
}
|
|
676
|
+
return errors;
|
|
677
|
+
}
|
|
192
678
|
function initialCredentialCheck(parsed) {
|
|
193
679
|
const alg = isString(parsed.header['alg']) ? parsed.header['alg'] : null;
|
|
194
680
|
const typ = isString(parsed.header['typ']) ? parsed.header['typ'] : null;
|
|
@@ -199,6 +685,7 @@ function initialCredentialCheck(parsed) {
|
|
|
199
685
|
typ,
|
|
200
686
|
kid,
|
|
201
687
|
signature: signatureCheck('not_checked'),
|
|
688
|
+
sdJwtConformance: sdJwtConformanceCheck('not_checked', null, 'async_required'),
|
|
202
689
|
sdHashOk: null,
|
|
203
690
|
disclosuresOk: disclosureDigestsOk(parsed),
|
|
204
691
|
mandateVcts: parsed.mandateValues.map((mandate) => String(mandate['vct'])),
|
|
@@ -264,6 +751,14 @@ async function verifyReceiptJwt(kind, receiptJwt, issuers, policy, nowSeconds, c
|
|
|
264
751
|
addReceiptJwtFailure(verification, kind, 'alg_unsupported', policy);
|
|
265
752
|
return verification;
|
|
266
753
|
}
|
|
754
|
+
if (header.crit !== undefined) {
|
|
755
|
+
addReceiptJwtFailure(verification, kind, 'crit_unsupported', policy);
|
|
756
|
+
return verification;
|
|
757
|
+
}
|
|
758
|
+
if (!isString(header.kid) || header.kid.length === 0) {
|
|
759
|
+
addReceiptJwtFailure(verification, kind, 'kid_missing', policy);
|
|
760
|
+
return verification;
|
|
761
|
+
}
|
|
267
762
|
const issuerConfig = findReceiptJwtIssuer(issuer, issuers);
|
|
268
763
|
if (!issuerConfig) {
|
|
269
764
|
addReceiptJwtFailure(verification, kind, 'issuer_untrusted', policy);
|
|
@@ -281,13 +776,18 @@ async function verifyReceiptJwt(kind, receiptJwt, issuers, policy, nowSeconds, c
|
|
|
281
776
|
if (issuerConfig.issuer !== undefined)
|
|
282
777
|
verifyOptions.issuer = issuerConfig.issuer;
|
|
283
778
|
const verified = await jwtVerify(receiptJwt, key.getKey, verifyOptions);
|
|
779
|
+
const iat = verified.payload.iat;
|
|
780
|
+
if (isNumber(iat) && nowSeconds + clockSkewSeconds < iat) {
|
|
781
|
+
addReceiptJwtFailure(verification, kind, 'iat_in_future', policy);
|
|
782
|
+
return verification;
|
|
783
|
+
}
|
|
284
784
|
verification.check.verified = true;
|
|
285
785
|
verification.payload = verified.payload;
|
|
286
786
|
return verification;
|
|
287
787
|
}
|
|
288
788
|
catch (error) {
|
|
289
789
|
const code = error instanceof Error && error.message ? error.message : 'invalid';
|
|
290
|
-
addReceiptJwtFailure(verification, kind, code
|
|
790
|
+
addReceiptJwtFailure(verification, kind, normalizeReceiptJwtErrorCode(code), policy);
|
|
291
791
|
if (code !== 'invalid' && code !== 'fetch_unavailable') {
|
|
292
792
|
verification.warnings.push(`${receiptJwtError(kind, 'detail')}:${code}`);
|
|
293
793
|
}
|
|
@@ -304,6 +804,11 @@ function expectedReceiptReference(serialized, explicitHash) {
|
|
|
304
804
|
function findMandates(parsedCredentials, vct) {
|
|
305
805
|
return parsedCredentials.flatMap((parsed) => parsed.mandateValues.filter((mandate) => mandate['vct'] === vct));
|
|
306
806
|
}
|
|
807
|
+
function findMandateDigests(parsedCredentials, vct) {
|
|
808
|
+
return parsedCredentials.flatMap((parsed) => parsed.mandateDisclosures
|
|
809
|
+
.filter((mandate) => mandate.value['vct'] === vct)
|
|
810
|
+
.map((mandate) => mandate.digest));
|
|
811
|
+
}
|
|
307
812
|
function checkCheckoutHash(mandates, errors) {
|
|
308
813
|
for (const mandate of mandates) {
|
|
309
814
|
const checkoutJwt = mandate['checkout_jwt'];
|
|
@@ -426,12 +931,176 @@ function checkTimeWindow(parsedCredentials, nowSeconds, clockSkewSeconds, errors
|
|
|
426
931
|
const iat = parsed.payload['iat'];
|
|
427
932
|
if (isNumber(iat) && nowSeconds + clockSkewSeconds < iat)
|
|
428
933
|
errors.push('vi_credential_iat_in_future');
|
|
934
|
+
const nbf = parsed.payload['nbf'];
|
|
935
|
+
if (isNumber(nbf) && nowSeconds + clockSkewSeconds < nbf)
|
|
936
|
+
errors.push('vi_credential_nbf_in_future');
|
|
429
937
|
}
|
|
430
938
|
}
|
|
939
|
+
function sdJwtHasher(data, alg) {
|
|
940
|
+
if (alg !== 'sha-256')
|
|
941
|
+
throw new Error('hash_alg_unsupported');
|
|
942
|
+
const bytes = typeof data === 'string' ? textEncoder.encode(data) : new Uint8Array(data);
|
|
943
|
+
return sha256(bytes);
|
|
944
|
+
}
|
|
945
|
+
function sdJwtVerifier(publicJwk) {
|
|
946
|
+
return (data, signature) => verifyEs256Signature(data, base64urlDecode(signature), publicJwk);
|
|
947
|
+
}
|
|
948
|
+
function credentialVerificationKey(parsed, trustedIssuerKeys, byLayer) {
|
|
949
|
+
const kid = isString(parsed.header['kid']) ? parsed.header['kid'] : null;
|
|
950
|
+
if (parsed.input.layer === 'L1')
|
|
951
|
+
return findIssuerKey(kid, trustedIssuerKeys);
|
|
952
|
+
const l1UserKey = getCnfJwk(byLayer.get('L1')?.payload);
|
|
953
|
+
if (parsed.input.layer === 'L2')
|
|
954
|
+
return l1UserKey;
|
|
955
|
+
const l2AgentKeys = byLayer
|
|
956
|
+
.get('L2')
|
|
957
|
+
?.mandateValues.map(getCnfJwk)
|
|
958
|
+
.filter((jwk) => jwk !== undefined) ?? [];
|
|
959
|
+
return l2AgentKeys.find((candidate) => getKid(candidate) === kid);
|
|
960
|
+
}
|
|
961
|
+
function sdJwtErrorReason(error) {
|
|
962
|
+
const message = error instanceof Error ? error.message.toLowerCase() : '';
|
|
963
|
+
if (message.includes('status_fetcher_unconfigured'))
|
|
964
|
+
return 'status_fetcher';
|
|
965
|
+
if (message.includes('vct_fetcher_unconfigured'))
|
|
966
|
+
return 'vct_fetcher';
|
|
967
|
+
if (message.includes('hash') || message.includes('digest'))
|
|
968
|
+
return 'disclosure_digest';
|
|
969
|
+
if (message.includes('signature'))
|
|
970
|
+
return 'signature';
|
|
971
|
+
if (message.includes('expired') || message.includes('exp'))
|
|
972
|
+
return 'exp';
|
|
973
|
+
if (message.includes('nbf'))
|
|
974
|
+
return 'nbf';
|
|
975
|
+
if (message.includes('iat'))
|
|
976
|
+
return 'iat';
|
|
977
|
+
if (message.includes('key binding') || message.includes('kb'))
|
|
978
|
+
return 'key_binding';
|
|
979
|
+
if (message.includes('vct'))
|
|
980
|
+
return 'vct';
|
|
981
|
+
if (message.includes('status'))
|
|
982
|
+
return 'status';
|
|
983
|
+
return 'invalid';
|
|
984
|
+
}
|
|
985
|
+
function addSdJwtConformanceFinding(policy, code, layer, errors, warnings) {
|
|
986
|
+
if (policy === 'require')
|
|
987
|
+
errors.push(code);
|
|
988
|
+
else
|
|
989
|
+
warnings.push(`${code}:${layer}`);
|
|
990
|
+
}
|
|
991
|
+
function addConstraintFindings(policy, constraints, errors, warnings) {
|
|
992
|
+
for (const check of constraints.checks) {
|
|
993
|
+
let code;
|
|
994
|
+
if (check.status === 'failed')
|
|
995
|
+
code = `vi_constraint_failed:${check.type}`;
|
|
996
|
+
if (check.status === 'unresolved')
|
|
997
|
+
code = `vi_constraint_unresolved:${check.type}`;
|
|
998
|
+
if (check.status === 'unsupported')
|
|
999
|
+
code = `vi_constraint_unsupported:${check.type}`;
|
|
1000
|
+
if (code === undefined)
|
|
1001
|
+
continue;
|
|
1002
|
+
if (policy === 'require')
|
|
1003
|
+
errors.push(code);
|
|
1004
|
+
else
|
|
1005
|
+
warnings.push(code);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
async function verifyCredentialSdJwtConformance(credentials, trustedIssuerKeys, policy, profile, options, nowSeconds, clockSkewSeconds) {
|
|
1009
|
+
const errors = [];
|
|
1010
|
+
const warnings = [];
|
|
1011
|
+
const parsedCredentials = [];
|
|
1012
|
+
const checks = credentials.map(() => sdJwtConformanceCheck('not_checked', profile));
|
|
1013
|
+
for (let index = 0; index < credentials.length; index += 1) {
|
|
1014
|
+
const credential = credentials[index];
|
|
1015
|
+
try {
|
|
1016
|
+
parsedCredentials.push({ index, parsed: parseCredential(credential) });
|
|
1017
|
+
}
|
|
1018
|
+
catch {
|
|
1019
|
+
checks[index] = sdJwtConformanceCheck('invalid', profile, 'parse');
|
|
1020
|
+
addSdJwtConformanceFinding(policy, 'vi_sd_jwt_conformance_invalid', credential.layer, errors, warnings);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
const byLayer = new Map();
|
|
1024
|
+
for (const { parsed } of parsedCredentials)
|
|
1025
|
+
byLayer.set(parsed.input.layer, parsed);
|
|
1026
|
+
for (const { index: checkIndex, parsed } of parsedCredentials) {
|
|
1027
|
+
const key = credentialVerificationKey(parsed, trustedIssuerKeys, byLayer);
|
|
1028
|
+
const structuralErrors = credentialStructuralErrors(parsed);
|
|
1029
|
+
if (parsed.header['alg'] !== 'ES256') {
|
|
1030
|
+
checks[checkIndex] = sdJwtConformanceCheck('invalid', profile, 'alg');
|
|
1031
|
+
addSdJwtConformanceFinding(policy, 'vi_sd_jwt_conformance_invalid', parsed.input.layer, errors, warnings);
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
if (structuralErrors.length > 0) {
|
|
1035
|
+
checks[checkIndex] = sdJwtConformanceCheck('invalid', profile, structuralErrors[0]);
|
|
1036
|
+
addSdJwtConformanceFinding(policy, 'vi_sd_jwt_conformance_invalid', parsed.input.layer, errors, warnings);
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
if (!key) {
|
|
1040
|
+
checks[checkIndex] = sdJwtConformanceCheck('not_checked', profile, 'missing_key');
|
|
1041
|
+
addSdJwtConformanceFinding(policy, 'vi_sd_jwt_conformance_key_missing', parsed.input.layer, errors, warnings);
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
if (disclosureDigestsOk(parsed) === false) {
|
|
1045
|
+
checks[checkIndex] = sdJwtConformanceCheck('invalid', profile, 'disclosure_digest');
|
|
1046
|
+
addSdJwtConformanceFinding(policy, 'vi_sd_jwt_conformance_invalid', parsed.input.layer, errors, warnings);
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
try {
|
|
1050
|
+
const verifier = sdJwtVerifier(key);
|
|
1051
|
+
const verifyOptions = { currentDate: nowSeconds, skewSeconds: clockSkewSeconds };
|
|
1052
|
+
if (profile === 'sd-jwt-vc') {
|
|
1053
|
+
const vcOptions = options.sdJwtVc ?? {};
|
|
1054
|
+
const statusListFetcher = vcOptions.statusListFetcher ??
|
|
1055
|
+
(async () => {
|
|
1056
|
+
throw new Error('status_fetcher_unconfigured');
|
|
1057
|
+
});
|
|
1058
|
+
const vctFetcher = vcOptions.vctFetcher ??
|
|
1059
|
+
(vcOptions.loadTypeMetadata
|
|
1060
|
+
? async () => {
|
|
1061
|
+
throw new Error('vct_fetcher_unconfigured');
|
|
1062
|
+
}
|
|
1063
|
+
: undefined);
|
|
1064
|
+
const vcConfig = {
|
|
1065
|
+
hasher: sdJwtHasher,
|
|
1066
|
+
hashAlg: 'sha-256',
|
|
1067
|
+
verifier,
|
|
1068
|
+
loadTypeMetadataFormat: vcOptions.loadTypeMetadata === true,
|
|
1069
|
+
statusListFetcher,
|
|
1070
|
+
};
|
|
1071
|
+
if (vcOptions.maxVctExtendsDepth !== undefined)
|
|
1072
|
+
Object.assign(vcConfig, { maxVctExtendsDepth: vcOptions.maxVctExtendsDepth });
|
|
1073
|
+
if (vcOptions.statusValidator !== undefined)
|
|
1074
|
+
Object.assign(vcConfig, { statusValidator: vcOptions.statusValidator });
|
|
1075
|
+
if (vctFetcher !== undefined)
|
|
1076
|
+
Object.assign(vcConfig, { vctFetcher });
|
|
1077
|
+
const verifierInstance = new SDJwtVcInstance(vcConfig);
|
|
1078
|
+
await verifierInstance.verify(parsed.input.sdJwt, verifyOptions);
|
|
1079
|
+
await verifierInstance.getClaims(parsed.input.sdJwt);
|
|
1080
|
+
}
|
|
1081
|
+
else {
|
|
1082
|
+
const verifierInstance = new SDJwtInstance({
|
|
1083
|
+
hasher: sdJwtHasher,
|
|
1084
|
+
hashAlg: 'sha-256',
|
|
1085
|
+
verifier,
|
|
1086
|
+
});
|
|
1087
|
+
await verifierInstance.verify(parsed.input.sdJwt, verifyOptions);
|
|
1088
|
+
await verifierInstance.getClaims(parsed.input.sdJwt);
|
|
1089
|
+
}
|
|
1090
|
+
checks[checkIndex] = sdJwtConformanceCheck('verified', profile);
|
|
1091
|
+
}
|
|
1092
|
+
catch (error) {
|
|
1093
|
+
checks[checkIndex] = sdJwtConformanceCheck('invalid', profile, sdJwtErrorReason(error));
|
|
1094
|
+
addSdJwtConformanceFinding(policy, 'vi_sd_jwt_conformance_invalid', parsed.input.layer, errors, warnings);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
return { checks, errors, warnings };
|
|
1098
|
+
}
|
|
431
1099
|
export function verifyAp2ViEvidence(bundle, options = {}) {
|
|
432
1100
|
const errors = [];
|
|
433
1101
|
const warnings = [];
|
|
434
1102
|
const signaturePolicy = options.signaturePolicy ?? 'require';
|
|
1103
|
+
const constraintPolicy = options.constraintPolicy ?? 'require';
|
|
435
1104
|
const trustedIssuerKeys = options.trustedIssuerKeys ?? bundle.trustedIssuerKeys ?? [];
|
|
436
1105
|
const nowSeconds = options.nowSeconds ?? Math.floor(Date.now() / 1000);
|
|
437
1106
|
const clockSkewSeconds = options.clockSkewSeconds ?? 300;
|
|
@@ -447,6 +1116,7 @@ export function verifyAp2ViEvidence(bundle, options = {}) {
|
|
|
447
1116
|
errors.push('vi_typ_mismatch');
|
|
448
1117
|
if (check.disclosuresOk === false)
|
|
449
1118
|
errors.push('vi_disclosure_digest_mismatch');
|
|
1119
|
+
errors.push(...credentialStructuralErrors(parsed));
|
|
450
1120
|
}
|
|
451
1121
|
catch (error) {
|
|
452
1122
|
const code = error instanceof Error && error.message ? error.message : 'vi_jwt_malformed';
|
|
@@ -457,6 +1127,7 @@ export function verifyAp2ViEvidence(bundle, options = {}) {
|
|
|
457
1127
|
typ: null,
|
|
458
1128
|
kid: null,
|
|
459
1129
|
signature: signatureCheck('invalid', 'parse'),
|
|
1130
|
+
sdJwtConformance: sdJwtConformanceCheck('invalid', null, 'parse'),
|
|
460
1131
|
sdHashOk: null,
|
|
461
1132
|
disclosuresOk: null,
|
|
462
1133
|
mandateVcts: [],
|
|
@@ -470,6 +1141,7 @@ export function verifyAp2ViEvidence(bundle, options = {}) {
|
|
|
470
1141
|
const finalCheckoutMandates = findMandates(parsedCredentials, 'mandate.checkout.1');
|
|
471
1142
|
const openPaymentMandates = findMandates(parsedCredentials, 'mandate.payment.open.1');
|
|
472
1143
|
const openCheckoutMandates = findMandates(parsedCredentials, 'mandate.checkout.open.1');
|
|
1144
|
+
const openCheckoutMandateDigests = findMandateDigests(parsedCredentials, 'mandate.checkout.open.1');
|
|
473
1145
|
checkCheckoutHash(finalCheckoutMandates, errors);
|
|
474
1146
|
const checkoutPaymentBindingOk = checkPaymentCheckoutBinding(finalPaymentMandates, finalCheckoutMandates, errors);
|
|
475
1147
|
const delegationOk = checkDelegation(openCheckoutMandates, openPaymentMandates, errors);
|
|
@@ -478,6 +1150,21 @@ export function verifyAp2ViEvidence(bundle, options = {}) {
|
|
|
478
1150
|
: finalPaymentMandates.length > 0 || finalCheckoutMandates.length > 0
|
|
479
1151
|
? 'immediate'
|
|
480
1152
|
: 'unknown';
|
|
1153
|
+
const constraints = constraintPolicy === 'off'
|
|
1154
|
+
? { status: 'not_checked', checks: [] }
|
|
1155
|
+
: evaluateAp2ViConstraints({
|
|
1156
|
+
openCheckoutMandates,
|
|
1157
|
+
openCheckoutMandateDigests,
|
|
1158
|
+
closedCheckoutMandates: finalCheckoutMandates,
|
|
1159
|
+
openPaymentMandates,
|
|
1160
|
+
closedPaymentMandates: finalPaymentMandates,
|
|
1161
|
+
checkoutPayload: bundle.ap2?.checkoutPayload,
|
|
1162
|
+
checkoutPaymentBindingOk,
|
|
1163
|
+
nowSeconds,
|
|
1164
|
+
}, disclosureMap(parsedCredentials));
|
|
1165
|
+
if (constraintPolicy !== 'off') {
|
|
1166
|
+
addConstraintFindings(constraintPolicy, constraints, errors, warnings);
|
|
1167
|
+
}
|
|
481
1168
|
const ap2 = {};
|
|
482
1169
|
const expectedPaymentReference = expectedReceiptReference(bundle.ap2?.closedPaymentMandate, bundle.ap2?.closedPaymentMandateHash);
|
|
483
1170
|
const expectedCheckoutReference = expectedReceiptReference(bundle.ap2?.closedCheckoutMandate, bundle.ap2?.closedCheckoutMandateHash);
|
|
@@ -512,6 +1199,7 @@ export function verifyAp2ViEvidence(bundle, options = {}) {
|
|
|
512
1199
|
credentials: credentialChecks,
|
|
513
1200
|
delegationOk,
|
|
514
1201
|
checkoutPaymentBindingOk,
|
|
1202
|
+
constraints,
|
|
515
1203
|
},
|
|
516
1204
|
errors: [...new Set(errors)],
|
|
517
1205
|
warnings: [...new Set(warnings)],
|
|
@@ -521,10 +1209,14 @@ export async function verifyAp2ViEvidenceAsync(bundle, options = {}) {
|
|
|
521
1209
|
const ap2 = { ...bundle.ap2 };
|
|
522
1210
|
const receiptJwtIssuers = options.receiptJwtIssuers ?? bundle.receiptJwtIssuers ?? [];
|
|
523
1211
|
const receiptJwtPolicy = options.receiptJwtPolicy ?? 'require';
|
|
1212
|
+
const sdJwtConformancePolicy = options.sdJwtConformancePolicy ?? 'require';
|
|
1213
|
+
const sdJwtConformanceProfile = options.sdJwtConformanceProfile ?? 'sd-jwt-vc';
|
|
524
1214
|
const nowSeconds = options.nowSeconds ?? Math.floor(Date.now() / 1000);
|
|
525
1215
|
const clockSkewSeconds = options.clockSkewSeconds ?? 300;
|
|
526
1216
|
const jwtErrors = [];
|
|
527
1217
|
const jwtWarnings = [];
|
|
1218
|
+
const sdJwtErrors = [];
|
|
1219
|
+
const sdJwtWarnings = [];
|
|
528
1220
|
const paymentJwt = ap2.paymentReceiptJwt
|
|
529
1221
|
? await verifyReceiptJwt('payment', ap2.paymentReceiptJwt, receiptJwtIssuers, receiptJwtPolicy, nowSeconds, clockSkewSeconds, options.fetch)
|
|
530
1222
|
: undefined;
|
|
@@ -561,6 +1253,18 @@ export async function verifyAp2ViEvidenceAsync(bundle, options = {}) {
|
|
|
561
1253
|
}
|
|
562
1254
|
}
|
|
563
1255
|
const result = verifyAp2ViEvidence({ ...bundle, ap2 }, options);
|
|
1256
|
+
if (sdJwtConformancePolicy !== 'off') {
|
|
1257
|
+
const credentials = bundle.vi?.credentials ?? [];
|
|
1258
|
+
const trustedIssuerKeys = options.trustedIssuerKeys ?? bundle.trustedIssuerKeys ?? [];
|
|
1259
|
+
const sdJwt = await verifyCredentialSdJwtConformance(credentials, trustedIssuerKeys, sdJwtConformancePolicy, sdJwtConformanceProfile, options, nowSeconds, clockSkewSeconds);
|
|
1260
|
+
for (let index = 0; index < sdJwt.checks.length; index += 1) {
|
|
1261
|
+
const check = result.vi.credentials[index];
|
|
1262
|
+
if (check)
|
|
1263
|
+
check.sdJwtConformance = sdJwt.checks[index];
|
|
1264
|
+
}
|
|
1265
|
+
sdJwtErrors.push(...sdJwt.errors);
|
|
1266
|
+
sdJwtWarnings.push(...sdJwt.warnings);
|
|
1267
|
+
}
|
|
564
1268
|
if (paymentJwt) {
|
|
565
1269
|
if (result.ap2.paymentReceipt)
|
|
566
1270
|
result.ap2.paymentReceipt.jwt = paymentJwt.check;
|
|
@@ -573,8 +1277,8 @@ export async function verifyAp2ViEvidenceAsync(bundle, options = {}) {
|
|
|
573
1277
|
else
|
|
574
1278
|
result.ap2.checkoutReceipt = emptyJwtReceiptCheck(checkoutJwt.check);
|
|
575
1279
|
}
|
|
576
|
-
const errors = [...new Set([...result.errors, ...jwtErrors])];
|
|
577
|
-
const warnings = [...new Set([...result.warnings, ...jwtWarnings])];
|
|
1280
|
+
const errors = [...new Set([...result.errors, ...jwtErrors, ...sdJwtErrors])];
|
|
1281
|
+
const warnings = [...new Set([...result.warnings, ...jwtWarnings, ...sdJwtWarnings])];
|
|
578
1282
|
const hasAp2Receipt = result.ap2.paymentReceipt?.present === true || result.ap2.checkoutReceipt?.present === true;
|
|
579
1283
|
return {
|
|
580
1284
|
...result,
|