@blamejs/blamejs-shop 0.0.128 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +2 -0
- package/lib/admin.js +1 -2
- package/lib/affiliates.js +4 -3
- package/lib/analytics.js +3 -2
- package/lib/api-keys.js +1 -1
- package/lib/assembly-instructions.js +2 -1
- package/lib/auto-replenish.js +4 -3
- package/lib/backorder.js +2 -1
- package/lib/business-hours.js +8 -1
- package/lib/carrier-accounts.js +1 -1
- package/lib/carrier-rates.js +1 -1
- package/lib/cart-abandonment.js +3 -2
- package/lib/cart-bulk-ops.js +2 -1
- package/lib/cart-recovery.js +5 -4
- package/lib/cart.js +6 -2
- package/lib/catalog-drafts.js +1 -1
- package/lib/click-and-collect.js +3 -2
- package/lib/clickstream.js +4 -3
- package/lib/config.js +2 -1
- package/lib/cookie-consent.js +2 -1
- package/lib/credit-limits.js +2 -1
- package/lib/currency-display.js +2 -1
- package/lib/customer-activity.js +3 -2
- package/lib/customer-impersonation.js +3 -3
- package/lib/customer-merge.js +4 -3
- package/lib/customer-portal.js +4 -4
- package/lib/customer-risk-profile.js +2 -1
- package/lib/customer-segments.js +2 -1
- package/lib/customer-surveys.js +6 -3
- package/lib/delivery-estimate.js +2 -2
- package/lib/demand-forecast.js +2 -1
- package/lib/discount-analytics.js +2 -2
- package/lib/dunning.js +4 -1
- package/lib/email-warmup.js +6 -1
- package/lib/email.js +1 -8
- package/lib/error-log.js +3 -2
- package/lib/event-log.js +3 -2
- package/lib/fraud-screen.js +3 -1
- package/lib/fulfillment-sla.js +3 -1
- package/lib/index.js +11 -3
- package/lib/inventory-allocations.js +3 -0
- package/lib/inventory-snapshots.js +2 -1
- package/lib/invoice-renderer.js +2 -1
- package/lib/line-gift-wrap.js +6 -1
- package/lib/live-chat.js +2 -1
- package/lib/loyalty-redemption.js +2 -1
- package/lib/newsletter.js +6 -1
- package/lib/operator-activity-feed.js +4 -3
- package/lib/operator-sessions.js +7 -7
- package/lib/order-exchanges.js +1 -0
- package/lib/order-timeline.js +2 -1
- package/lib/payment-retries.js +2 -1
- package/lib/payment.js +5 -4
- package/lib/pixel-events.js +6 -5
- package/lib/preorder.js +2 -1
- package/lib/print-queue.js +2 -1
- package/lib/product-compare.js +2 -1
- package/lib/product-qa.js +2 -1
- package/lib/push-notifications.js +6 -5
- package/lib/recently-viewed.js +7 -2
- package/lib/recommendations.js +7 -2
- package/lib/referral-leaderboard.js +2 -1
- package/lib/refund-automation.js +1 -1
- package/lib/refund-policy.js +1 -1
- package/lib/reorder-reminders.js +2 -1
- package/lib/reorder-thresholds.js +2 -1
- package/lib/robots-config.js +1 -0
- package/lib/sales-reports.js +17 -14
- package/lib/sales-tax-filings.js +2 -1
- package/lib/save-for-later.js +2 -1
- package/lib/search-suggestions.js +1 -1
- package/lib/shipping-insurance.js +2 -1
- package/lib/shipping-labels.js +3 -2
- package/lib/shipping-zones.js +1 -0
- package/lib/shrinkage-report.js +9 -8
- package/lib/sms-dispatcher.js +6 -5
- package/lib/stock-alerts.js +1 -1
- package/lib/stock-receipts.js +2 -1
- package/lib/store-credit.js +2 -1
- package/lib/storefront-forms.js +1 -1
- package/lib/storefront.js +223 -141
- package/lib/subscription-analytics.js +7 -2
- package/lib/subscription-controls.js +9 -8
- package/lib/subscription-gifts.js +2 -1
- package/lib/subscriptions.js +2 -0
- package/lib/support-tickets.js +4 -4
- package/lib/tax-cert-renewals.js +2 -1
- package/lib/tax-remittance.js +2 -1
- package/lib/theme-assets.js +1 -1
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +4 -0
- package/lib/vendor/blamejs/README.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +122 -2
- package/lib/vendor/blamejs/index.js +2 -0
- package/lib/vendor/blamejs/lib/did.js +367 -0
- package/lib/vendor/blamejs/lib/mdoc.js +305 -0
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.12.40.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.41.json +18 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +27 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/did.test.js +147 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mdoc.test.js +230 -0
- package/lib/vendor-invoices.js +1 -1
- package/lib/webhook-receiver.js +8 -2
- package/lib/webhook-subscriptions.js +1 -1
- package/lib/webhooks.js +6 -5
- package/lib/winback-campaigns.js +2 -1
- package/lib/wishlist-alerts.js +2 -1
- package/lib/wishlist-digest.js +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.mdoc
|
|
4
|
+
* @nav Crypto
|
|
5
|
+
* @title ISO mdoc / mDL (ISO 18013-5)
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Verify the issuer-signed data of an ISO/IEC 18013-5 mdoc — the
|
|
9
|
+
* credential format behind mobile driving licences (mDL) and the ISO
|
|
10
|
+
* track of the EU Digital Identity Wallet. This is the relying-party
|
|
11
|
+
* side: confirm that the data elements a holder presents were signed
|
|
12
|
+
* by the issuer and have not been altered.
|
|
13
|
+
*
|
|
14
|
+
* An mdoc's <code>IssuerSigned</code> structure carries the disclosed
|
|
15
|
+
* data elements (<code>nameSpaces</code>) and an <code>issuerAuth</code>
|
|
16
|
+
* that is a COSE_Sign1 (<code>b.cose</code>) over a Mobile Security
|
|
17
|
+
* Object (MSO). The MSO holds, per namespace, a SHA-256/384/512 digest
|
|
18
|
+
* of every issued element. <code>b.mdoc.verifyIssuerSigned</code>
|
|
19
|
+
* verifies the COSE signature with the issuer certificate carried in
|
|
20
|
+
* the COSE <code>x5chain</code> (label 33), parses the MSO, enforces
|
|
21
|
+
* its <code>validityInfo</code> window, and — the integrity check that
|
|
22
|
+
* makes selective disclosure trustworthy — recomputes the digest of
|
|
23
|
+
* every disclosed element (the full Tag-24 <code>IssuerSignedItemBytes</code>)
|
|
24
|
+
* and matches it against the MSO, constant-time. A disclosed element
|
|
25
|
+
* whose digest is absent or mismatched is refused.
|
|
26
|
+
*
|
|
27
|
+
* Signing algorithms follow <code>b.cose</code> verification: the
|
|
28
|
+
* classical ES256 / 384 / 512 and EdDSA that real mDL issuers use are
|
|
29
|
+
* accepted (consume-what-exists; the caller names the allowlist).
|
|
30
|
+
* <code>opts.trustAnchorsPem</code> additionally verifies the issuer
|
|
31
|
+
* certificate chain and its validity at the asserted time.
|
|
32
|
+
*
|
|
33
|
+
* <strong>Scope.</strong> This is issuer-data authentication
|
|
34
|
+
* (ISO 18013-5 §9.1.2.4) — the data is genuine and issuer-signed. The
|
|
35
|
+
* mdoc <em>device authentication</em> half (DeviceSigned / the
|
|
36
|
+
* SessionTranscript-bound holder-binding proof, §9.1.3) is deferred:
|
|
37
|
+
* it needs the live session transcript a verifier negotiates, so it is
|
|
38
|
+
* a presentation-protocol concern rather than a credential check.
|
|
39
|
+
* Composes <code>b.cose</code> + <code>b.cbor</code>; no new runtime
|
|
40
|
+
* dependency. Distinct from W3C VCDM (<code>b.vc</code>) and IETF
|
|
41
|
+
* SD-JWT VC (<code>b.auth.sdJwtVc</code>) — the three credential
|
|
42
|
+
* ecosystems.
|
|
43
|
+
*
|
|
44
|
+
* @card
|
|
45
|
+
* ISO 18013-5 mdoc / mDL issuer-data verification — checks the
|
|
46
|
+
* COSE_Sign1 IssuerAuth, the MSO validity window, and every disclosed
|
|
47
|
+
* element's digest against the Mobile Security Object. Composes
|
|
48
|
+
* b.cose + b.cbor; device-auth holder-binding deferred.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
var nodeCrypto = require("node:crypto");
|
|
52
|
+
var C = require("./constants");
|
|
53
|
+
var cbor = require("./cbor");
|
|
54
|
+
var cose = require("./cose");
|
|
55
|
+
var bCrypto = require("./crypto");
|
|
56
|
+
var validateOpts = require("./validate-opts");
|
|
57
|
+
var { defineClass } = require("./framework-error");
|
|
58
|
+
|
|
59
|
+
var MdocError = defineClass("MdocError", { alwaysPermanent: true });
|
|
60
|
+
|
|
61
|
+
var HDR_X5CHAIN = 33; // allow:raw-byte-literal allow:raw-time-literal — x5chain COSE header label (RFC 9360 is a spec number, not a size/duration)
|
|
62
|
+
var TAG_ENCODED_CBOR = 24; // allow:raw-byte-literal — RFC 8949 §3.4.5.1 embedded-CBOR tag
|
|
63
|
+
// Tags ISO 18013-5 uses in issuer data: tdate(0), epoch(1), embedded
|
|
64
|
+
// CBOR(24), full-date(1004, RFC 8943). Bounded — others are refused.
|
|
65
|
+
var ALLOWED_TAGS = [0, 1, TAG_ENCODED_CBOR, 1004];
|
|
66
|
+
var DIGEST_ALGS = { "SHA-256": "sha256", "SHA-384": "sha384", "SHA-512": "sha512" };
|
|
67
|
+
|
|
68
|
+
function _bytes(x, what) {
|
|
69
|
+
if (Buffer.isBuffer(x)) return x;
|
|
70
|
+
if (x instanceof Uint8Array) return Buffer.from(x);
|
|
71
|
+
throw new MdocError("mdoc/bad-input", "mdoc: " + what + " must be a Buffer / Uint8Array of CBOR");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// validityInfo dates are tdate (Tag 0, an RFC 3339 string) or epoch
|
|
75
|
+
// (Tag 1). Returns epoch-ms; fails closed on a malformed value.
|
|
76
|
+
function _validityMs(v, name) {
|
|
77
|
+
var raw = (v instanceof cbor.Tag) ? v.value : v;
|
|
78
|
+
if (typeof raw === "string") {
|
|
79
|
+
var ms = Date.parse(raw);
|
|
80
|
+
if (!isFinite(ms)) throw new MdocError("mdoc/bad-validity", "mdoc: validityInfo." + name + " is not a valid date: " + raw);
|
|
81
|
+
return ms;
|
|
82
|
+
}
|
|
83
|
+
if (typeof raw === "number" && isFinite(raw)) return raw * C.TIME.seconds(1); // epoch seconds → ms
|
|
84
|
+
throw new MdocError("mdoc/bad-validity", "mdoc: validityInfo." + name + " is missing or malformed");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function _mapGet(m, k) { return m instanceof Map ? m.get(k) : (m ? m[k] : undefined); }
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @primitive b.mdoc.verifyIssuerSigned
|
|
91
|
+
* @signature b.mdoc.verifyIssuerSigned(issuerSigned, opts)
|
|
92
|
+
* @since 0.12.40
|
|
93
|
+
* @status experimental
|
|
94
|
+
* @compliance gdpr, soc2
|
|
95
|
+
* @related b.cose.verify, b.vc.verify
|
|
96
|
+
*
|
|
97
|
+
* Verify the issuer-signed data of an ISO 18013-5 mdoc and return the
|
|
98
|
+
* disclosed elements. <code>issuerSigned</code> is the CBOR
|
|
99
|
+
* <code>IssuerSigned</code> map (the operator extracts it from the
|
|
100
|
+
* device response / QR). The COSE_Sign1 <code>issuerAuth</code> is
|
|
101
|
+
* verified with the issuer certificate from its <code>x5chain</code>
|
|
102
|
+
* header against the mandatory <code>opts.algorithms</code> allowlist;
|
|
103
|
+
* the MSO <code>validityInfo</code> window is enforced; and every
|
|
104
|
+
* disclosed element's digest is matched against the Mobile Security
|
|
105
|
+
* Object (a mismatch or absence is refused). Pass
|
|
106
|
+
* <code>opts.trustAnchorsPem</code> to also verify the issuer
|
|
107
|
+
* certificate chain.
|
|
108
|
+
*
|
|
109
|
+
* @opts
|
|
110
|
+
* {
|
|
111
|
+
* algorithms: string[], // required — accepted COSE alg names (ES256/384/512, EdDSA)
|
|
112
|
+
* trustAnchorsPem: string|string[], // optional issuer roots — enables chain + validity verification
|
|
113
|
+
* expectedDocType: string, // require the MSO docType to match (e.g. "org.iso.18013.5.1.mDL")
|
|
114
|
+
* at: Date, // validity instant (default now); must be a valid Date
|
|
115
|
+
* maxBytes: number, // forwarded to b.cbor.decode
|
|
116
|
+
* maxDepth: number,
|
|
117
|
+
* }
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* var out = await b.mdoc.verifyIssuerSigned(issuerSignedBytes, {
|
|
121
|
+
* algorithms: ["ES256"], expectedDocType: "org.iso.18013.5.1.mDL",
|
|
122
|
+
* });
|
|
123
|
+
* // → { docType, validityInfo, namespaces: { "org.iso.18013.5.1": { family_name, age_over_18, … } }, signerCert, alg }
|
|
124
|
+
*/
|
|
125
|
+
async function verifyIssuerSigned(issuerSigned, opts) {
|
|
126
|
+
validateOpts.requireObject(opts, "mdoc.verifyIssuerSigned", MdocError);
|
|
127
|
+
validateOpts(opts, ["algorithms", "trustAnchorsPem", "expectedDocType", "at", "maxBytes", "maxDepth"], "mdoc.verifyIssuerSigned");
|
|
128
|
+
if (!Array.isArray(opts.algorithms) || opts.algorithms.length === 0) {
|
|
129
|
+
throw new MdocError("mdoc/algorithms-required", "mdoc.verifyIssuerSigned: opts.algorithms is required");
|
|
130
|
+
}
|
|
131
|
+
var at = new Date();
|
|
132
|
+
if (opts.at !== undefined && opts.at !== null) {
|
|
133
|
+
if (!(opts.at instanceof Date) || !isFinite(opts.at.getTime())) {
|
|
134
|
+
throw new MdocError("mdoc/bad-at", "mdoc.verifyIssuerSigned: opts.at must be a valid Date");
|
|
135
|
+
}
|
|
136
|
+
at = opts.at;
|
|
137
|
+
}
|
|
138
|
+
var decodeOpts = { allowedTags: ALLOWED_TAGS, maxBytes: opts.maxBytes, maxDepth: opts.maxDepth };
|
|
139
|
+
|
|
140
|
+
var top = cbor.decode(_bytes(issuerSigned, "issuerSigned"), decodeOpts);
|
|
141
|
+
var nameSpaces = _mapGet(top, "nameSpaces");
|
|
142
|
+
var issuerAuth = _mapGet(top, "issuerAuth");
|
|
143
|
+
if (!Array.isArray(issuerAuth) || issuerAuth.length !== 4) {
|
|
144
|
+
throw new MdocError("mdoc/malformed", "mdoc.verifyIssuerSigned: issuerAuth must be a COSE_Sign1 (4-element array)");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// The signer certificate rides in the COSE x5chain (label 33): a
|
|
148
|
+
// single cert bstr or an array of bstrs, leaf first.
|
|
149
|
+
var unprotected = issuerAuth[1];
|
|
150
|
+
var x5 = _mapGet(unprotected, HDR_X5CHAIN);
|
|
151
|
+
var chain = Array.isArray(x5) ? x5 : (x5 != null ? [x5] : []);
|
|
152
|
+
if (!chain.length || !Buffer.isBuffer(chain[0])) {
|
|
153
|
+
throw new MdocError("mdoc/no-cert", "mdoc.verifyIssuerSigned: issuerAuth has no x5chain certificate (label 33)");
|
|
154
|
+
}
|
|
155
|
+
// The x5chain certificate is attacker-controlled — a malformed DER
|
|
156
|
+
// must surface as a clean error, not a raw OpenSSL throw.
|
|
157
|
+
var signerCert;
|
|
158
|
+
try { signerCert = new nodeCrypto.X509Certificate(chain[0]); }
|
|
159
|
+
catch (e) {
|
|
160
|
+
throw new MdocError("mdoc/bad-cert", "mdoc.verifyIssuerSigned: x5chain certificate is not valid DER: " + ((e && e.message) || e));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Verify the COSE_Sign1 signature with the embedded signer key.
|
|
164
|
+
var coseBytes = cbor.encode(issuerAuth);
|
|
165
|
+
var verified = await cose.verify(coseBytes, {
|
|
166
|
+
algorithms: opts.algorithms,
|
|
167
|
+
keyResolver: function () { return signerCert.publicKey; },
|
|
168
|
+
maxBytes: opts.maxBytes,
|
|
169
|
+
maxDepth: opts.maxDepth,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// payload = Tag 24 ( bstr .cbor MSO ).
|
|
173
|
+
var payloadTag = cbor.decode(verified.payload, decodeOpts);
|
|
174
|
+
var msoBytes = (payloadTag instanceof cbor.Tag && payloadTag.tag === TAG_ENCODED_CBOR) ? payloadTag.value : null;
|
|
175
|
+
if (!Buffer.isBuffer(msoBytes)) {
|
|
176
|
+
throw new MdocError("mdoc/malformed", "mdoc.verifyIssuerSigned: issuerAuth payload is not a Tag-24 MobileSecurityObject");
|
|
177
|
+
}
|
|
178
|
+
var mso = cbor.decode(msoBytes, decodeOpts);
|
|
179
|
+
|
|
180
|
+
var digestAlgName = _mapGet(mso, "digestAlgorithm");
|
|
181
|
+
var digestNode = DIGEST_ALGS[digestAlgName];
|
|
182
|
+
if (!digestNode) {
|
|
183
|
+
throw new MdocError("mdoc/bad-digest-alg", "mdoc.verifyIssuerSigned: unsupported MSO digestAlgorithm '" + digestAlgName + "'");
|
|
184
|
+
}
|
|
185
|
+
var docType = _mapGet(mso, "docType");
|
|
186
|
+
if (opts.expectedDocType !== undefined && docType !== opts.expectedDocType) {
|
|
187
|
+
throw new MdocError("mdoc/doctype-mismatch", "mdoc.verifyIssuerSigned: MSO docType '" + docType + "' does not match expectedDocType");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// validityInfo window (fail closed on malformed dates).
|
|
191
|
+
var vi = _mapGet(mso, "validityInfo");
|
|
192
|
+
if (!(vi instanceof Map) && (!vi || typeof vi !== "object")) {
|
|
193
|
+
throw new MdocError("mdoc/malformed", "mdoc.verifyIssuerSigned: MSO has no validityInfo");
|
|
194
|
+
}
|
|
195
|
+
var nowMs = at.getTime();
|
|
196
|
+
var validFromMs = _validityMs(_mapGet(vi, "validFrom"), "validFrom");
|
|
197
|
+
var validUntilMs = _validityMs(_mapGet(vi, "validUntil"), "validUntil");
|
|
198
|
+
if (nowMs < validFromMs) throw new MdocError("mdoc/not-yet-valid", "mdoc.verifyIssuerSigned: credential not yet valid");
|
|
199
|
+
if (nowMs > validUntilMs) throw new MdocError("mdoc/expired", "mdoc.verifyIssuerSigned: credential validity has passed");
|
|
200
|
+
|
|
201
|
+
// Match every disclosed element's digest against the MSO. The digest
|
|
202
|
+
// covers the full Tag-24 IssuerSignedItemBytes (ISO 18013-5 §9.1.2.5).
|
|
203
|
+
var valueDigests = _mapGet(mso, "valueDigests");
|
|
204
|
+
var out = {};
|
|
205
|
+
if (nameSpaces instanceof Map) {
|
|
206
|
+
var nsNames = Array.from(nameSpaces.keys());
|
|
207
|
+
for (var ni = 0; ni < nsNames.length; ni += 1) {
|
|
208
|
+
var ns = nsNames[ni];
|
|
209
|
+
var items = nameSpaces.get(ns);
|
|
210
|
+
var nsDigests = _mapGet(valueDigests, ns);
|
|
211
|
+
if (!Array.isArray(items) || !(nsDigests instanceof Map)) {
|
|
212
|
+
throw new MdocError("mdoc/malformed", "mdoc.verifyIssuerSigned: namespace '" + ns + "' has no matching valueDigests");
|
|
213
|
+
}
|
|
214
|
+
out[ns] = {};
|
|
215
|
+
var seen = Object.create(null); // dup-elementIdentifier guard (proto-safe)
|
|
216
|
+
for (var ii = 0; ii < items.length; ii += 1) {
|
|
217
|
+
var item = items[ii];
|
|
218
|
+
if (!(item instanceof cbor.Tag) || item.tag !== TAG_ENCODED_CBOR || !Buffer.isBuffer(item.value)) {
|
|
219
|
+
throw new MdocError("mdoc/malformed", "mdoc.verifyIssuerSigned: IssuerSignedItem is not a Tag-24 byte string");
|
|
220
|
+
}
|
|
221
|
+
var itemBytes = cbor.encode(new cbor.Tag(TAG_ENCODED_CBOR, item.value));
|
|
222
|
+
var digest = nodeCrypto.createHash(digestNode).update(itemBytes).digest();
|
|
223
|
+
var inner = cbor.decode(item.value, decodeOpts);
|
|
224
|
+
var digestID = _mapGet(inner, "digestID");
|
|
225
|
+
var expected = nsDigests.get(digestID);
|
|
226
|
+
if (!Buffer.isBuffer(expected) || !bCrypto.timingSafeEqual(digest, expected)) {
|
|
227
|
+
throw new MdocError("mdoc/digest-mismatch",
|
|
228
|
+
"mdoc.verifyIssuerSigned: disclosed element (digestID " + digestID + ", namespace " + ns + ") does not match the MSO");
|
|
229
|
+
}
|
|
230
|
+
// Refuse a duplicate elementIdentifier within a namespace — two
|
|
231
|
+
// signed values for one element is ambiguous; fail closed rather
|
|
232
|
+
// than silently keep the last.
|
|
233
|
+
var elementId = _mapGet(inner, "elementIdentifier");
|
|
234
|
+
if (seen[elementId]) {
|
|
235
|
+
throw new MdocError("mdoc/duplicate-element",
|
|
236
|
+
"mdoc.verifyIssuerSigned: namespace '" + ns + "' has duplicate elementIdentifier '" + elementId + "'");
|
|
237
|
+
}
|
|
238
|
+
seen[elementId] = true;
|
|
239
|
+
out[ns][elementId] = _mapGet(inner, "elementValue");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Optional issuer chain + validity at the asserted time.
|
|
245
|
+
if (opts.trustAnchorsPem !== undefined && opts.trustAnchorsPem !== null) {
|
|
246
|
+
var anchors = typeof opts.trustAnchorsPem === "string" ? [opts.trustAnchorsPem] : opts.trustAnchorsPem;
|
|
247
|
+
if (!Array.isArray(anchors) || anchors.length === 0 ||
|
|
248
|
+
!anchors.every(function (a) { return typeof a === "string" && a.length > 0; })) {
|
|
249
|
+
throw new MdocError("mdoc/bad-trust-anchors", "mdoc.verifyIssuerSigned: trustAnchorsPem must be a non-empty PEM string or array");
|
|
250
|
+
}
|
|
251
|
+
_verifyChain(chain, anchors, at);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
docType: docType,
|
|
256
|
+
version: _mapGet(mso, "version"),
|
|
257
|
+
digestAlgorithm: digestAlgName,
|
|
258
|
+
validityInfo: { validFrom: new Date(validFromMs), validUntil: new Date(validUntilMs) },
|
|
259
|
+
namespaces: out,
|
|
260
|
+
signerCert: signerCert.toString(),
|
|
261
|
+
alg: verified.alg,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Verify the leaf (chain[0]) chains to a supplied anchor and every cert
|
|
266
|
+
// is valid at `at`. Intermediates in the x5chain are consulted.
|
|
267
|
+
function _verifyChain(chainDer, anchorsPem, at) {
|
|
268
|
+
var anchors = anchorsPem.map(function (p) { return new nodeCrypto.X509Certificate(p); });
|
|
269
|
+
var pool = chainDer.map(function (d) { return new nodeCrypto.X509Certificate(d); });
|
|
270
|
+
var current = pool[0];
|
|
271
|
+
var atMs = at.getTime();
|
|
272
|
+
var steps = 0;
|
|
273
|
+
while (steps <= pool.length + 1) {
|
|
274
|
+
_assertValidAt(current, atMs);
|
|
275
|
+
for (var a = 0; a < anchors.length; a += 1) {
|
|
276
|
+
if (_issued(anchors[a], current)) { _assertValidAt(anchors[a], atMs); return; }
|
|
277
|
+
if (current.fingerprint256 === anchors[a].fingerprint256) return;
|
|
278
|
+
}
|
|
279
|
+
var parent = null;
|
|
280
|
+
for (var p = 0; p < pool.length; p += 1) {
|
|
281
|
+
if (pool[p].fingerprint256 !== current.fingerprint256 && _issued(pool[p], current)) { parent = pool[p]; break; }
|
|
282
|
+
}
|
|
283
|
+
if (!parent) {
|
|
284
|
+
throw new MdocError("mdoc/untrusted-chain", "mdoc.verifyIssuerSigned: issuer certificate does not chain to a supplied trust anchor");
|
|
285
|
+
}
|
|
286
|
+
current = parent;
|
|
287
|
+
steps += 1;
|
|
288
|
+
}
|
|
289
|
+
throw new MdocError("mdoc/chain-loop", "mdoc.verifyIssuerSigned: certificate chain did not terminate");
|
|
290
|
+
}
|
|
291
|
+
function _issued(issuer, subject) {
|
|
292
|
+
try { return subject.checkIssued(issuer) && subject.verify(issuer.publicKey); }
|
|
293
|
+
catch (_e) { return false; }
|
|
294
|
+
}
|
|
295
|
+
function _assertValidAt(cert, atMs) {
|
|
296
|
+
if (atMs < cert.validFromDate.getTime() || atMs > cert.validToDate.getTime()) {
|
|
297
|
+
throw new MdocError("mdoc/cert-expired", "mdoc.verifyIssuerSigned: certificate '" + cert.subject + "' is not valid at the asserted time");
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
module.exports = {
|
|
302
|
+
verifyIssuerSigned: verifyIssuerSigned,
|
|
303
|
+
DIGEST_ALGS: DIGEST_ALGS,
|
|
304
|
+
MdocError: MdocError,
|
|
305
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.12.40",
|
|
4
|
+
"date": "2026-05-24",
|
|
5
|
+
"headline": "`b.mdoc` — ISO 18013-5 mdoc / mDL issuer-data verification",
|
|
6
|
+
"summary": "Verify the issuer-signed data of an ISO/IEC 18013-5 mdoc — the credential format behind mobile driving licences (mDL) and the ISO track of the EU Digital Identity Wallet. This is the relying-party side: confirm that the data elements a holder presents were signed by the issuer and have not been altered. An mdoc's IssuerSigned carries the disclosed data elements and an issuerAuth that is a COSE_Sign1 (b.cose) over a Mobile Security Object (MSO) holding a per-element digest. b.mdoc.verifyIssuerSigned verifies the COSE signature with the issuer certificate from the COSE x5chain header, parses the MSO, enforces its validityInfo window, and recomputes each disclosed element's digest (the full Tag-24 IssuerSignedItemBytes) to match it against the MSO constant-time — the integrity check that makes selective disclosure trustworthy. An absent or mismatched digest is refused. Signing algorithms follow b.cose verification (the classical ES256/384/512 + EdDSA that real mDL issuers use; the caller names the allowlist); opts.trustAnchorsPem additionally verifies the issuer certificate chain. This completes the credential trio alongside W3C VCDM (b.vc) and IETF SD-JWT VC (b.auth.sdJwtVc). Composes b.cose + b.cbor; no new runtime dependency.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "`b.mdoc.verifyIssuerSigned(issuerSigned, opts)`",
|
|
13
|
+
"body": "Takes the CBOR `IssuerSigned` map (the operator extracts it from the device response / QR) and returns `{ docType, version, digestAlgorithm, validityInfo, namespaces, signerCert, alg }`. Verifies the COSE_Sign1 `issuerAuth` against the mandatory `opts.algorithms` allowlist using the issuer certificate from its `x5chain` (label 33) header; parses the Tag-24 Mobile Security Object; enforces the MSO `validityInfo` window against `opts.at` (default now; must be a valid Date; malformed dates fail closed); and recomputes the digest of every disclosed `IssuerSignedItem` (over the full Tag-24 bytes, with the MSO `digestAlgorithm` — SHA-256/384/512) to match the MSO `valueDigests` constant-time — an absent or mismatched digest is refused with `mdoc/digest-mismatch`. `opts.expectedDocType` pins the document type; `opts.trustAnchorsPem` (a PEM string or array) additionally verifies the issuer certificate chain and validity at the asserted time. A malformed `x5chain` certificate is refused with a clean `mdoc/bad-cert`. The mdoc device-authentication half (the SessionTranscript-bound holder-binding proof) is a presentation-protocol concern and is not part of issuer-data verification."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.12.41",
|
|
4
|
+
"date": "2026-05-24",
|
|
5
|
+
"headline": "`b.did` — W3C DID resolution (did:key + did:web) feeding the credential verifiers",
|
|
6
|
+
"summary": "Resolve W3C Decentralized Identifiers (DID Core 1.0) to verification keys — the link that lets a credential's issuer be named by a DID rather than a raw key. Resolve the issuer DID of a b.vc / b.mdoc / b.scitt credential to a node:crypto KeyObject and hand it to the verifier. did:key encodes the public key in the identifier (multicodec + base58btc), so resolution is deterministic and offline — Ed25519, P-256, P-384, and secp256k1 round-trip; did:web places the DID document at an HTTPS URL derived from the identifier, with the network fetch left to the operator (the framework parses the operator-fetched document and extracts its verification methods, as publicKeyMultibase or publicKeyJwk). b.did.keyToDid encodes a KeyObject as a did:key (an issuer naming itself), b.did.parse splits the identifier (and returns the did:web URL to fetch), and b.did.resolve returns the document and verification keys. DID Core 1.0 is a W3C Recommendation; the method specs (did:key W3C CCG report, did:web DID method registry — EUDI-mandated) are deployed-stable. Composes node:crypto; no new runtime dependency.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "`b.did.resolve(did, opts?)` / `b.did.keyToDid(publicKey)` / `b.did.parse(did)`",
|
|
13
|
+
"body": "`resolve` returns `{ didDocument, verificationMethods: [{ id, controller, type, publicKey }] }` with each `publicKey` a `node:crypto` KeyObject ready for `b.vc.verify` / `b.mdoc.verifyIssuerSigned` / `b.scitt.verifyStatement`. did:key resolves deterministically and offline (base58btc + multicodec → Ed25519 raw key or EC compressed point, rebuilt via SPKI); did:web requires the operator to pass the fetched DID document as `opts.document` (the URL to GET is on `b.did.parse(did).url`) and the document `id` must match the requested DID. A publicKeyJwk in a DID document is imported only after its `kty`/`crv` is allowlisted (Ed25519 / P-256 / P-384 / secp256k1) — an unexpected key type from an untrusted document is refused, not blindly imported. `keyToDid` encodes an Ed25519 / P-256 / P-384 / secp256k1 KeyObject as a did:key; `parse` derives the did:web HTTPS URL (`host[:port][:path]` → `https://host/path/did.json`, or `/.well-known/did.json`). Unknown methods, malformed base58, unsupported multicodec codes, and unsupported key types are each refused."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -2252,6 +2252,24 @@ async function testNoDuplicateCodeBlocks() {
|
|
|
2252
2252
|
],
|
|
2253
2253
|
reason: "v0.12.33 — opts / structure validation prelude (`validateOpts(allowedKeys) + chained required-field + typeof guards + typed-error throw`). cose.verify validates a COSE_Sign1 opts blob + decoded structure (RFC 9052); the peers each validate a distinct spec's shape (SD-JWT-VC issuer opts / break-glass policy set / JSCalendar object / DDL dual-control declaration / DSR request / FedCM well-known manifest / Android Asset Links / heartbeat config). Each throws a primitive-specific typed error; the shingle is the validateOpts-then-guard idiom, not behaviour. Same family as the v0.12.29 input-shape-validation cluster.",
|
|
2254
2254
|
},
|
|
2255
|
+
{
|
|
2256
|
+
mode: "family-subset",
|
|
2257
|
+
files: [
|
|
2258
|
+
"lib/mdoc.js:verifyIssuerSigned",
|
|
2259
|
+
"lib/tsa.js:verifyToken",
|
|
2260
|
+
"lib/vc.js:verify",
|
|
2261
|
+
],
|
|
2262
|
+
reason: "v0.12.40 — signature-verify entry preamble shared by three credential / token verifiers: `validateOpts(allowedKeys) + mandatory algorithms-allowlist check + opts.at valid-Date guard + publicKey/keyResolver presence check`, then divergent domain logic. tsa.verifyToken verifies an RFC 3161 timestamp token (CMS SignedData + message-imprint + EKU); vc.verify verifies a W3C VC-JOSE-COSE credential (JWS/COSE + VCDM structural + validity window); mdoc.verifyIssuerSigned verifies an ISO 18013-5 mdoc (COSE_Sign1 IssuerAuth + MSO valueDigests matching). Each consumes a different wire format, returns a different shape, and throws a primitive-specific typed error — the shingle is the validate-then-guard preamble, not behaviour. Same family as the v0.12.33 cose.verify cluster.",
|
|
2263
|
+
},
|
|
2264
|
+
{
|
|
2265
|
+
mode: "family-subset",
|
|
2266
|
+
files: [
|
|
2267
|
+
"lib/dual-control.js:create",
|
|
2268
|
+
"lib/mdoc.js:verifyIssuerSigned",
|
|
2269
|
+
"lib/tsa.js:verifyToken",
|
|
2270
|
+
],
|
|
2271
|
+
reason: "v0.12.40 — validateOpts-then-guard prelude shared between a create-style validator (dual-control.create builds a two-person-rule grant after validating its opts) and the timestamp / mdoc verifiers. The common shingle is the `validateOpts(allowedKeys) + chained guard + typed-error` idiom; the bodies diverge entirely (dual-control persists a control record; tsa/mdoc verify cryptographic structures). Same validate-then-guard family as the v0.12.29 / v0.12.33 clusters.",
|
|
2272
|
+
},
|
|
2255
2273
|
{
|
|
2256
2274
|
mode: "family-subset",
|
|
2257
2275
|
files: [
|
|
@@ -6197,8 +6215,16 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
6197
6215
|
// jwtExternal._assertAlgKtyMatch BEFORE createPublicKey on
|
|
6198
6216
|
// the browser-supplied DBSC binding JWK.
|
|
6199
6217
|
"lib/dbsc.js",
|
|
6218
|
+
// did.js — _jwkToKey allowlists the JWK's kty/crv (OKP/Ed25519 or
|
|
6219
|
+
// EC/P-256/P-384/secp256k1) and refuses any other type BEFORE
|
|
6220
|
+
// createPublicKey, which is the DID-context equivalent of the
|
|
6221
|
+
// alg/kty cross-check: a DID document carries verification keys,
|
|
6222
|
+
// not a verification alg (the consuming verifier — b.vc / b.mdoc —
|
|
6223
|
+
// supplies the alg allowlist), so there is no `alg` to pass to
|
|
6224
|
+
// _assertAlgKtyMatch; the kty/crv allowlist is the confusion guard.
|
|
6225
|
+
"lib/did.js",
|
|
6200
6226
|
],
|
|
6201
|
-
reason: "CVE-2026-22817 — every JWT verifier that resolves a JWK BY ATTACKER-CONTROLLED HEADER (kid / x5t) must cross-check the declared alg against the JWK's kty (and crv for EC) BEFORE handing the key to node:crypto.verify. Imports that skip the check are exactly the confused-deputy shape (RS256→HS256 family). The shared helper `jwtExternal._assertAlgKtyMatch(alg, jwk)` is the single point of enforcement; new code routes through it. Allowlist entries are sign-side / pinned-cert paths where the JWK is not attacker-supplied.",
|
|
6227
|
+
reason: "CVE-2026-22817 — every JWT verifier that resolves a JWK BY ATTACKER-CONTROLLED HEADER (kid / x5t) must cross-check the declared alg against the JWK's kty (and crv for EC) BEFORE handing the key to node:crypto.verify. Imports that skip the check are exactly the confused-deputy shape (RS256→HS256 family). The shared helper `jwtExternal._assertAlgKtyMatch(alg, jwk)` is the single point of enforcement; new code routes through it. Allowlist entries are sign-side / pinned-cert paths where the JWK is not attacker-supplied, or (did.js) where a kty/crv allowlist stands in for alg/kty because the format carries no verification alg.",
|
|
6202
6228
|
},
|
|
6203
6229
|
{
|
|
6204
6230
|
// CVE-2026-23552 — cross-realm JWT acceptance via non-CT iss
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Layer 0 — b.did (W3C DID resolution: did:key + did:web).
|
|
4
|
+
* Covers the published did:key spec vector (an independent-implementation
|
|
5
|
+
* oracle), Ed25519 / P-256 / P-384 / secp256k1 keyToDid ↔ resolve
|
|
6
|
+
* round-trips, did:web URL derivation + document extraction
|
|
7
|
+
* (publicKeyMultibase + publicKeyJwk), the integration with b.vc
|
|
8
|
+
* (resolve an issuer DID → verify its credential), and the refusal paths.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
var b = require("../../index");
|
|
12
|
+
var helpers = require("../helpers");
|
|
13
|
+
var check = helpers.check;
|
|
14
|
+
var nodeCrypto = require("node:crypto");
|
|
15
|
+
|
|
16
|
+
// Published did:key Ed25519 example (W3C CCG did:key test vector).
|
|
17
|
+
var SPEC_ED25519 = "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH";
|
|
18
|
+
|
|
19
|
+
function testSurface() {
|
|
20
|
+
check("b.did.parse is a function", typeof b.did.parse === "function");
|
|
21
|
+
check("b.did.resolve is a function", typeof b.did.resolve === "function");
|
|
22
|
+
check("b.did.keyToDid is a function", typeof b.did.keyToDid === "function");
|
|
23
|
+
check("b.did.MULTICODEC maps Ed25519", b.did.MULTICODEC[0xed].name === "Ed25519");
|
|
24
|
+
check("b.did.DidError is a class", typeof b.did.DidError === "function");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function testSpecVector() {
|
|
28
|
+
var r = b.did.resolve(SPEC_ED25519);
|
|
29
|
+
check("spec vector: resolves to an Ed25519 key", r.verificationMethods[0].publicKey.asymmetricKeyType === "ed25519");
|
|
30
|
+
check("spec vector: verificationMethod type Ed25519", r.verificationMethods[0].type === "Ed25519");
|
|
31
|
+
check("spec vector: DID document has assertionMethod + authentication", r.didDocument.assertionMethod.length === 1 && r.didDocument.authentication.length === 1);
|
|
32
|
+
// Re-encoding the resolved key must reproduce the exact published DID —
|
|
33
|
+
// an independent-implementation interop check.
|
|
34
|
+
check("spec vector: keyToDid round-trips the published DID", b.did.keyToDid(r.verificationMethods[0].publicKey) === SPEC_ED25519);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Key equality via JWK (an EC point imported from a compressed form may
|
|
38
|
+
// re-export with a different point_conversion_form, so SPKI bytes can
|
|
39
|
+
// differ even for the same key — compare the coordinates instead).
|
|
40
|
+
function _spkiEq(a, b2) {
|
|
41
|
+
var ja = a.export({ format: "jwk" }), jb = b2.export({ format: "jwk" });
|
|
42
|
+
return ja.kty === jb.kty && ja.crv === jb.crv && ja.x === jb.x && (ja.y || "") === (jb.y || "");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function testRoundTrips() {
|
|
46
|
+
// Ed25519
|
|
47
|
+
var ed = nodeCrypto.generateKeyPairSync("ed25519");
|
|
48
|
+
var edDid = b.did.keyToDid(ed.publicKey);
|
|
49
|
+
check("Ed25519: did:key starts z6Mk", edDid.indexOf("did:key:z6Mk") === 0);
|
|
50
|
+
check("Ed25519: resolve round-trips the key", _spkiEq(b.did.resolve(edDid).verificationMethods[0].publicKey, ed.publicKey));
|
|
51
|
+
|
|
52
|
+
// P-256 (the EUDI / mdoc curve) — did:key starts zDna
|
|
53
|
+
var p = nodeCrypto.generateKeyPairSync("ec", { namedCurve: "P-256" });
|
|
54
|
+
var pDid = b.did.keyToDid(p.publicKey);
|
|
55
|
+
check("P-256: did:key starts zDn", pDid.indexOf("did:key:zDn") === 0);
|
|
56
|
+
check("P-256: resolve round-trips the key", _spkiEq(b.did.resolve(pDid).verificationMethods[0].publicKey, p.publicKey));
|
|
57
|
+
|
|
58
|
+
// P-384
|
|
59
|
+
var p384 = nodeCrypto.generateKeyPairSync("ec", { namedCurve: "secp384r1" });
|
|
60
|
+
check("P-384: resolve round-trips the key", _spkiEq(b.did.resolve(b.did.keyToDid(p384.publicKey)).verificationMethods[0].publicKey, p384.publicKey));
|
|
61
|
+
|
|
62
|
+
// secp256k1
|
|
63
|
+
var k1 = nodeCrypto.generateKeyPairSync("ec", { namedCurve: "secp256k1" });
|
|
64
|
+
check("secp256k1: resolve round-trips the key", _spkiEq(b.did.resolve(b.did.keyToDid(k1.publicKey)).verificationMethods[0].publicKey, k1.publicKey));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function testCredentialIntegration() {
|
|
68
|
+
// The point of b.did: resolve an issuer DID, then verify its credential.
|
|
69
|
+
var issuer = nodeCrypto.generateKeyPairSync("ec", { namedCurve: "P-256" });
|
|
70
|
+
var issuerDid = b.did.keyToDid(issuer.publicKey);
|
|
71
|
+
var cred = {
|
|
72
|
+
"@context": ["https://www.w3.org/ns/credentials/v2"],
|
|
73
|
+
type: ["VerifiableCredential"], issuer: issuerDid,
|
|
74
|
+
credentialSubject: { id: "did:example:subject" },
|
|
75
|
+
};
|
|
76
|
+
var jws = await b.vc.issue(cred, { securing: "jose", alg: "ES256", privateKey: issuer.privateKey });
|
|
77
|
+
var resolvedKey = b.did.resolve(issuerDid).verificationMethods[0].publicKey;
|
|
78
|
+
var out = await b.vc.verify(jws, { algorithms: ["ES256"], publicKey: resolvedKey, expectedIssuer: issuerDid });
|
|
79
|
+
check("integration: resolved issuer DID verifies its b.vc credential", out.issuer === issuerDid);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function testDidWeb() {
|
|
83
|
+
var p = b.did.parse("did:web:example.com:issuers:42");
|
|
84
|
+
check("did:web: method + id parsed", p.method === "web" && p.id === "example.com:issuers:42");
|
|
85
|
+
check("did:web: path URL derived", p.url === "https://example.com/issuers/42/did.json");
|
|
86
|
+
check("did:web: bare host → .well-known", b.did.parse("did:web:example.com").url === "https://example.com/.well-known/did.json");
|
|
87
|
+
// Port encoded as %3A in the host is decoded to ':'.
|
|
88
|
+
check("did:web: %3A port decoded in host", b.did.parse("did:web:example.com%3A8443:a").url === "https://example.com:8443/a/did.json");
|
|
89
|
+
// Escaped reserved chars in a PATH segment stay verbatim (not turned
|
|
90
|
+
// into URL control syntax) — a path %3F must not become '?'.
|
|
91
|
+
check("did:web: escaped delimiter in path preserved", b.did.parse("did:web:example.com:foo%3Fbar").url === "https://example.com/foo%3Fbar/did.json");
|
|
92
|
+
// A malformed percent-escape must not throw a raw URIError.
|
|
93
|
+
var pctCode = (function () { try { b.did.parse("did:web:example.com:%"); return "ok"; } catch (e) { return e.code || e.name; } })();
|
|
94
|
+
check("did:web: malformed escape does not throw URIError", pctCode === "ok");
|
|
95
|
+
|
|
96
|
+
var issuer = nodeCrypto.generateKeyPairSync("ec", { namedCurve: "P-256" });
|
|
97
|
+
var webDid = "did:web:example.com";
|
|
98
|
+
// publicKeyJwk verification method
|
|
99
|
+
var docJwk = {
|
|
100
|
+
"@context": ["https://www.w3.org/ns/did/v1"], id: webDid,
|
|
101
|
+
verificationMethod: [{ id: webDid + "#k1", controller: webDid, type: "JsonWebKey2020", publicKeyJwk: issuer.publicKey.export({ format: "jwk" }) }],
|
|
102
|
+
};
|
|
103
|
+
var rJwk = b.did.resolve(webDid, { document: docJwk });
|
|
104
|
+
check("did:web: publicKeyJwk → KeyObject", _spkiEq(rJwk.verificationMethods[0].publicKey, issuer.publicKey));
|
|
105
|
+
|
|
106
|
+
// publicKeyMultibase verification method (did:key-style)
|
|
107
|
+
var ed = nodeCrypto.generateKeyPairSync("ed25519");
|
|
108
|
+
var multibase = b.did.keyToDid(ed.publicKey).slice("did:key:".length);
|
|
109
|
+
var docMb = {
|
|
110
|
+
"@context": ["https://www.w3.org/ns/did/v1"], id: webDid,
|
|
111
|
+
verificationMethod: [{ id: webDid + "#k1", controller: webDid, type: "Multikey", publicKeyMultibase: multibase }],
|
|
112
|
+
};
|
|
113
|
+
check("did:web: publicKeyMultibase → KeyObject", _spkiEq(b.did.resolve(webDid, { document: docMb }).verificationMethods[0].publicKey, ed.publicKey));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function testRefusals() {
|
|
117
|
+
function code(fn) { try { fn(); return "NO-THROW"; } catch (e) { return e.code; } }
|
|
118
|
+
check("not a DID refused", code(function () { b.did.parse("https://x"); }) === "did/bad-did");
|
|
119
|
+
check("unsupported method refused", code(function () { b.did.resolve("did:ion:abc"); }) === "did/unsupported-method");
|
|
120
|
+
check("did:web without document refused", code(function () { b.did.resolve("did:web:example.com"); }) === "did/document-required");
|
|
121
|
+
check("did:key non-multibase refused", code(function () { b.did.resolve("did:key:Qabc"); }) === "did/bad-did");
|
|
122
|
+
check("did:key bad base58 refused", code(function () { b.did.resolve("did:key:z0OIl"); }) === "did/bad-base58");
|
|
123
|
+
check("did:web document id mismatch refused", code(function () {
|
|
124
|
+
b.did.resolve("did:web:example.com", { document: { id: "did:web:evil.com", verificationMethod: [] } });
|
|
125
|
+
}) === "did/document-mismatch");
|
|
126
|
+
// RSA key cannot be a did:key
|
|
127
|
+
var rsa = nodeCrypto.generateKeyPairSync("rsa", { modulusLength: 2048 });
|
|
128
|
+
check("RSA key → unsupported", code(function () { b.did.keyToDid(rsa.publicKey); }) === "did/unsupported-key");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function run() {
|
|
132
|
+
testSurface();
|
|
133
|
+
testSpecVector();
|
|
134
|
+
testRoundTrips();
|
|
135
|
+
await testCredentialIntegration();
|
|
136
|
+
testDidWeb();
|
|
137
|
+
testRefusals();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = { run: run };
|
|
141
|
+
|
|
142
|
+
if (require.main === module) {
|
|
143
|
+
run().then(
|
|
144
|
+
function () { console.log("[did] OK — " + helpers.getChecks() + " checks passed"); },
|
|
145
|
+
function (e) { console.error("FAIL:", e && e.stack || e); process.exit(1); }
|
|
146
|
+
);
|
|
147
|
+
}
|