@atrib/verify 0.3.6 → 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 +85 -4
- package/dist/ap2-vi-evidence.d.ts +141 -0
- package/dist/ap2-vi-evidence.d.ts.map +1 -0
- package/dist/ap2-vi-evidence.js +1244 -0
- package/dist/ap2-vi-evidence.js.map +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +58 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +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 +7 -4
|
@@ -0,0 +1,1244 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import canonicalize from 'canonicalize';
|
|
3
|
+
import { createPublicKey, verify as verifyEcdsa } from 'node:crypto';
|
|
4
|
+
import { base64urlDecode, base64urlEncode, sha256 } from '@atrib/mcp';
|
|
5
|
+
import { SDJwtInstance } from '@sd-jwt/core';
|
|
6
|
+
import { SDJwtVcInstance } from '@sd-jwt/sd-jwt-vc';
|
|
7
|
+
import { createLocalJWKSet, createRemoteJWKSet, customFetch, decodeJwt, decodeProtectedHeader, jwtVerify, } from 'jose';
|
|
8
|
+
const textEncoder = new TextEncoder();
|
|
9
|
+
const textDecoder = new TextDecoder();
|
|
10
|
+
function isRecord(value) {
|
|
11
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
function isString(value) {
|
|
14
|
+
return typeof value === 'string';
|
|
15
|
+
}
|
|
16
|
+
function isNumber(value) {
|
|
17
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
18
|
+
}
|
|
19
|
+
function isSafeInteger(value) {
|
|
20
|
+
return typeof value === 'number' && Number.isSafeInteger(value);
|
|
21
|
+
}
|
|
22
|
+
function isJsonWebKeySet(value) {
|
|
23
|
+
return (isRecord(value) && Array.isArray(value['keys']) && value['keys'].every((key) => isRecord(key)));
|
|
24
|
+
}
|
|
25
|
+
function hashAscii(value) {
|
|
26
|
+
return base64urlEncode(sha256(textEncoder.encode(value)));
|
|
27
|
+
}
|
|
28
|
+
function decodeBase64urlJson(value) {
|
|
29
|
+
return JSON.parse(textDecoder.decode(base64urlDecode(value)));
|
|
30
|
+
}
|
|
31
|
+
function signatureCheck(status, reason) {
|
|
32
|
+
return reason === undefined ? { status } : { status, reason };
|
|
33
|
+
}
|
|
34
|
+
function sdJwtConformanceCheck(status, profile, reason) {
|
|
35
|
+
return reason === undefined ? { status, profile } : { status, profile, reason };
|
|
36
|
+
}
|
|
37
|
+
function collectDelegatePayloadDigestList(payload) {
|
|
38
|
+
const digests = [];
|
|
39
|
+
const delegatePayload = payload['delegate_payload'];
|
|
40
|
+
if (!Array.isArray(delegatePayload))
|
|
41
|
+
return digests;
|
|
42
|
+
for (const item of delegatePayload) {
|
|
43
|
+
if (isRecord(item) && isString(item['...'])) {
|
|
44
|
+
digests.push(item['...']);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return digests;
|
|
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
|
+
}
|
|
55
|
+
function disclosureValue(decoded) {
|
|
56
|
+
if (!Array.isArray(decoded))
|
|
57
|
+
return undefined;
|
|
58
|
+
if (decoded.length === 2)
|
|
59
|
+
return decoded[1];
|
|
60
|
+
if (decoded.length >= 3)
|
|
61
|
+
return decoded[2];
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
function parseCredential(input) {
|
|
65
|
+
const [jwt, ...rawDisclosures] = input.sdJwt.split('~');
|
|
66
|
+
if (!jwt)
|
|
67
|
+
throw new Error('vi_jwt_malformed');
|
|
68
|
+
const parts = jwt.split('.');
|
|
69
|
+
if (parts.length !== 3 || !parts[0] || !parts[1] || !parts[2]) {
|
|
70
|
+
throw new Error('vi_jwt_malformed');
|
|
71
|
+
}
|
|
72
|
+
const header = decodeBase64urlJson(parts[0]);
|
|
73
|
+
const payload = decodeBase64urlJson(parts[1]);
|
|
74
|
+
if (!isRecord(header) || !isRecord(payload))
|
|
75
|
+
throw new Error('vi_jwt_malformed');
|
|
76
|
+
const disclosures = rawDisclosures
|
|
77
|
+
.filter((value) => value.length > 0)
|
|
78
|
+
.map((encoded) => {
|
|
79
|
+
const value = disclosureValue(decodeBase64urlJson(encoded));
|
|
80
|
+
return { encoded, digest: hashAscii(encoded), value };
|
|
81
|
+
});
|
|
82
|
+
const mandateValues = disclosures.flatMap((disclosure) => isRecord(disclosure.value) && isString(disclosure.value['vct']) ? [disclosure.value] : []);
|
|
83
|
+
return {
|
|
84
|
+
input,
|
|
85
|
+
jwt,
|
|
86
|
+
header,
|
|
87
|
+
payload,
|
|
88
|
+
signingInput: `${parts[0]}.${parts[1]}`,
|
|
89
|
+
signature: base64urlDecode(parts[2]),
|
|
90
|
+
disclosures,
|
|
91
|
+
mandateValues,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function getKid(jwk) {
|
|
95
|
+
const kid = jwk ? jwk['kid'] : undefined;
|
|
96
|
+
return isString(kid) ? kid : null;
|
|
97
|
+
}
|
|
98
|
+
function getCnfJwk(record) {
|
|
99
|
+
const cnf = record?.['cnf'];
|
|
100
|
+
if (!isRecord(cnf))
|
|
101
|
+
return undefined;
|
|
102
|
+
const jwk = cnf['jwk'];
|
|
103
|
+
return isRecord(jwk) ? jwk : undefined;
|
|
104
|
+
}
|
|
105
|
+
function canonicalJson(value) {
|
|
106
|
+
const canonical = canonicalize(value);
|
|
107
|
+
return typeof canonical === 'string' ? canonical : null;
|
|
108
|
+
}
|
|
109
|
+
function sameJson(left, right) {
|
|
110
|
+
const leftCanonical = canonicalJson(left);
|
|
111
|
+
const rightCanonical = canonicalJson(right);
|
|
112
|
+
return leftCanonical !== null && leftCanonical === rightCanonical;
|
|
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
|
+
}
|
|
484
|
+
function normalizeJwks(value) {
|
|
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);
|
|
491
|
+
return { keys: keys };
|
|
492
|
+
}
|
|
493
|
+
function verifyEs256Signature(data, signature, publicJwk) {
|
|
494
|
+
const key = createPublicKey({ key: publicJwk, format: 'jwk' });
|
|
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);
|
|
499
|
+
}
|
|
500
|
+
function findIssuerKey(kid, keys) {
|
|
501
|
+
if (kid === null)
|
|
502
|
+
return keys[0];
|
|
503
|
+
return keys.find((key) => getKid(key) === kid);
|
|
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
|
+
}
|
|
530
|
+
function receiptJwtError(kind, code) {
|
|
531
|
+
return `ap2_${kind}_receipt_jwt_${code}`;
|
|
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
|
+
}
|
|
547
|
+
function addReceiptJwtFailure(verification, kind, code, policy) {
|
|
548
|
+
const error = receiptJwtError(kind, code);
|
|
549
|
+
verification.check.error = error;
|
|
550
|
+
if (policy === 'require')
|
|
551
|
+
verification.errors.push(error);
|
|
552
|
+
else
|
|
553
|
+
verification.warnings.push(error);
|
|
554
|
+
}
|
|
555
|
+
function findReceiptJwtIssuer(issuer, issuers) {
|
|
556
|
+
if (issuer !== null) {
|
|
557
|
+
const exact = issuers.find((candidate) => candidate.issuer === issuer);
|
|
558
|
+
if (exact)
|
|
559
|
+
return exact;
|
|
560
|
+
}
|
|
561
|
+
return issuers.find((candidate) => candidate.issuer === undefined);
|
|
562
|
+
}
|
|
563
|
+
async function fetchJson(url, fetchImpl) {
|
|
564
|
+
const fetchFn = fetchImpl ?? globalThis.fetch;
|
|
565
|
+
if (typeof fetchFn !== 'function')
|
|
566
|
+
throw new Error('fetch_unavailable');
|
|
567
|
+
const response = await fetchFn(url);
|
|
568
|
+
if (!response.ok)
|
|
569
|
+
throw new Error('metadata_fetch_failed');
|
|
570
|
+
return response.json();
|
|
571
|
+
}
|
|
572
|
+
async function resolveReceiptJwtKey(issuer, fetchImpl) {
|
|
573
|
+
const remoteOptions = fetchImpl ? { [customFetch]: fetchImpl } : undefined;
|
|
574
|
+
if (issuer.jwks) {
|
|
575
|
+
return {
|
|
576
|
+
getKey: createLocalJWKSet(normalizeJwks(issuer.jwks)),
|
|
577
|
+
source: 'static',
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
if (issuer.jwksUrl) {
|
|
581
|
+
return {
|
|
582
|
+
getKey: createRemoteJWKSet(new URL(issuer.jwksUrl), remoteOptions),
|
|
583
|
+
source: 'jwks_url',
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
if (issuer.metadataUrl) {
|
|
587
|
+
const metadata = await fetchJson(issuer.metadataUrl, fetchImpl);
|
|
588
|
+
if (!isRecord(metadata))
|
|
589
|
+
throw new Error('metadata_invalid');
|
|
590
|
+
const inlineJwks = metadata['jwks'];
|
|
591
|
+
if (isJsonWebKeySet(inlineJwks)) {
|
|
592
|
+
return {
|
|
593
|
+
getKey: createLocalJWKSet(normalizeJwks(inlineJwks)),
|
|
594
|
+
source: 'metadata',
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
const jwksUri = metadata['jwks_uri'];
|
|
598
|
+
if (isString(jwksUri)) {
|
|
599
|
+
return {
|
|
600
|
+
getKey: createRemoteJWKSet(new URL(jwksUri), remoteOptions),
|
|
601
|
+
source: 'metadata',
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
throw new Error('metadata_jwks_missing');
|
|
605
|
+
}
|
|
606
|
+
throw new Error('key_source_missing');
|
|
607
|
+
}
|
|
608
|
+
function expectedTyp(layer, typ) {
|
|
609
|
+
if (layer === 'L1')
|
|
610
|
+
return typ === 'sd+jwt';
|
|
611
|
+
if (layer === 'L2')
|
|
612
|
+
return typ === 'kb-sd-jwt' || typ === 'kb-sd-jwt+kb';
|
|
613
|
+
return typ === 'kb-sd-jwt';
|
|
614
|
+
}
|
|
615
|
+
function disclosureDigestsOk(parsed) {
|
|
616
|
+
if (parsed.disclosures.length === 0)
|
|
617
|
+
return null;
|
|
618
|
+
const sd = parsed.payload['_sd'];
|
|
619
|
+
const sdDigests = new Set(Array.isArray(sd) ? sd.filter(isString) : []);
|
|
620
|
+
const delegateDigests = collectDelegatePayloadDigests(parsed.payload);
|
|
621
|
+
return parsed.disclosures.every((disclosure) => sdDigests.has(disclosure.digest) || delegateDigests.has(disclosure.digest));
|
|
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
|
+
}
|
|
640
|
+
function initialCredentialCheck(parsed) {
|
|
641
|
+
const alg = isString(parsed.header['alg']) ? parsed.header['alg'] : null;
|
|
642
|
+
const typ = isString(parsed.header['typ']) ? parsed.header['typ'] : null;
|
|
643
|
+
const kid = isString(parsed.header['kid']) ? parsed.header['kid'] : null;
|
|
644
|
+
return {
|
|
645
|
+
layer: parsed.input.layer,
|
|
646
|
+
alg,
|
|
647
|
+
typ,
|
|
648
|
+
kid,
|
|
649
|
+
signature: signatureCheck('not_checked'),
|
|
650
|
+
sdJwtConformance: sdJwtConformanceCheck('not_checked', null, 'async_required'),
|
|
651
|
+
sdHashOk: null,
|
|
652
|
+
disclosuresOk: disclosureDigestsOk(parsed),
|
|
653
|
+
mandateVcts: parsed.mandateValues.map((mandate) => String(mandate['vct'])),
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
function checkReceipt(value, requiredFields, expectedReference) {
|
|
657
|
+
if (!isRecord(value)) {
|
|
658
|
+
return {
|
|
659
|
+
present: value !== undefined,
|
|
660
|
+
status: null,
|
|
661
|
+
success: false,
|
|
662
|
+
reference: null,
|
|
663
|
+
referenceOk: null,
|
|
664
|
+
missingFields: ['receipt_object'],
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
const status = isString(value['status']) ? value['status'] : null;
|
|
668
|
+
const reference = isString(value['reference']) ? value['reference'] : null;
|
|
669
|
+
const missingFields = requiredFields.filter((field) => !isString(value[field]) && !isNumber(value[field]));
|
|
670
|
+
const success = status === 'Success';
|
|
671
|
+
const referenceOk = expectedReference === undefined ? null : reference === expectedReference;
|
|
672
|
+
return {
|
|
673
|
+
present: true,
|
|
674
|
+
status,
|
|
675
|
+
success,
|
|
676
|
+
reference,
|
|
677
|
+
referenceOk,
|
|
678
|
+
missingFields,
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
function emptyJwtReceiptCheck(jwt) {
|
|
682
|
+
return {
|
|
683
|
+
present: true,
|
|
684
|
+
status: null,
|
|
685
|
+
success: false,
|
|
686
|
+
reference: null,
|
|
687
|
+
referenceOk: null,
|
|
688
|
+
missingFields: ['receipt_object'],
|
|
689
|
+
jwt,
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
async function verifyReceiptJwt(kind, receiptJwt, issuers, policy, nowSeconds, clockSkewSeconds, fetchImpl) {
|
|
693
|
+
const verification = {
|
|
694
|
+
check: {
|
|
695
|
+
present: true,
|
|
696
|
+
verified: false,
|
|
697
|
+
issuer: null,
|
|
698
|
+
kid: null,
|
|
699
|
+
alg: null,
|
|
700
|
+
jwksSource: null,
|
|
701
|
+
},
|
|
702
|
+
errors: [],
|
|
703
|
+
warnings: [],
|
|
704
|
+
};
|
|
705
|
+
try {
|
|
706
|
+
const header = decodeProtectedHeader(receiptJwt);
|
|
707
|
+
const payload = decodeJwt(receiptJwt);
|
|
708
|
+
const issuer = isString(payload.iss) ? payload.iss : null;
|
|
709
|
+
verification.check.alg = isString(header.alg) ? header.alg : null;
|
|
710
|
+
verification.check.kid = isString(header.kid) ? header.kid : null;
|
|
711
|
+
verification.check.issuer = issuer;
|
|
712
|
+
if (header.alg !== 'ES256') {
|
|
713
|
+
addReceiptJwtFailure(verification, kind, 'alg_unsupported', policy);
|
|
714
|
+
return verification;
|
|
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
|
+
}
|
|
724
|
+
const issuerConfig = findReceiptJwtIssuer(issuer, issuers);
|
|
725
|
+
if (!issuerConfig) {
|
|
726
|
+
addReceiptJwtFailure(verification, kind, 'issuer_untrusted', policy);
|
|
727
|
+
return verification;
|
|
728
|
+
}
|
|
729
|
+
const key = await resolveReceiptJwtKey(issuerConfig, fetchImpl);
|
|
730
|
+
verification.check.jwksSource = key.source;
|
|
731
|
+
const verifyOptions = {
|
|
732
|
+
algorithms: ['ES256'],
|
|
733
|
+
clockTolerance: clockSkewSeconds,
|
|
734
|
+
currentDate: new Date(nowSeconds * 1000),
|
|
735
|
+
};
|
|
736
|
+
if (issuerConfig.audience !== undefined)
|
|
737
|
+
verifyOptions.audience = issuerConfig.audience;
|
|
738
|
+
if (issuerConfig.issuer !== undefined)
|
|
739
|
+
verifyOptions.issuer = issuerConfig.issuer;
|
|
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
|
+
}
|
|
746
|
+
verification.check.verified = true;
|
|
747
|
+
verification.payload = verified.payload;
|
|
748
|
+
return verification;
|
|
749
|
+
}
|
|
750
|
+
catch (error) {
|
|
751
|
+
const code = error instanceof Error && error.message ? error.message : 'invalid';
|
|
752
|
+
addReceiptJwtFailure(verification, kind, normalizeReceiptJwtErrorCode(code), policy);
|
|
753
|
+
if (code !== 'invalid' && code !== 'fetch_unavailable') {
|
|
754
|
+
verification.warnings.push(`${receiptJwtError(kind, 'detail')}:${code}`);
|
|
755
|
+
}
|
|
756
|
+
return verification;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
function expectedReceiptReference(serialized, explicitHash) {
|
|
760
|
+
if (explicitHash)
|
|
761
|
+
return explicitHash;
|
|
762
|
+
if (serialized)
|
|
763
|
+
return hashAscii(serialized);
|
|
764
|
+
return undefined;
|
|
765
|
+
}
|
|
766
|
+
function findMandates(parsedCredentials, vct) {
|
|
767
|
+
return parsedCredentials.flatMap((parsed) => parsed.mandateValues.filter((mandate) => mandate['vct'] === vct));
|
|
768
|
+
}
|
|
769
|
+
function checkCheckoutHash(mandates, errors) {
|
|
770
|
+
for (const mandate of mandates) {
|
|
771
|
+
const checkoutJwt = mandate['checkout_jwt'];
|
|
772
|
+
const checkoutHash = mandate['checkout_hash'];
|
|
773
|
+
if (!isString(checkoutJwt) || !isString(checkoutHash))
|
|
774
|
+
continue;
|
|
775
|
+
if (hashAscii(checkoutJwt) !== checkoutHash) {
|
|
776
|
+
errors.push('vi_checkout_hash_mismatch');
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
function checkPaymentCheckoutBinding(paymentMandates, checkoutMandates, errors) {
|
|
781
|
+
if (paymentMandates.length === 0 || checkoutMandates.length === 0)
|
|
782
|
+
return null;
|
|
783
|
+
const checkoutHashes = new Set(checkoutMandates.map((mandate) => mandate['checkout_hash']).filter(isString));
|
|
784
|
+
const ok = paymentMandates.every((mandate) => isString(mandate['transaction_id']) && checkoutHashes.has(mandate['transaction_id']));
|
|
785
|
+
if (!ok)
|
|
786
|
+
errors.push('vi_checkout_payment_binding_mismatch');
|
|
787
|
+
return ok;
|
|
788
|
+
}
|
|
789
|
+
function checkDelegation(openCheckoutMandates, openPaymentMandates, errors) {
|
|
790
|
+
const jwks = [...openCheckoutMandates, ...openPaymentMandates]
|
|
791
|
+
.map(getCnfJwk)
|
|
792
|
+
.filter((jwk) => jwk !== undefined);
|
|
793
|
+
if (jwks.length === 0)
|
|
794
|
+
return null;
|
|
795
|
+
const first = jwks[0];
|
|
796
|
+
const ok = jwks.every((jwk) => sameJson(jwk, first));
|
|
797
|
+
if (!ok)
|
|
798
|
+
errors.push('vi_l2_cnf_mismatch');
|
|
799
|
+
return ok;
|
|
800
|
+
}
|
|
801
|
+
function verifyCredentialSignatures(parsedCredentials, checks, trustedIssuerKeys, signaturePolicy, errors, warnings) {
|
|
802
|
+
const byLayer = new Map();
|
|
803
|
+
for (const parsed of parsedCredentials)
|
|
804
|
+
byLayer.set(parsed.input.layer, parsed);
|
|
805
|
+
const l1 = byLayer.get('L1');
|
|
806
|
+
const l2 = byLayer.get('L2');
|
|
807
|
+
const l1UserKey = getCnfJwk(l1?.payload);
|
|
808
|
+
const l2AgentKeys = l2?.mandateValues.map(getCnfJwk).filter((jwk) => jwk !== undefined) ?? [];
|
|
809
|
+
for (let index = 0; index < parsedCredentials.length; index += 1) {
|
|
810
|
+
const parsed = parsedCredentials[index];
|
|
811
|
+
const check = checks[index];
|
|
812
|
+
if (check.alg !== 'ES256') {
|
|
813
|
+
check.signature = signatureCheck('invalid', 'alg');
|
|
814
|
+
errors.push('vi_alg_unsupported');
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
let key;
|
|
818
|
+
if (parsed.input.layer === 'L1') {
|
|
819
|
+
key = findIssuerKey(check.kid, trustedIssuerKeys);
|
|
820
|
+
}
|
|
821
|
+
else if (parsed.input.layer === 'L2') {
|
|
822
|
+
key = l1UserKey;
|
|
823
|
+
if (key && getKid(key) !== null && check.kid !== getKid(key))
|
|
824
|
+
errors.push('vi_l2_kid_mismatch');
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
key = l2AgentKeys.find((candidate) => getKid(candidate) === check.kid);
|
|
828
|
+
}
|
|
829
|
+
if (!key) {
|
|
830
|
+
check.signature = signatureCheck('not_checked', 'missing_key');
|
|
831
|
+
warnings.push(`vi_signature_key_missing:${parsed.input.layer}`);
|
|
832
|
+
if (signaturePolicy === 'require')
|
|
833
|
+
errors.push('vi_signature_key_missing');
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
try {
|
|
837
|
+
check.signature = verifyJwtSignature(parsed, key)
|
|
838
|
+
? signatureCheck('verified')
|
|
839
|
+
: signatureCheck('invalid', 'signature');
|
|
840
|
+
if (check.signature.status === 'invalid')
|
|
841
|
+
errors.push('vi_signature_invalid');
|
|
842
|
+
}
|
|
843
|
+
catch {
|
|
844
|
+
check.signature = signatureCheck('invalid', 'signature');
|
|
845
|
+
errors.push('vi_signature_invalid');
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
function checkSdHash(parsedCredentials, checks, errors) {
|
|
850
|
+
const byLayer = new Map();
|
|
851
|
+
for (const parsed of parsedCredentials)
|
|
852
|
+
byLayer.set(parsed.input.layer, parsed);
|
|
853
|
+
const l1 = byLayer.get('L1');
|
|
854
|
+
for (let index = 0; index < parsedCredentials.length; index += 1) {
|
|
855
|
+
const parsed = parsedCredentials[index];
|
|
856
|
+
const check = checks[index];
|
|
857
|
+
if (parsed.input.layer === 'L1') {
|
|
858
|
+
check.sdHashOk = parsed.payload['sd_hash'] === undefined;
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
const sdHash = parsed.payload['sd_hash'];
|
|
862
|
+
if (!isString(sdHash)) {
|
|
863
|
+
check.sdHashOk = false;
|
|
864
|
+
errors.push('vi_sd_hash_missing');
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
let parentPresentation;
|
|
868
|
+
if (parsed.input.layer === 'L2') {
|
|
869
|
+
parentPresentation = l1?.input.sdJwt;
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
parentPresentation = parsed.input.parentPresentation;
|
|
873
|
+
}
|
|
874
|
+
if (!parentPresentation) {
|
|
875
|
+
check.sdHashOk = null;
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
check.sdHashOk = hashAscii(parentPresentation) === sdHash;
|
|
879
|
+
if (!check.sdHashOk)
|
|
880
|
+
errors.push('vi_sd_hash_mismatch');
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
function checkTimeWindow(parsedCredentials, nowSeconds, clockSkewSeconds, errors) {
|
|
884
|
+
for (const parsed of parsedCredentials) {
|
|
885
|
+
const exp = parsed.payload['exp'];
|
|
886
|
+
if (isNumber(exp) && nowSeconds > exp + clockSkewSeconds)
|
|
887
|
+
errors.push('vi_credential_expired');
|
|
888
|
+
const iat = parsed.payload['iat'];
|
|
889
|
+
if (isNumber(iat) && nowSeconds + clockSkewSeconds < iat)
|
|
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);
|
|
963
|
+
}
|
|
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
|
+
}
|
|
1056
|
+
export function verifyAp2ViEvidence(bundle, options = {}) {
|
|
1057
|
+
const errors = [];
|
|
1058
|
+
const warnings = [];
|
|
1059
|
+
const signaturePolicy = options.signaturePolicy ?? 'require';
|
|
1060
|
+
const constraintPolicy = options.constraintPolicy ?? 'require';
|
|
1061
|
+
const trustedIssuerKeys = options.trustedIssuerKeys ?? bundle.trustedIssuerKeys ?? [];
|
|
1062
|
+
const nowSeconds = options.nowSeconds ?? Math.floor(Date.now() / 1000);
|
|
1063
|
+
const clockSkewSeconds = options.clockSkewSeconds ?? 300;
|
|
1064
|
+
const parsedCredentials = [];
|
|
1065
|
+
const credentialChecks = [];
|
|
1066
|
+
for (const credential of bundle.vi?.credentials ?? []) {
|
|
1067
|
+
try {
|
|
1068
|
+
const parsed = parseCredential(credential);
|
|
1069
|
+
const check = initialCredentialCheck(parsed);
|
|
1070
|
+
parsedCredentials.push(parsed);
|
|
1071
|
+
credentialChecks.push(check);
|
|
1072
|
+
if (!expectedTyp(credential.layer, check.typ))
|
|
1073
|
+
errors.push('vi_typ_mismatch');
|
|
1074
|
+
if (check.disclosuresOk === false)
|
|
1075
|
+
errors.push('vi_disclosure_digest_mismatch');
|
|
1076
|
+
errors.push(...credentialStructuralErrors(parsed));
|
|
1077
|
+
}
|
|
1078
|
+
catch (error) {
|
|
1079
|
+
const code = error instanceof Error && error.message ? error.message : 'vi_jwt_malformed';
|
|
1080
|
+
errors.push(code);
|
|
1081
|
+
credentialChecks.push({
|
|
1082
|
+
layer: credential.layer,
|
|
1083
|
+
alg: null,
|
|
1084
|
+
typ: null,
|
|
1085
|
+
kid: null,
|
|
1086
|
+
signature: signatureCheck('invalid', 'parse'),
|
|
1087
|
+
sdJwtConformance: sdJwtConformanceCheck('invalid', null, 'parse'),
|
|
1088
|
+
sdHashOk: null,
|
|
1089
|
+
disclosuresOk: null,
|
|
1090
|
+
mandateVcts: [],
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
checkSdHash(parsedCredentials, credentialChecks, errors);
|
|
1095
|
+
checkTimeWindow(parsedCredentials, nowSeconds, clockSkewSeconds, errors);
|
|
1096
|
+
verifyCredentialSignatures(parsedCredentials, credentialChecks, trustedIssuerKeys, signaturePolicy, errors, warnings);
|
|
1097
|
+
const finalPaymentMandates = findMandates(parsedCredentials, 'mandate.payment.1');
|
|
1098
|
+
const finalCheckoutMandates = findMandates(parsedCredentials, 'mandate.checkout.1');
|
|
1099
|
+
const openPaymentMandates = findMandates(parsedCredentials, 'mandate.payment.open.1');
|
|
1100
|
+
const openCheckoutMandates = findMandates(parsedCredentials, 'mandate.checkout.open.1');
|
|
1101
|
+
checkCheckoutHash(finalCheckoutMandates, errors);
|
|
1102
|
+
const checkoutPaymentBindingOk = checkPaymentCheckoutBinding(finalPaymentMandates, finalCheckoutMandates, errors);
|
|
1103
|
+
const delegationOk = checkDelegation(openCheckoutMandates, openPaymentMandates, errors);
|
|
1104
|
+
const mode = openPaymentMandates.length > 0 || openCheckoutMandates.length > 0
|
|
1105
|
+
? 'autonomous'
|
|
1106
|
+
: finalPaymentMandates.length > 0 || finalCheckoutMandates.length > 0
|
|
1107
|
+
? 'immediate'
|
|
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
|
+
}
|
|
1122
|
+
const ap2 = {};
|
|
1123
|
+
const expectedPaymentReference = expectedReceiptReference(bundle.ap2?.closedPaymentMandate, bundle.ap2?.closedPaymentMandateHash);
|
|
1124
|
+
const expectedCheckoutReference = expectedReceiptReference(bundle.ap2?.closedCheckoutMandate, bundle.ap2?.closedCheckoutMandateHash);
|
|
1125
|
+
if (bundle.ap2?.paymentReceipt !== undefined) {
|
|
1126
|
+
ap2.paymentReceipt = checkReceipt(bundle.ap2.paymentReceipt, ['status', 'iss', 'iat', 'reference', 'payment_id'], expectedPaymentReference);
|
|
1127
|
+
if (!ap2.paymentReceipt.success)
|
|
1128
|
+
errors.push('ap2_payment_receipt_not_success');
|
|
1129
|
+
if (ap2.paymentReceipt.missingFields.length > 0)
|
|
1130
|
+
errors.push('ap2_payment_receipt_missing_fields');
|
|
1131
|
+
if (ap2.paymentReceipt.referenceOk === false)
|
|
1132
|
+
errors.push('ap2_payment_receipt_reference_mismatch');
|
|
1133
|
+
}
|
|
1134
|
+
if (bundle.ap2?.checkoutReceipt !== undefined) {
|
|
1135
|
+
ap2.checkoutReceipt = checkReceipt(bundle.ap2.checkoutReceipt, ['status', 'iss', 'iat', 'reference', 'order_id'], expectedCheckoutReference);
|
|
1136
|
+
if (!ap2.checkoutReceipt.success)
|
|
1137
|
+
errors.push('ap2_checkout_receipt_not_success');
|
|
1138
|
+
if (ap2.checkoutReceipt.missingFields.length > 0)
|
|
1139
|
+
errors.push('ap2_checkout_receipt_missing_fields');
|
|
1140
|
+
if (ap2.checkoutReceipt.referenceOk === false)
|
|
1141
|
+
errors.push('ap2_checkout_receipt_reference_mismatch');
|
|
1142
|
+
}
|
|
1143
|
+
const paymentAccepted = ap2.paymentReceipt?.success === true && ap2.paymentReceipt.referenceOk !== false;
|
|
1144
|
+
const checkoutAccepted = ap2.checkoutReceipt?.success === true && ap2.checkoutReceipt.referenceOk !== false;
|
|
1145
|
+
const transactionAccepted = paymentAccepted || checkoutAccepted;
|
|
1146
|
+
const hasAp2Receipt = ap2.paymentReceipt?.present === true || ap2.checkoutReceipt?.present === true;
|
|
1147
|
+
return {
|
|
1148
|
+
valid: errors.length === 0 && (!hasAp2Receipt || transactionAccepted),
|
|
1149
|
+
transactionAccepted,
|
|
1150
|
+
ap2,
|
|
1151
|
+
vi: {
|
|
1152
|
+
mode,
|
|
1153
|
+
credentials: credentialChecks,
|
|
1154
|
+
delegationOk,
|
|
1155
|
+
checkoutPaymentBindingOk,
|
|
1156
|
+
constraints,
|
|
1157
|
+
},
|
|
1158
|
+
errors: [...new Set(errors)],
|
|
1159
|
+
warnings: [...new Set(warnings)],
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
export async function verifyAp2ViEvidenceAsync(bundle, options = {}) {
|
|
1163
|
+
const ap2 = { ...bundle.ap2 };
|
|
1164
|
+
const receiptJwtIssuers = options.receiptJwtIssuers ?? bundle.receiptJwtIssuers ?? [];
|
|
1165
|
+
const receiptJwtPolicy = options.receiptJwtPolicy ?? 'require';
|
|
1166
|
+
const sdJwtConformancePolicy = options.sdJwtConformancePolicy ?? 'require';
|
|
1167
|
+
const sdJwtConformanceProfile = options.sdJwtConformanceProfile ?? 'sd-jwt-vc';
|
|
1168
|
+
const nowSeconds = options.nowSeconds ?? Math.floor(Date.now() / 1000);
|
|
1169
|
+
const clockSkewSeconds = options.clockSkewSeconds ?? 300;
|
|
1170
|
+
const jwtErrors = [];
|
|
1171
|
+
const jwtWarnings = [];
|
|
1172
|
+
const sdJwtErrors = [];
|
|
1173
|
+
const sdJwtWarnings = [];
|
|
1174
|
+
const paymentJwt = ap2.paymentReceiptJwt
|
|
1175
|
+
? await verifyReceiptJwt('payment', ap2.paymentReceiptJwt, receiptJwtIssuers, receiptJwtPolicy, nowSeconds, clockSkewSeconds, options.fetch)
|
|
1176
|
+
: undefined;
|
|
1177
|
+
if (paymentJwt) {
|
|
1178
|
+
jwtErrors.push(...paymentJwt.errors);
|
|
1179
|
+
jwtWarnings.push(...paymentJwt.warnings);
|
|
1180
|
+
if (paymentJwt.payload) {
|
|
1181
|
+
if (ap2.paymentReceipt !== undefined && !sameJson(ap2.paymentReceipt, paymentJwt.payload)) {
|
|
1182
|
+
const code = receiptJwtError('payment', 'payload_mismatch');
|
|
1183
|
+
if (receiptJwtPolicy === 'require')
|
|
1184
|
+
jwtErrors.push(code);
|
|
1185
|
+
else
|
|
1186
|
+
jwtWarnings.push(code);
|
|
1187
|
+
}
|
|
1188
|
+
ap2.paymentReceipt = paymentJwt.payload;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
const checkoutJwt = ap2.checkoutReceiptJwt
|
|
1192
|
+
? await verifyReceiptJwt('checkout', ap2.checkoutReceiptJwt, receiptJwtIssuers, receiptJwtPolicy, nowSeconds, clockSkewSeconds, options.fetch)
|
|
1193
|
+
: undefined;
|
|
1194
|
+
if (checkoutJwt) {
|
|
1195
|
+
jwtErrors.push(...checkoutJwt.errors);
|
|
1196
|
+
jwtWarnings.push(...checkoutJwt.warnings);
|
|
1197
|
+
if (checkoutJwt.payload) {
|
|
1198
|
+
if (ap2.checkoutReceipt !== undefined &&
|
|
1199
|
+
!sameJson(ap2.checkoutReceipt, checkoutJwt.payload)) {
|
|
1200
|
+
const code = receiptJwtError('checkout', 'payload_mismatch');
|
|
1201
|
+
if (receiptJwtPolicy === 'require')
|
|
1202
|
+
jwtErrors.push(code);
|
|
1203
|
+
else
|
|
1204
|
+
jwtWarnings.push(code);
|
|
1205
|
+
}
|
|
1206
|
+
ap2.checkoutReceipt = checkoutJwt.payload;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
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
|
+
}
|
|
1222
|
+
if (paymentJwt) {
|
|
1223
|
+
if (result.ap2.paymentReceipt)
|
|
1224
|
+
result.ap2.paymentReceipt.jwt = paymentJwt.check;
|
|
1225
|
+
else
|
|
1226
|
+
result.ap2.paymentReceipt = emptyJwtReceiptCheck(paymentJwt.check);
|
|
1227
|
+
}
|
|
1228
|
+
if (checkoutJwt) {
|
|
1229
|
+
if (result.ap2.checkoutReceipt)
|
|
1230
|
+
result.ap2.checkoutReceipt.jwt = checkoutJwt.check;
|
|
1231
|
+
else
|
|
1232
|
+
result.ap2.checkoutReceipt = emptyJwtReceiptCheck(checkoutJwt.check);
|
|
1233
|
+
}
|
|
1234
|
+
const errors = [...new Set([...result.errors, ...jwtErrors, ...sdJwtErrors])];
|
|
1235
|
+
const warnings = [...new Set([...result.warnings, ...jwtWarnings, ...sdJwtWarnings])];
|
|
1236
|
+
const hasAp2Receipt = result.ap2.paymentReceipt?.present === true || result.ap2.checkoutReceipt?.present === true;
|
|
1237
|
+
return {
|
|
1238
|
+
...result,
|
|
1239
|
+
valid: errors.length === 0 && (!hasAp2Receipt || result.transactionAccepted),
|
|
1240
|
+
errors,
|
|
1241
|
+
warnings,
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
//# sourceMappingURL=ap2-vi-evidence.js.map
|