@atrib/verify 0.3.7 → 0.4.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 +24 -5
- package/dist/ap2-vi-evidence.d.ts +45 -0
- package/dist/ap2-vi-evidence.d.ts.map +1 -1
- package/dist/ap2-vi-evidence.js +666 -8
- package/dist/ap2-vi-evidence.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -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 +22 -2
- package/dist/verify-record.d.ts.map +1 -1
- package/dist/verify-record.js +60 -13
- 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;
|
|
@@ -97,22 +111,439 @@ function sameJson(left, right) {
|
|
|
97
111
|
const rightCanonical = canonicalJson(right);
|
|
98
112
|
return leftCanonical !== null && leftCanonical === rightCanonical;
|
|
99
113
|
}
|
|
114
|
+
function getConstraintObjects(mandate) {
|
|
115
|
+
const constraints = mandate['constraints'];
|
|
116
|
+
if (!Array.isArray(constraints))
|
|
117
|
+
return [];
|
|
118
|
+
return constraints.filter(isRecord);
|
|
119
|
+
}
|
|
120
|
+
function normalizeConstraintType(value) {
|
|
121
|
+
return value.startsWith('mandate.') ? value.slice('mandate.'.length) : value;
|
|
122
|
+
}
|
|
123
|
+
function makeConstraintCheck(constraint, domain, status, reason) {
|
|
124
|
+
const type = isString(constraint['type']) ? constraint['type'] : `${domain}.unknown`;
|
|
125
|
+
return reason === undefined ? { type, domain, status } : { type, domain, status, reason };
|
|
126
|
+
}
|
|
127
|
+
function resolveDisclosureRefs(value, disclosures) {
|
|
128
|
+
if (Array.isArray(value))
|
|
129
|
+
return value.map((item) => resolveDisclosureRefs(item, disclosures));
|
|
130
|
+
if (!isRecord(value))
|
|
131
|
+
return value;
|
|
132
|
+
const digest = value['...'];
|
|
133
|
+
if (isString(digest) && Object.keys(value).length === 1) {
|
|
134
|
+
return disclosures.has(digest)
|
|
135
|
+
? resolveDisclosureRefs(disclosures.get(digest), disclosures)
|
|
136
|
+
: value;
|
|
137
|
+
}
|
|
138
|
+
const resolved = {};
|
|
139
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
140
|
+
resolved[key] = resolveDisclosureRefs(nested, disclosures);
|
|
141
|
+
}
|
|
142
|
+
return resolved;
|
|
143
|
+
}
|
|
144
|
+
function disclosureMap(parsedCredentials) {
|
|
145
|
+
const disclosures = new Map();
|
|
146
|
+
for (const parsed of parsedCredentials) {
|
|
147
|
+
for (const disclosure of parsed.disclosures) {
|
|
148
|
+
disclosures.set(disclosure.digest, disclosure.value);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return disclosures;
|
|
152
|
+
}
|
|
153
|
+
function comparableFields(left, right, fields) {
|
|
154
|
+
return fields.filter((field) => isString(left[field]) && isString(right[field]));
|
|
155
|
+
}
|
|
156
|
+
function entityMatches(left, right, fields) {
|
|
157
|
+
if (!isRecord(left) || !isRecord(right))
|
|
158
|
+
return false;
|
|
159
|
+
const fieldsToCompare = comparableFields(left, right, fields);
|
|
160
|
+
return (fieldsToCompare.length > 0 && fieldsToCompare.every((field) => left[field] === right[field]));
|
|
161
|
+
}
|
|
162
|
+
function anyEntityMatches(allowed, actual, fields) {
|
|
163
|
+
if (!Array.isArray(allowed))
|
|
164
|
+
return null;
|
|
165
|
+
if (!isRecord(actual))
|
|
166
|
+
return null;
|
|
167
|
+
const allowedRecords = allowed.filter(isRecord);
|
|
168
|
+
if (allowedRecords.length === 0)
|
|
169
|
+
return null;
|
|
170
|
+
return allowedRecords.some((candidate) => entityMatches(candidate, actual, fields));
|
|
171
|
+
}
|
|
172
|
+
function paymentAmount(target) {
|
|
173
|
+
const amount = target['payment_amount'];
|
|
174
|
+
if (!isRecord(amount) || !isSafeInteger(amount['amount']) || !isString(amount['currency'])) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
return { amount: amount['amount'], currency: amount['currency'] };
|
|
178
|
+
}
|
|
179
|
+
function optionalIntegerBound(value) {
|
|
180
|
+
if (value === undefined)
|
|
181
|
+
return undefined;
|
|
182
|
+
return isSafeInteger(value) ? value : null;
|
|
183
|
+
}
|
|
184
|
+
function compareIsoDate(value, lower, upper) {
|
|
185
|
+
const time = Date.parse(value);
|
|
186
|
+
if (!Number.isFinite(time))
|
|
187
|
+
return null;
|
|
188
|
+
if (isString(lower)) {
|
|
189
|
+
const lowerTime = Date.parse(lower);
|
|
190
|
+
if (!Number.isFinite(lowerTime) || time < lowerTime)
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
if (isString(upper)) {
|
|
194
|
+
const upperTime = Date.parse(upper);
|
|
195
|
+
if (!Number.isFinite(upperTime) || time > upperTime)
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
function decodeCompactJwtPayload(value) {
|
|
201
|
+
try {
|
|
202
|
+
const payload = decodeJwt(value);
|
|
203
|
+
return isRecord(payload) ? payload : undefined;
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function resolveCheckoutPayload(explicitPayload, closedCheckoutMandates) {
|
|
210
|
+
if (isRecord(explicitPayload))
|
|
211
|
+
return explicitPayload;
|
|
212
|
+
for (const mandate of closedCheckoutMandates) {
|
|
213
|
+
const payload = mandate['checkout_payload'];
|
|
214
|
+
if (isRecord(payload))
|
|
215
|
+
return payload;
|
|
216
|
+
const checkoutJwt = mandate['checkout_jwt'];
|
|
217
|
+
if (isString(checkoutJwt)) {
|
|
218
|
+
const decoded = decodeCompactJwtPayload(checkoutJwt);
|
|
219
|
+
if (decoded)
|
|
220
|
+
return decoded;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
function checkoutLineItemId(value) {
|
|
226
|
+
const product = value['product'];
|
|
227
|
+
if (isRecord(product) && isString(product['id']))
|
|
228
|
+
return product['id'];
|
|
229
|
+
return isString(value['id']) ? value['id'] : null;
|
|
230
|
+
}
|
|
231
|
+
function checkoutLineItemQuantities(checkoutPayload) {
|
|
232
|
+
const items = checkoutPayload['line_items'];
|
|
233
|
+
if (!Array.isArray(items))
|
|
234
|
+
return null;
|
|
235
|
+
const quantities = new Map();
|
|
236
|
+
for (const item of items) {
|
|
237
|
+
if (!isRecord(item))
|
|
238
|
+
return null;
|
|
239
|
+
const id = checkoutLineItemId(item);
|
|
240
|
+
const quantity = item['quantity'];
|
|
241
|
+
if (id === null || !isSafeInteger(quantity) || quantity < 0)
|
|
242
|
+
return null;
|
|
243
|
+
quantities.set(id, (quantities.get(id) ?? 0) + quantity);
|
|
244
|
+
}
|
|
245
|
+
return quantities;
|
|
246
|
+
}
|
|
247
|
+
function maxLineItemFlow(requirements, checkoutQuantities) {
|
|
248
|
+
const source = 'source';
|
|
249
|
+
const sink = 'sink';
|
|
250
|
+
const graph = new Map();
|
|
251
|
+
const addEdge = (from, to, capacity) => {
|
|
252
|
+
if (!graph.has(from))
|
|
253
|
+
graph.set(from, new Map());
|
|
254
|
+
if (!graph.has(to))
|
|
255
|
+
graph.set(to, new Map());
|
|
256
|
+
graph.get(from).set(to, (graph.get(from).get(to) ?? 0) + capacity);
|
|
257
|
+
graph.get(to).set(from, graph.get(to).get(from) ?? 0);
|
|
258
|
+
};
|
|
259
|
+
requirements.forEach((requirement, index) => {
|
|
260
|
+
const reqNode = `req:${index}`;
|
|
261
|
+
addEdge(source, reqNode, requirement.quantity);
|
|
262
|
+
for (const itemId of requirement.acceptableIds) {
|
|
263
|
+
if (checkoutQuantities.has(itemId))
|
|
264
|
+
addEdge(reqNode, `item:${itemId}`, Number.MAX_SAFE_INTEGER);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
for (const [itemId, quantity] of checkoutQuantities) {
|
|
268
|
+
addEdge(`item:${itemId}`, sink, quantity);
|
|
269
|
+
}
|
|
270
|
+
let flow = 0;
|
|
271
|
+
for (;;) {
|
|
272
|
+
const parent = new Map();
|
|
273
|
+
const queue = [source];
|
|
274
|
+
parent.set(source, '');
|
|
275
|
+
for (let i = 0; i < queue.length && !parent.has(sink); i += 1) {
|
|
276
|
+
const node = queue[i];
|
|
277
|
+
for (const [next, capacity] of graph.get(node) ?? []) {
|
|
278
|
+
if (capacity > 0 && !parent.has(next)) {
|
|
279
|
+
parent.set(next, node);
|
|
280
|
+
queue.push(next);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (!parent.has(sink))
|
|
285
|
+
return flow;
|
|
286
|
+
let increment = Number.MAX_SAFE_INTEGER;
|
|
287
|
+
for (let node = sink; node !== source; node = parent.get(node)) {
|
|
288
|
+
const prev = parent.get(node);
|
|
289
|
+
increment = Math.min(increment, graph.get(prev).get(node));
|
|
290
|
+
}
|
|
291
|
+
for (let node = sink; node !== source; node = parent.get(node)) {
|
|
292
|
+
const prev = parent.get(node);
|
|
293
|
+
graph.get(prev).set(node, graph.get(prev).get(node) - increment);
|
|
294
|
+
graph.get(node).set(prev, graph.get(node).get(prev) + increment);
|
|
295
|
+
}
|
|
296
|
+
flow += increment;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function evaluatePaymentConstraint(constraint, closedPaymentMandates, nowSeconds) {
|
|
300
|
+
const type = isString(constraint['type']) ? normalizeConstraintType(constraint['type']) : '';
|
|
301
|
+
if (closedPaymentMandates.length === 0) {
|
|
302
|
+
return makeConstraintCheck(constraint, 'payment', 'unresolved', 'closed_payment_missing');
|
|
303
|
+
}
|
|
304
|
+
if (type === 'payment.amount_range') {
|
|
305
|
+
const min = optionalIntegerBound(constraint['min']);
|
|
306
|
+
const max = optionalIntegerBound(constraint['max']);
|
|
307
|
+
if (min === null || max === null) {
|
|
308
|
+
return makeConstraintCheck(constraint, 'payment', 'unresolved', 'amount_range');
|
|
309
|
+
}
|
|
310
|
+
for (const mandate of closedPaymentMandates) {
|
|
311
|
+
const amount = paymentAmount(mandate);
|
|
312
|
+
if (amount === null) {
|
|
313
|
+
return makeConstraintCheck(constraint, 'payment', 'unresolved', 'payment_amount');
|
|
314
|
+
}
|
|
315
|
+
if (isString(constraint['currency']) && constraint['currency'] !== amount.currency) {
|
|
316
|
+
return makeConstraintCheck(constraint, 'payment', 'failed', 'currency');
|
|
317
|
+
}
|
|
318
|
+
if (min !== undefined && amount.amount < min) {
|
|
319
|
+
return makeConstraintCheck(constraint, 'payment', 'failed', 'min');
|
|
320
|
+
}
|
|
321
|
+
if (max !== undefined && amount.amount > max) {
|
|
322
|
+
return makeConstraintCheck(constraint, 'payment', 'failed', 'max');
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return makeConstraintCheck(constraint, 'payment', 'passed');
|
|
326
|
+
}
|
|
327
|
+
if (type === 'payment.allowed_payees') {
|
|
328
|
+
for (const mandate of closedPaymentMandates) {
|
|
329
|
+
const ok = anyEntityMatches(constraint['allowed'], mandate['payee'], [
|
|
330
|
+
'id',
|
|
331
|
+
'website',
|
|
332
|
+
'name',
|
|
333
|
+
]);
|
|
334
|
+
if (ok === null)
|
|
335
|
+
return makeConstraintCheck(constraint, 'payment', 'unresolved', 'allowed');
|
|
336
|
+
if (!ok)
|
|
337
|
+
return makeConstraintCheck(constraint, 'payment', 'failed', 'payee');
|
|
338
|
+
}
|
|
339
|
+
return makeConstraintCheck(constraint, 'payment', 'passed');
|
|
340
|
+
}
|
|
341
|
+
if (type === 'payment.allowed_payment_instruments') {
|
|
342
|
+
for (const mandate of closedPaymentMandates) {
|
|
343
|
+
const ok = anyEntityMatches(constraint['allowed'], mandate['payment_instrument'], [
|
|
344
|
+
'id',
|
|
345
|
+
'type',
|
|
346
|
+
'description',
|
|
347
|
+
]);
|
|
348
|
+
if (ok === null)
|
|
349
|
+
return makeConstraintCheck(constraint, 'payment', 'unresolved', 'allowed');
|
|
350
|
+
if (!ok)
|
|
351
|
+
return makeConstraintCheck(constraint, 'payment', 'failed', 'payment_instrument');
|
|
352
|
+
}
|
|
353
|
+
return makeConstraintCheck(constraint, 'payment', 'passed');
|
|
354
|
+
}
|
|
355
|
+
if (type === 'payment.allowed_pisps') {
|
|
356
|
+
for (const mandate of closedPaymentMandates) {
|
|
357
|
+
const ok = anyEntityMatches(constraint['allowed'], mandate['pisp'], [
|
|
358
|
+
'domain_name',
|
|
359
|
+
'brand_name',
|
|
360
|
+
'legal_name',
|
|
361
|
+
]);
|
|
362
|
+
if (ok === null)
|
|
363
|
+
return makeConstraintCheck(constraint, 'payment', 'unresolved', 'allowed');
|
|
364
|
+
if (!ok)
|
|
365
|
+
return makeConstraintCheck(constraint, 'payment', 'failed', 'pisp');
|
|
366
|
+
}
|
|
367
|
+
return makeConstraintCheck(constraint, 'payment', 'passed');
|
|
368
|
+
}
|
|
369
|
+
if (type === 'payment.execution_date') {
|
|
370
|
+
const immediateExecutionDate = new Date(nowSeconds * 1000).toISOString();
|
|
371
|
+
for (const mandate of closedPaymentMandates) {
|
|
372
|
+
const executionDate = isString(mandate['execution_date'])
|
|
373
|
+
? mandate['execution_date']
|
|
374
|
+
: immediateExecutionDate;
|
|
375
|
+
const ok = compareIsoDate(executionDate, constraint['not_before'], constraint['not_after']);
|
|
376
|
+
if (ok === null)
|
|
377
|
+
return makeConstraintCheck(constraint, 'payment', 'unresolved', 'date');
|
|
378
|
+
if (!ok)
|
|
379
|
+
return makeConstraintCheck(constraint, 'payment', 'failed', 'execution_date');
|
|
380
|
+
}
|
|
381
|
+
return makeConstraintCheck(constraint, 'payment', 'passed');
|
|
382
|
+
}
|
|
383
|
+
return makeConstraintCheck(constraint, 'payment', 'unsupported');
|
|
384
|
+
}
|
|
385
|
+
function evaluateCheckoutConstraint(constraint, checkoutPayload, closedPaymentMandates) {
|
|
386
|
+
const type = isString(constraint['type']) ? normalizeConstraintType(constraint['type']) : '';
|
|
387
|
+
if (type === 'checkout.allowed_merchants') {
|
|
388
|
+
const merchant = checkoutPayload?.['merchant'] ??
|
|
389
|
+
closedPaymentMandates.find((mandate) => mandate['payee'] !== undefined)?.['payee'];
|
|
390
|
+
const ok = anyEntityMatches(constraint['allowed'], merchant, ['id', 'website', 'name']);
|
|
391
|
+
if (ok === null)
|
|
392
|
+
return makeConstraintCheck(constraint, 'checkout', 'unresolved', 'merchant');
|
|
393
|
+
if (!ok)
|
|
394
|
+
return makeConstraintCheck(constraint, 'checkout', 'failed', 'merchant');
|
|
395
|
+
return makeConstraintCheck(constraint, 'checkout', 'passed');
|
|
396
|
+
}
|
|
397
|
+
if (type === 'checkout.line_items') {
|
|
398
|
+
if (!checkoutPayload) {
|
|
399
|
+
return makeConstraintCheck(constraint, 'checkout', 'unresolved', 'checkout_payload_missing');
|
|
400
|
+
}
|
|
401
|
+
const checkoutQuantities = checkoutLineItemQuantities(checkoutPayload);
|
|
402
|
+
const items = constraint['items'];
|
|
403
|
+
if (checkoutQuantities === null || !Array.isArray(items)) {
|
|
404
|
+
return makeConstraintCheck(constraint, 'checkout', 'unresolved', 'line_items');
|
|
405
|
+
}
|
|
406
|
+
const requirements = [];
|
|
407
|
+
for (const item of items) {
|
|
408
|
+
if (!isRecord(item)) {
|
|
409
|
+
return makeConstraintCheck(constraint, 'checkout', 'unresolved', 'requirement');
|
|
410
|
+
}
|
|
411
|
+
const quantity = item['quantity'];
|
|
412
|
+
if (!isSafeInteger(quantity) || quantity < 0) {
|
|
413
|
+
return makeConstraintCheck(constraint, 'checkout', 'unresolved', 'requirement');
|
|
414
|
+
}
|
|
415
|
+
const acceptableItems = item['acceptable_items'];
|
|
416
|
+
if (!Array.isArray(acceptableItems)) {
|
|
417
|
+
return makeConstraintCheck(constraint, 'checkout', 'unresolved', 'acceptable_items');
|
|
418
|
+
}
|
|
419
|
+
const acceptableIds = new Set();
|
|
420
|
+
for (const acceptable of acceptableItems) {
|
|
421
|
+
if (isRecord(acceptable) && isString(acceptable['id']))
|
|
422
|
+
acceptableIds.add(acceptable['id']);
|
|
423
|
+
}
|
|
424
|
+
if (acceptableIds.size === 0) {
|
|
425
|
+
return makeConstraintCheck(constraint, 'checkout', 'unresolved', 'acceptable_items');
|
|
426
|
+
}
|
|
427
|
+
requirements.push({ quantity, acceptableIds });
|
|
428
|
+
}
|
|
429
|
+
const requiredQuantity = requirements.reduce((sum, item) => sum + item.quantity, 0);
|
|
430
|
+
const checkoutQuantity = [...checkoutQuantities.values()].reduce((sum, quantity) => sum + quantity, 0);
|
|
431
|
+
const maxFlow = maxLineItemFlow(requirements, checkoutQuantities);
|
|
432
|
+
if (maxFlow !== requiredQuantity || requiredQuantity !== checkoutQuantity) {
|
|
433
|
+
return makeConstraintCheck(constraint, 'checkout', 'failed', 'line_items');
|
|
434
|
+
}
|
|
435
|
+
return makeConstraintCheck(constraint, 'checkout', 'passed');
|
|
436
|
+
}
|
|
437
|
+
return makeConstraintCheck(constraint, 'checkout', 'unsupported');
|
|
438
|
+
}
|
|
439
|
+
function summarizeConstraintChecks(checks) {
|
|
440
|
+
if (checks.length === 0)
|
|
441
|
+
return 'not_applicable';
|
|
442
|
+
if (checks.some((check) => check.status === 'failed'))
|
|
443
|
+
return 'failed';
|
|
444
|
+
if (checks.some((check) => check.status === 'unresolved' || check.status === 'unsupported')) {
|
|
445
|
+
return 'unresolved';
|
|
446
|
+
}
|
|
447
|
+
return 'passed';
|
|
448
|
+
}
|
|
449
|
+
export function evaluateAp2ViConstraints(input, disclosures = new Map()) {
|
|
450
|
+
const openCheckoutMandates = (input.openCheckoutMandates ?? [])
|
|
451
|
+
.filter(isRecord)
|
|
452
|
+
.map((mandate) => resolveDisclosureRefs(mandate, disclosures))
|
|
453
|
+
.filter(isRecord);
|
|
454
|
+
const closedCheckoutMandates = (input.closedCheckoutMandates ?? [])
|
|
455
|
+
.filter(isRecord)
|
|
456
|
+
.map((mandate) => resolveDisclosureRefs(mandate, disclosures))
|
|
457
|
+
.filter(isRecord);
|
|
458
|
+
const openPaymentMandates = (input.openPaymentMandates ?? [])
|
|
459
|
+
.filter(isRecord)
|
|
460
|
+
.map((mandate) => resolveDisclosureRefs(mandate, disclosures))
|
|
461
|
+
.filter(isRecord);
|
|
462
|
+
const closedPaymentMandates = (input.closedPaymentMandates ?? [])
|
|
463
|
+
.filter(isRecord)
|
|
464
|
+
.map((mandate) => resolveDisclosureRefs(mandate, disclosures))
|
|
465
|
+
.filter(isRecord);
|
|
466
|
+
const resolvedCheckoutPayload = resolveDisclosureRefs(input.checkoutPayload, disclosures);
|
|
467
|
+
const checkoutPayload = resolveCheckoutPayload(resolvedCheckoutPayload, closedCheckoutMandates);
|
|
468
|
+
const nowSeconds = input.nowSeconds ?? Math.floor(Date.now() / 1000);
|
|
469
|
+
const checks = [];
|
|
470
|
+
for (const mandate of openCheckoutMandates) {
|
|
471
|
+
for (const constraint of getConstraintObjects(mandate)) {
|
|
472
|
+
const resolved = resolveDisclosureRefs(constraint, disclosures);
|
|
473
|
+
checks.push(evaluateCheckoutConstraint(isRecord(resolved) ? resolved : constraint, checkoutPayload, closedPaymentMandates));
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
for (const mandate of openPaymentMandates) {
|
|
477
|
+
for (const constraint of getConstraintObjects(mandate)) {
|
|
478
|
+
const resolved = resolveDisclosureRefs(constraint, disclosures);
|
|
479
|
+
checks.push(evaluatePaymentConstraint(isRecord(resolved) ? resolved : constraint, closedPaymentMandates, nowSeconds));
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return { status: summarizeConstraintChecks(checks), checks };
|
|
483
|
+
}
|
|
100
484
|
function normalizeJwks(value) {
|
|
101
485
|
const keys = Array.isArray(value) ? value : value.keys;
|
|
486
|
+
if (keys.length === 0)
|
|
487
|
+
throw new Error('jwks_empty');
|
|
488
|
+
const seenKids = new Set();
|
|
489
|
+
for (const key of keys)
|
|
490
|
+
validateReceiptJwk(key, seenKids);
|
|
102
491
|
return { keys: keys };
|
|
103
492
|
}
|
|
104
|
-
function
|
|
493
|
+
function verifyEs256Signature(data, signature, publicJwk) {
|
|
105
494
|
const key = createPublicKey({ key: publicJwk, format: 'jwk' });
|
|
106
|
-
return verifyEcdsa('sha256', Buffer.from(
|
|
495
|
+
return verifyEcdsa('sha256', Buffer.from(data, 'ascii'), { key, dsaEncoding: 'ieee-p1363' }, Buffer.from(signature));
|
|
496
|
+
}
|
|
497
|
+
function verifyJwtSignature(parsed, publicJwk) {
|
|
498
|
+
return verifyEs256Signature(parsed.signingInput, parsed.signature, publicJwk);
|
|
107
499
|
}
|
|
108
500
|
function findIssuerKey(kid, keys) {
|
|
109
501
|
if (kid === null)
|
|
110
502
|
return keys[0];
|
|
111
503
|
return keys.find((key) => getKid(key) === kid);
|
|
112
504
|
}
|
|
505
|
+
function validateReceiptJwk(key, seenKids) {
|
|
506
|
+
const record = key;
|
|
507
|
+
const kid = getKid(record);
|
|
508
|
+
if (kid !== null) {
|
|
509
|
+
if (seenKids.has(kid))
|
|
510
|
+
throw new Error('jwks_duplicate_kid');
|
|
511
|
+
seenKids.add(kid);
|
|
512
|
+
}
|
|
513
|
+
if (record['kty'] !== 'EC' || record['crv'] !== 'P-256') {
|
|
514
|
+
throw new Error('jwks_key_unsupported');
|
|
515
|
+
}
|
|
516
|
+
const alg = record['alg'];
|
|
517
|
+
if (alg !== undefined && alg !== 'ES256') {
|
|
518
|
+
throw new Error('jwks_key_unsupported');
|
|
519
|
+
}
|
|
520
|
+
const use = record['use'];
|
|
521
|
+
if (use !== undefined && use !== 'sig') {
|
|
522
|
+
throw new Error('jwks_key_unsupported');
|
|
523
|
+
}
|
|
524
|
+
const keyOps = record['key_ops'];
|
|
525
|
+
if (keyOps !== undefined &&
|
|
526
|
+
(!Array.isArray(keyOps) || !keyOps.every(isString) || !keyOps.includes('verify'))) {
|
|
527
|
+
throw new Error('jwks_key_unsupported');
|
|
528
|
+
}
|
|
529
|
+
}
|
|
113
530
|
function receiptJwtError(kind, code) {
|
|
114
531
|
return `ap2_${kind}_receipt_jwt_${code}`;
|
|
115
532
|
}
|
|
533
|
+
function normalizeReceiptJwtErrorCode(code) {
|
|
534
|
+
if (code === 'fetch_unavailable' ||
|
|
535
|
+
code === 'metadata_fetch_failed' ||
|
|
536
|
+
code === 'metadata_invalid' ||
|
|
537
|
+
code === 'metadata_jwks_missing' ||
|
|
538
|
+
code === 'key_source_missing' ||
|
|
539
|
+
code === 'jwks_empty' ||
|
|
540
|
+
code === 'jwks_duplicate_kid' ||
|
|
541
|
+
code === 'jwks_key_unsupported' ||
|
|
542
|
+
code === 'iat_in_future') {
|
|
543
|
+
return code === 'fetch_unavailable' ? 'metadata_fetch_unavailable' : code;
|
|
544
|
+
}
|
|
545
|
+
return 'invalid';
|
|
546
|
+
}
|
|
116
547
|
function addReceiptJwtFailure(verification, kind, code, policy) {
|
|
117
548
|
const error = receiptJwtError(kind, code);
|
|
118
549
|
verification.check.error = error;
|
|
@@ -189,6 +620,23 @@ function disclosureDigestsOk(parsed) {
|
|
|
189
620
|
const delegateDigests = collectDelegatePayloadDigests(parsed.payload);
|
|
190
621
|
return parsed.disclosures.every((disclosure) => sdDigests.has(disclosure.digest) || delegateDigests.has(disclosure.digest));
|
|
191
622
|
}
|
|
623
|
+
function credentialStructuralErrors(parsed) {
|
|
624
|
+
const errors = [];
|
|
625
|
+
if (hasDuplicateStrings(parsed.disclosures.map((disclosure) => disclosure.digest))) {
|
|
626
|
+
errors.push('vi_disclosure_duplicate');
|
|
627
|
+
}
|
|
628
|
+
const sdAlg = parsed.payload['_sd_alg'];
|
|
629
|
+
if (sdAlg !== undefined && sdAlg !== 'sha-256') {
|
|
630
|
+
errors.push('vi_sd_alg_unsupported');
|
|
631
|
+
}
|
|
632
|
+
const sd = parsed.payload['_sd'];
|
|
633
|
+
const sdDigests = Array.isArray(sd) ? sd.filter(isString) : [];
|
|
634
|
+
const delegateDigests = collectDelegatePayloadDigestList(parsed.payload);
|
|
635
|
+
if (hasDuplicateStrings(sdDigests) || hasDuplicateStrings(delegateDigests)) {
|
|
636
|
+
errors.push('vi_disclosure_digest_duplicate');
|
|
637
|
+
}
|
|
638
|
+
return errors;
|
|
639
|
+
}
|
|
192
640
|
function initialCredentialCheck(parsed) {
|
|
193
641
|
const alg = isString(parsed.header['alg']) ? parsed.header['alg'] : null;
|
|
194
642
|
const typ = isString(parsed.header['typ']) ? parsed.header['typ'] : null;
|
|
@@ -199,6 +647,7 @@ function initialCredentialCheck(parsed) {
|
|
|
199
647
|
typ,
|
|
200
648
|
kid,
|
|
201
649
|
signature: signatureCheck('not_checked'),
|
|
650
|
+
sdJwtConformance: sdJwtConformanceCheck('not_checked', null, 'async_required'),
|
|
202
651
|
sdHashOk: null,
|
|
203
652
|
disclosuresOk: disclosureDigestsOk(parsed),
|
|
204
653
|
mandateVcts: parsed.mandateValues.map((mandate) => String(mandate['vct'])),
|
|
@@ -264,6 +713,14 @@ async function verifyReceiptJwt(kind, receiptJwt, issuers, policy, nowSeconds, c
|
|
|
264
713
|
addReceiptJwtFailure(verification, kind, 'alg_unsupported', policy);
|
|
265
714
|
return verification;
|
|
266
715
|
}
|
|
716
|
+
if (header.crit !== undefined) {
|
|
717
|
+
addReceiptJwtFailure(verification, kind, 'crit_unsupported', policy);
|
|
718
|
+
return verification;
|
|
719
|
+
}
|
|
720
|
+
if (!isString(header.kid) || header.kid.length === 0) {
|
|
721
|
+
addReceiptJwtFailure(verification, kind, 'kid_missing', policy);
|
|
722
|
+
return verification;
|
|
723
|
+
}
|
|
267
724
|
const issuerConfig = findReceiptJwtIssuer(issuer, issuers);
|
|
268
725
|
if (!issuerConfig) {
|
|
269
726
|
addReceiptJwtFailure(verification, kind, 'issuer_untrusted', policy);
|
|
@@ -281,13 +738,18 @@ async function verifyReceiptJwt(kind, receiptJwt, issuers, policy, nowSeconds, c
|
|
|
281
738
|
if (issuerConfig.issuer !== undefined)
|
|
282
739
|
verifyOptions.issuer = issuerConfig.issuer;
|
|
283
740
|
const verified = await jwtVerify(receiptJwt, key.getKey, verifyOptions);
|
|
741
|
+
const iat = verified.payload.iat;
|
|
742
|
+
if (isNumber(iat) && nowSeconds + clockSkewSeconds < iat) {
|
|
743
|
+
addReceiptJwtFailure(verification, kind, 'iat_in_future', policy);
|
|
744
|
+
return verification;
|
|
745
|
+
}
|
|
284
746
|
verification.check.verified = true;
|
|
285
747
|
verification.payload = verified.payload;
|
|
286
748
|
return verification;
|
|
287
749
|
}
|
|
288
750
|
catch (error) {
|
|
289
751
|
const code = error instanceof Error && error.message ? error.message : 'invalid';
|
|
290
|
-
addReceiptJwtFailure(verification, kind, code
|
|
752
|
+
addReceiptJwtFailure(verification, kind, normalizeReceiptJwtErrorCode(code), policy);
|
|
291
753
|
if (code !== 'invalid' && code !== 'fetch_unavailable') {
|
|
292
754
|
verification.warnings.push(`${receiptJwtError(kind, 'detail')}:${code}`);
|
|
293
755
|
}
|
|
@@ -426,12 +888,176 @@ function checkTimeWindow(parsedCredentials, nowSeconds, clockSkewSeconds, errors
|
|
|
426
888
|
const iat = parsed.payload['iat'];
|
|
427
889
|
if (isNumber(iat) && nowSeconds + clockSkewSeconds < iat)
|
|
428
890
|
errors.push('vi_credential_iat_in_future');
|
|
891
|
+
const nbf = parsed.payload['nbf'];
|
|
892
|
+
if (isNumber(nbf) && nowSeconds + clockSkewSeconds < nbf)
|
|
893
|
+
errors.push('vi_credential_nbf_in_future');
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
function sdJwtHasher(data, alg) {
|
|
897
|
+
if (alg !== 'sha-256')
|
|
898
|
+
throw new Error('hash_alg_unsupported');
|
|
899
|
+
const bytes = typeof data === 'string' ? textEncoder.encode(data) : new Uint8Array(data);
|
|
900
|
+
return sha256(bytes);
|
|
901
|
+
}
|
|
902
|
+
function sdJwtVerifier(publicJwk) {
|
|
903
|
+
return (data, signature) => verifyEs256Signature(data, base64urlDecode(signature), publicJwk);
|
|
904
|
+
}
|
|
905
|
+
function credentialVerificationKey(parsed, trustedIssuerKeys, byLayer) {
|
|
906
|
+
const kid = isString(parsed.header['kid']) ? parsed.header['kid'] : null;
|
|
907
|
+
if (parsed.input.layer === 'L1')
|
|
908
|
+
return findIssuerKey(kid, trustedIssuerKeys);
|
|
909
|
+
const l1UserKey = getCnfJwk(byLayer.get('L1')?.payload);
|
|
910
|
+
if (parsed.input.layer === 'L2')
|
|
911
|
+
return l1UserKey;
|
|
912
|
+
const l2AgentKeys = byLayer
|
|
913
|
+
.get('L2')
|
|
914
|
+
?.mandateValues.map(getCnfJwk)
|
|
915
|
+
.filter((jwk) => jwk !== undefined) ?? [];
|
|
916
|
+
return l2AgentKeys.find((candidate) => getKid(candidate) === kid);
|
|
917
|
+
}
|
|
918
|
+
function sdJwtErrorReason(error) {
|
|
919
|
+
const message = error instanceof Error ? error.message.toLowerCase() : '';
|
|
920
|
+
if (message.includes('status_fetcher_unconfigured'))
|
|
921
|
+
return 'status_fetcher';
|
|
922
|
+
if (message.includes('vct_fetcher_unconfigured'))
|
|
923
|
+
return 'vct_fetcher';
|
|
924
|
+
if (message.includes('hash') || message.includes('digest'))
|
|
925
|
+
return 'disclosure_digest';
|
|
926
|
+
if (message.includes('signature'))
|
|
927
|
+
return 'signature';
|
|
928
|
+
if (message.includes('expired') || message.includes('exp'))
|
|
929
|
+
return 'exp';
|
|
930
|
+
if (message.includes('nbf'))
|
|
931
|
+
return 'nbf';
|
|
932
|
+
if (message.includes('iat'))
|
|
933
|
+
return 'iat';
|
|
934
|
+
if (message.includes('key binding') || message.includes('kb'))
|
|
935
|
+
return 'key_binding';
|
|
936
|
+
if (message.includes('vct'))
|
|
937
|
+
return 'vct';
|
|
938
|
+
if (message.includes('status'))
|
|
939
|
+
return 'status';
|
|
940
|
+
return 'invalid';
|
|
941
|
+
}
|
|
942
|
+
function addSdJwtConformanceFinding(policy, code, layer, errors, warnings) {
|
|
943
|
+
if (policy === 'require')
|
|
944
|
+
errors.push(code);
|
|
945
|
+
else
|
|
946
|
+
warnings.push(`${code}:${layer}`);
|
|
947
|
+
}
|
|
948
|
+
function addConstraintFindings(policy, constraints, errors, warnings) {
|
|
949
|
+
for (const check of constraints.checks) {
|
|
950
|
+
let code;
|
|
951
|
+
if (check.status === 'failed')
|
|
952
|
+
code = `vi_constraint_failed:${check.type}`;
|
|
953
|
+
if (check.status === 'unresolved')
|
|
954
|
+
code = `vi_constraint_unresolved:${check.type}`;
|
|
955
|
+
if (check.status === 'unsupported')
|
|
956
|
+
code = `vi_constraint_unsupported:${check.type}`;
|
|
957
|
+
if (code === undefined)
|
|
958
|
+
continue;
|
|
959
|
+
if (policy === 'require')
|
|
960
|
+
errors.push(code);
|
|
961
|
+
else
|
|
962
|
+
warnings.push(code);
|
|
429
963
|
}
|
|
430
964
|
}
|
|
965
|
+
async function verifyCredentialSdJwtConformance(credentials, trustedIssuerKeys, policy, profile, options, nowSeconds, clockSkewSeconds) {
|
|
966
|
+
const errors = [];
|
|
967
|
+
const warnings = [];
|
|
968
|
+
const parsedCredentials = [];
|
|
969
|
+
const checks = credentials.map(() => sdJwtConformanceCheck('not_checked', profile));
|
|
970
|
+
for (let index = 0; index < credentials.length; index += 1) {
|
|
971
|
+
const credential = credentials[index];
|
|
972
|
+
try {
|
|
973
|
+
parsedCredentials.push({ index, parsed: parseCredential(credential) });
|
|
974
|
+
}
|
|
975
|
+
catch {
|
|
976
|
+
checks[index] = sdJwtConformanceCheck('invalid', profile, 'parse');
|
|
977
|
+
addSdJwtConformanceFinding(policy, 'vi_sd_jwt_conformance_invalid', credential.layer, errors, warnings);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
const byLayer = new Map();
|
|
981
|
+
for (const { parsed } of parsedCredentials)
|
|
982
|
+
byLayer.set(parsed.input.layer, parsed);
|
|
983
|
+
for (const { index: checkIndex, parsed } of parsedCredentials) {
|
|
984
|
+
const key = credentialVerificationKey(parsed, trustedIssuerKeys, byLayer);
|
|
985
|
+
const structuralErrors = credentialStructuralErrors(parsed);
|
|
986
|
+
if (parsed.header['alg'] !== 'ES256') {
|
|
987
|
+
checks[checkIndex] = sdJwtConformanceCheck('invalid', profile, 'alg');
|
|
988
|
+
addSdJwtConformanceFinding(policy, 'vi_sd_jwt_conformance_invalid', parsed.input.layer, errors, warnings);
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
if (structuralErrors.length > 0) {
|
|
992
|
+
checks[checkIndex] = sdJwtConformanceCheck('invalid', profile, structuralErrors[0]);
|
|
993
|
+
addSdJwtConformanceFinding(policy, 'vi_sd_jwt_conformance_invalid', parsed.input.layer, errors, warnings);
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
if (!key) {
|
|
997
|
+
checks[checkIndex] = sdJwtConformanceCheck('not_checked', profile, 'missing_key');
|
|
998
|
+
addSdJwtConformanceFinding(policy, 'vi_sd_jwt_conformance_key_missing', parsed.input.layer, errors, warnings);
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
if (disclosureDigestsOk(parsed) === false) {
|
|
1002
|
+
checks[checkIndex] = sdJwtConformanceCheck('invalid', profile, 'disclosure_digest');
|
|
1003
|
+
addSdJwtConformanceFinding(policy, 'vi_sd_jwt_conformance_invalid', parsed.input.layer, errors, warnings);
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
try {
|
|
1007
|
+
const verifier = sdJwtVerifier(key);
|
|
1008
|
+
const verifyOptions = { currentDate: nowSeconds, skewSeconds: clockSkewSeconds };
|
|
1009
|
+
if (profile === 'sd-jwt-vc') {
|
|
1010
|
+
const vcOptions = options.sdJwtVc ?? {};
|
|
1011
|
+
const statusListFetcher = vcOptions.statusListFetcher ??
|
|
1012
|
+
(async () => {
|
|
1013
|
+
throw new Error('status_fetcher_unconfigured');
|
|
1014
|
+
});
|
|
1015
|
+
const vctFetcher = vcOptions.vctFetcher ??
|
|
1016
|
+
(vcOptions.loadTypeMetadata
|
|
1017
|
+
? async () => {
|
|
1018
|
+
throw new Error('vct_fetcher_unconfigured');
|
|
1019
|
+
}
|
|
1020
|
+
: undefined);
|
|
1021
|
+
const vcConfig = {
|
|
1022
|
+
hasher: sdJwtHasher,
|
|
1023
|
+
hashAlg: 'sha-256',
|
|
1024
|
+
verifier,
|
|
1025
|
+
loadTypeMetadataFormat: vcOptions.loadTypeMetadata === true,
|
|
1026
|
+
statusListFetcher,
|
|
1027
|
+
};
|
|
1028
|
+
if (vcOptions.maxVctExtendsDepth !== undefined)
|
|
1029
|
+
Object.assign(vcConfig, { maxVctExtendsDepth: vcOptions.maxVctExtendsDepth });
|
|
1030
|
+
if (vcOptions.statusValidator !== undefined)
|
|
1031
|
+
Object.assign(vcConfig, { statusValidator: vcOptions.statusValidator });
|
|
1032
|
+
if (vctFetcher !== undefined)
|
|
1033
|
+
Object.assign(vcConfig, { vctFetcher });
|
|
1034
|
+
const verifierInstance = new SDJwtVcInstance(vcConfig);
|
|
1035
|
+
await verifierInstance.verify(parsed.input.sdJwt, verifyOptions);
|
|
1036
|
+
await verifierInstance.getClaims(parsed.input.sdJwt);
|
|
1037
|
+
}
|
|
1038
|
+
else {
|
|
1039
|
+
const verifierInstance = new SDJwtInstance({
|
|
1040
|
+
hasher: sdJwtHasher,
|
|
1041
|
+
hashAlg: 'sha-256',
|
|
1042
|
+
verifier,
|
|
1043
|
+
});
|
|
1044
|
+
await verifierInstance.verify(parsed.input.sdJwt, verifyOptions);
|
|
1045
|
+
await verifierInstance.getClaims(parsed.input.sdJwt);
|
|
1046
|
+
}
|
|
1047
|
+
checks[checkIndex] = sdJwtConformanceCheck('verified', profile);
|
|
1048
|
+
}
|
|
1049
|
+
catch (error) {
|
|
1050
|
+
checks[checkIndex] = sdJwtConformanceCheck('invalid', profile, sdJwtErrorReason(error));
|
|
1051
|
+
addSdJwtConformanceFinding(policy, 'vi_sd_jwt_conformance_invalid', parsed.input.layer, errors, warnings);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
return { checks, errors, warnings };
|
|
1055
|
+
}
|
|
431
1056
|
export function verifyAp2ViEvidence(bundle, options = {}) {
|
|
432
1057
|
const errors = [];
|
|
433
1058
|
const warnings = [];
|
|
434
1059
|
const signaturePolicy = options.signaturePolicy ?? 'require';
|
|
1060
|
+
const constraintPolicy = options.constraintPolicy ?? 'require';
|
|
435
1061
|
const trustedIssuerKeys = options.trustedIssuerKeys ?? bundle.trustedIssuerKeys ?? [];
|
|
436
1062
|
const nowSeconds = options.nowSeconds ?? Math.floor(Date.now() / 1000);
|
|
437
1063
|
const clockSkewSeconds = options.clockSkewSeconds ?? 300;
|
|
@@ -447,6 +1073,7 @@ export function verifyAp2ViEvidence(bundle, options = {}) {
|
|
|
447
1073
|
errors.push('vi_typ_mismatch');
|
|
448
1074
|
if (check.disclosuresOk === false)
|
|
449
1075
|
errors.push('vi_disclosure_digest_mismatch');
|
|
1076
|
+
errors.push(...credentialStructuralErrors(parsed));
|
|
450
1077
|
}
|
|
451
1078
|
catch (error) {
|
|
452
1079
|
const code = error instanceof Error && error.message ? error.message : 'vi_jwt_malformed';
|
|
@@ -457,6 +1084,7 @@ export function verifyAp2ViEvidence(bundle, options = {}) {
|
|
|
457
1084
|
typ: null,
|
|
458
1085
|
kid: null,
|
|
459
1086
|
signature: signatureCheck('invalid', 'parse'),
|
|
1087
|
+
sdJwtConformance: sdJwtConformanceCheck('invalid', null, 'parse'),
|
|
460
1088
|
sdHashOk: null,
|
|
461
1089
|
disclosuresOk: null,
|
|
462
1090
|
mandateVcts: [],
|
|
@@ -478,6 +1106,19 @@ export function verifyAp2ViEvidence(bundle, options = {}) {
|
|
|
478
1106
|
: finalPaymentMandates.length > 0 || finalCheckoutMandates.length > 0
|
|
479
1107
|
? 'immediate'
|
|
480
1108
|
: 'unknown';
|
|
1109
|
+
const constraints = constraintPolicy === 'off'
|
|
1110
|
+
? { status: 'not_checked', checks: [] }
|
|
1111
|
+
: evaluateAp2ViConstraints({
|
|
1112
|
+
openCheckoutMandates,
|
|
1113
|
+
closedCheckoutMandates: finalCheckoutMandates,
|
|
1114
|
+
openPaymentMandates,
|
|
1115
|
+
closedPaymentMandates: finalPaymentMandates,
|
|
1116
|
+
checkoutPayload: bundle.ap2?.checkoutPayload,
|
|
1117
|
+
nowSeconds,
|
|
1118
|
+
}, disclosureMap(parsedCredentials));
|
|
1119
|
+
if (constraintPolicy !== 'off') {
|
|
1120
|
+
addConstraintFindings(constraintPolicy, constraints, errors, warnings);
|
|
1121
|
+
}
|
|
481
1122
|
const ap2 = {};
|
|
482
1123
|
const expectedPaymentReference = expectedReceiptReference(bundle.ap2?.closedPaymentMandate, bundle.ap2?.closedPaymentMandateHash);
|
|
483
1124
|
const expectedCheckoutReference = expectedReceiptReference(bundle.ap2?.closedCheckoutMandate, bundle.ap2?.closedCheckoutMandateHash);
|
|
@@ -512,6 +1153,7 @@ export function verifyAp2ViEvidence(bundle, options = {}) {
|
|
|
512
1153
|
credentials: credentialChecks,
|
|
513
1154
|
delegationOk,
|
|
514
1155
|
checkoutPaymentBindingOk,
|
|
1156
|
+
constraints,
|
|
515
1157
|
},
|
|
516
1158
|
errors: [...new Set(errors)],
|
|
517
1159
|
warnings: [...new Set(warnings)],
|
|
@@ -521,10 +1163,14 @@ export async function verifyAp2ViEvidenceAsync(bundle, options = {}) {
|
|
|
521
1163
|
const ap2 = { ...bundle.ap2 };
|
|
522
1164
|
const receiptJwtIssuers = options.receiptJwtIssuers ?? bundle.receiptJwtIssuers ?? [];
|
|
523
1165
|
const receiptJwtPolicy = options.receiptJwtPolicy ?? 'require';
|
|
1166
|
+
const sdJwtConformancePolicy = options.sdJwtConformancePolicy ?? 'require';
|
|
1167
|
+
const sdJwtConformanceProfile = options.sdJwtConformanceProfile ?? 'sd-jwt-vc';
|
|
524
1168
|
const nowSeconds = options.nowSeconds ?? Math.floor(Date.now() / 1000);
|
|
525
1169
|
const clockSkewSeconds = options.clockSkewSeconds ?? 300;
|
|
526
1170
|
const jwtErrors = [];
|
|
527
1171
|
const jwtWarnings = [];
|
|
1172
|
+
const sdJwtErrors = [];
|
|
1173
|
+
const sdJwtWarnings = [];
|
|
528
1174
|
const paymentJwt = ap2.paymentReceiptJwt
|
|
529
1175
|
? await verifyReceiptJwt('payment', ap2.paymentReceiptJwt, receiptJwtIssuers, receiptJwtPolicy, nowSeconds, clockSkewSeconds, options.fetch)
|
|
530
1176
|
: undefined;
|
|
@@ -561,6 +1207,18 @@ export async function verifyAp2ViEvidenceAsync(bundle, options = {}) {
|
|
|
561
1207
|
}
|
|
562
1208
|
}
|
|
563
1209
|
const result = verifyAp2ViEvidence({ ...bundle, ap2 }, options);
|
|
1210
|
+
if (sdJwtConformancePolicy !== 'off') {
|
|
1211
|
+
const credentials = bundle.vi?.credentials ?? [];
|
|
1212
|
+
const trustedIssuerKeys = options.trustedIssuerKeys ?? bundle.trustedIssuerKeys ?? [];
|
|
1213
|
+
const sdJwt = await verifyCredentialSdJwtConformance(credentials, trustedIssuerKeys, sdJwtConformancePolicy, sdJwtConformanceProfile, options, nowSeconds, clockSkewSeconds);
|
|
1214
|
+
for (let index = 0; index < sdJwt.checks.length; index += 1) {
|
|
1215
|
+
const check = result.vi.credentials[index];
|
|
1216
|
+
if (check)
|
|
1217
|
+
check.sdJwtConformance = sdJwt.checks[index];
|
|
1218
|
+
}
|
|
1219
|
+
sdJwtErrors.push(...sdJwt.errors);
|
|
1220
|
+
sdJwtWarnings.push(...sdJwt.warnings);
|
|
1221
|
+
}
|
|
564
1222
|
if (paymentJwt) {
|
|
565
1223
|
if (result.ap2.paymentReceipt)
|
|
566
1224
|
result.ap2.paymentReceipt.jwt = paymentJwt.check;
|
|
@@ -573,8 +1231,8 @@ export async function verifyAp2ViEvidenceAsync(bundle, options = {}) {
|
|
|
573
1231
|
else
|
|
574
1232
|
result.ap2.checkoutReceipt = emptyJwtReceiptCheck(checkoutJwt.check);
|
|
575
1233
|
}
|
|
576
|
-
const errors = [...new Set([...result.errors, ...jwtErrors])];
|
|
577
|
-
const warnings = [...new Set([...result.warnings, ...jwtWarnings])];
|
|
1234
|
+
const errors = [...new Set([...result.errors, ...jwtErrors, ...sdJwtErrors])];
|
|
1235
|
+
const warnings = [...new Set([...result.warnings, ...jwtWarnings, ...sdJwtWarnings])];
|
|
578
1236
|
const hasAp2Receipt = result.ap2.paymentReceipt?.present === true || result.ap2.checkoutReceipt?.present === true;
|
|
579
1237
|
return {
|
|
580
1238
|
...result,
|