@blamejs/core 0.12.38 → 0.12.40

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 CHANGED
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.12.x
10
10
 
11
+ - v0.12.40 (2026-05-24) — **`b.mdoc` — ISO 18013-5 mdoc / mDL issuer-data verification.** 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. **Added:** *`b.mdoc.verifyIssuerSigned(issuerSigned, opts)`* — 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.
12
+
13
+ - v0.12.39 (2026-05-24) — **`b.vc` — W3C Verifiable Credentials 2.0 (issue / verify, JOSE + COSE securing).** Issue and verify W3C Verifiable Credentials (VC Data Model 2.0, a W3C Recommendation) secured per Securing Verifiable Credentials using JOSE and COSE (VC-JOSE-COSE, also a W3C Recommendation, May 2025). A verifiable credential is a tamper-evident, signed set of claims an issuer makes about a subject — a diploma, a membership, a license, an age assertion. Two securing mechanisms are supported, both signing the credential itself (no JWT/CWT claims wrapper): JOSE produces a compact JWS with the vc+jwt media type, signed with ES256/384/512 or EdDSA; COSE produces a COSE_Sign1 (application/vc+cose) over b.cose, which also accepts ML-DSA-87 for PQC-forward deployments. b.vc.verify auto-detects the form from the input, requires an algorithm allowlist, always refuses the JOSE none algorithm, re-checks the VCDM 2.0 structural rules, and enforces the validFrom / validUntil window. This is the W3C credential model, distinct from the IETF SD-JWT VC already at b.auth.sdJwtVc. Composes b.cose; no new runtime dependency. **Added:** *`b.vc.issue(credential, opts)` / `b.vc.verify(secured, opts)`* — `issue` validates the credential against the VCDM 2.0 structural rules (the `credentials/v2` context first, a `VerifiableCredential` type, an issuer, a credential subject) and signs it: `securing: "jose"` returns a compact JWS string (`typ` header `vc+jwt`), `securing: "cose"` returns COSE_Sign1 bytes (`typ` header `application/vc+cose`, content type `application/vc`) via `b.cose`. The credential is the exact signed payload — no JWT/CWT claims are injected. `verify` auto-detects the securing form from the input (compact-JWS string vs. COSE_Sign1 bytes), verifies the signature against the mandatory `opts.algorithms` allowlist (the JOSE `none` algorithm is always refused), re-checks the structural rules, enforces the `validFrom` / `validUntil` window against `opts.at` (default now; must be a valid Date), and optionally matches `opts.expectedIssuer` against the credential issuer id. Returns `{ credential, securing, alg, issuer }`.
14
+
11
15
  - v0.12.38 (2026-05-24) — **`b.tsa` — RFC 3161 trusted timestamping client (build / parse / verify).** A timestamp authority binds a hash of your data to a trusted time, producing a token that proves the data existed at that instant — timestamp a release artifact, an audit-log checkpoint, a b.scitt signed statement, or a contract. b.tsa is the requester/verifier side of RFC 3161: buildRequest produces the DER TimeStampReq (the message imprint plus an optional nonce and a cert request), parseResponse reads the TimeStampResp (PKIStatus, failure-info bits, and the token), and verifyToken checks a token against your data and returns the asserted time. Verification is done in full per §2.4.2 / §2.3: the token is a CMS SignedData (b.cms) whose eContentType must be id-ct-TSTInfo; the message imprint must equal the hash of your data (constant-time); a sent nonce must round-trip; the signer certificate's extendedKeyUsage must be a critical, sole id-kp-timeStamping; and the CMS signature over the signed attributes must verify after the messageDigest attribute is matched to the recomputed eContent digest. An optional trust-anchor set verifies the certificate chain and validity at the asserted time. The HTTP transport to the TSA is the operator's to make. Composes b.cms and the in-tree ASN.1 DER codec; no new runtime dependency. **Added:** *`b.tsa.buildRequest(data, opts?)` / `b.tsa.parseResponse(der)` / `b.tsa.verifyToken(token, opts)`* — `buildRequest` returns `{ der, nonce, hashAlg, messageImprint }`; the imprint hash defaults to SHA-512 and may be SHA-256/384/512 or SHA3-256/512, a random 64-bit nonce and a certificate request are included by default, and a pre-hashed input is accepted with `hashed: true`. `parseResponse` returns `{ granted, status, statusString, failInfo, token }`, decoding the PKIFailureInfo bits for a non-granted response rather than throwing. `verifyToken` enforces the imprint match (`opts.data` or `opts.hash`), the nonce round-trip, the critical/sole `id-kp-timeStamping` EKU, and the CMS signature, returning `{ genTime, policy, serialHex, accuracy, hashAlg, signerCertPem }`; pass `opts.trustAnchorsPem` to also verify the certificate chain and validity at the asserted time. Timestamp tokens are third-party artifacts, so verification accepts the classical RSA (PKCS#1 v1.5 and PSS) and ECDSA-over-SHA-2 signatures that public TSAs emit — the same consume-what-exists posture as `b.cose` verification, not a framework signing default.
12
16
 
13
17
  - v0.12.37 (2026-05-24) — **`b.scitt.signStatement` / `b.scitt.verifyStatement` — SCITT signed statements over COSE (RFC 9052 + RFC 9597).** A SCITT signed statement is a signed, attributable claim about an artifact — a signed SBOM, a build attestation, a release approval. It is a COSE_Sign1 (b.cose) whose integrity-protected CWT_Claims header (label 15, RFC 9597) binds the issuer (who makes the statement) and the subject (the artifact it is about); the artifact, or a hash/reference to it, is the payload. signStatement places iss/sub in the protected header and declares the payload media type; verifyStatement checks the COSE signature (the algorithm allowlist is mandatory) and refuses any statement that lacks the iss/sub binding, with optional expected-issuer/subject matching. Signing uses the same algorithms as b.cose — classical ES256/384/512 + EdDSA (final COSE ids, interoperable today) plus ML-DSA-87 (PQC-forward). This is the issuer side of SCITT, buildable today on finalized RFCs; the transparency receipt (an inclusion proof from a transparency service, the COSE Receipts draft) is not yet shipped — a statement produced here is the input a transparency service registers, and the receipt format is the part still in flux. It opts in when COSE Receipts publishes. **Added:** *`b.scitt.signStatement(payload, opts)` / `b.scitt.verifyStatement(statement, opts)`* — `signStatement` produces a COSE_Sign1 whose protected CWT_Claims header (label 15) carries `iss` (`opts.issuer`) and `sub` (`opts.subject`), with the payload media type declared via `opts.contentType` and extra CWT claims allowed by integer label (iss/sub cannot be overridden through `opts.claims`). `verifyStatement` verifies the signature through `b.cose.verify` (passing `opts.algorithms` as the mandatory allowlist), then requires a CWT_Claims header with both `iss` and `sub` — a bare COSE_Sign1 with no such binding is refused with `scitt/missing-cwt-claims` — and enforces `expectedIssuer` / `expectedSubject` when given. Returns `{ payload, issuer, subject, cwtClaims, alg, protectedHeaders, unprotectedHeaders }`. Because the identity binding lives in the integrity-protected header it is covered by the signature and cannot be substituted without detection. **Changed:** *`b.cose.sign` accepts `protectedHeaders` and a media-type-string `contentType`* — `opts.protectedHeaders` (a numeric-keyed object or Map) adds extra integrity-protected header parameters — the CWT_Claims map (label 15) is the SCITT case. Label 1 (alg) is reserved and managed via `opts.alg`; setting it through `protectedHeaders` is refused with `cose/reserved-header`. `opts.contentType` now accepts a media-type string (RFC 9052 §3.1 tstr form, e.g. `"application/spdx+json"`) in addition to a CoAP Content-Format uint; a string was previously dropped.
package/README.md CHANGED
@@ -131,6 +131,8 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
131
131
  - **Entity Attestation Token** — `b.eat` EAT sign/verify (RFC 9711) over `b.cwt`: device + software attestation claims (ueid / oemid / hwmodel / measurements / submods) with verifier-nonce freshness binding, `dbgstat` debug-status policy, and `eat_profile` pinning
132
132
  - **SCITT signed statements** — `b.scitt` sign/verify a signed, attributable claim about an artifact (signed SBOM, build attestation, release approval) over `b.cose`: the issuer + subject bind in the integrity-protected CWT_Claims header (RFC 9597); verification refuses any statement missing the iss/sub binding. The issuer side, on finalized RFCs; the transparency receipt (COSE Receipts draft) opts in on publication
133
133
  - **Trusted timestamping** — `b.tsa` RFC 3161 timestamp client: `buildRequest` a TimeStampReq, `parseResponse`, and `verifyToken` against your data — the message imprint, sent nonce, critical/sole `id-kp-timeStamping` EKU, and CMS signature are all checked, with optional certificate-chain verification. Timestamp a release artifact, audit checkpoint, or signed statement against any RFC 3161 TSA. Composes `b.cms` + the in-tree ASN.1 DER codec
134
+ - **Verifiable Credentials** — `b.vc` W3C Verifiable Credentials Data Model 2.0 (VC-JOSE-COSE): `issue` / `verify` a signed credential as a compact JWS (`vc+jwt`, ES256/384/512 + EdDSA) or a COSE_Sign1 (`vc+cose`, + ML-DSA-87) over `b.cose`. VCDM structural + `validFrom`/`validUntil` checks; the JOSE `none` algorithm is always refused. The W3C model, distinct from the IETF SD-JWT VC at `b.auth.sdJwtVc`
135
+ - **Mobile credentials (mDL)** — `b.mdoc` ISO/IEC 18013-5 issuer-data verification: `verifyIssuerSigned` checks the COSE_Sign1 IssuerAuth (issuer cert from the `x5chain` header), the Mobile Security Object validity window, and every disclosed element's digest against the MSO `valueDigests` (the selective-disclosure integrity check), with optional issuer-chain verification. The ISO credential ecosystem alongside `b.vc` and `b.auth.sdJwtVc`. Composes `b.cose` + `b.cbor`
134
136
  - **Document parsers** — `b.parsers` (XML / TOML / YAML / .env); `b.config` (schema-validated env)
135
137
  - **File-type detection** — `b.fileType` magic-byte content classification with deny-on-upload categories (image / document / archive / executable / etc.)
136
138
  ### Content-safety gates
package/index.js CHANGED
@@ -461,6 +461,8 @@ module.exports = {
461
461
  eat: require("./lib/eat"),
462
462
  scitt: require("./lib/scitt"),
463
463
  tsa: require("./lib/tsa"),
464
+ vc: require("./lib/vc"),
465
+ mdoc: require("./lib/mdoc"),
464
466
  queue: queue,
465
467
  logStream: logStream,
466
468
  redact: redact,
package/lib/mdoc.js ADDED
@@ -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
+ };
package/lib/vc.js ADDED
@@ -0,0 +1,328 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.vc
4
+ * @nav Crypto
5
+ * @title Verifiable Credentials (W3C VCDM 2.0)
6
+ *
7
+ * @intro
8
+ * Issue and verify W3C Verifiable Credentials (VC Data Model 2.0, a
9
+ * W3C Recommendation) secured per "Securing Verifiable Credentials
10
+ * using JOSE and COSE" (VC-JOSE-COSE, also a W3C Recommendation). A
11
+ * verifiable credential is a tamper-evident, cryptographically-signed
12
+ * set of claims an issuer makes about a subject — a diploma, a
13
+ * membership, a license, an age assertion.
14
+ *
15
+ * Two securing mechanisms are supported, both putting the credential
16
+ * itself (not a JWT/CWT claims wrapper) as the signed payload:
17
+ * <strong>JOSE</strong> produces a compact JWS with the <code>vc+jwt</code>
18
+ * media type (<code>typ</code> header <code>"vc+jwt"</code>), signed
19
+ * with the classical ES256 / 384 / 512 or EdDSA JOSE algorithms;
20
+ * <strong>COSE</strong> produces a COSE_Sign1 (<code>application/vc+cose</code>)
21
+ * over <code>b.cose</code>, adding ML-DSA-87 (PQC-forward) to that set.
22
+ * <code>b.vc.verify</code> auto-detects the form from the input (a
23
+ * compact-JWS string vs. COSE_Sign1 bytes).
24
+ *
25
+ * <code>b.vc.issue(credential, opts)</code> validates the credential
26
+ * against the VCDM 2.0 structural rules (the <code>credentials/v2</code>
27
+ * context first, a <code>VerifiableCredential</code> type, an issuer,
28
+ * a credential subject) and signs it. <code>b.vc.verify(secured, opts)</code>
29
+ * verifies the signature (the algorithm allowlist is mandatory; the
30
+ * JOSE <code>none</code> algorithm is always refused), re-checks the
31
+ * structural rules, and enforces the <code>validFrom</code> /
32
+ * <code>validUntil</code> validity window. This is the W3C model and
33
+ * is distinct from the IETF SD-JWT VC at <code>b.auth.sdJwtVc</code>.
34
+ *
35
+ * @card
36
+ * W3C Verifiable Credentials 2.0 (VC-JOSE-COSE) — issue / verify a
37
+ * signed credential as a compact JWS (vc+jwt) or a COSE_Sign1
38
+ * (vc+cose), with VCDM structural + validity-window checks. Composes
39
+ * b.cose; the JOSE alg `none` is always refused.
40
+ */
41
+
42
+ var nodeCrypto = require("node:crypto");
43
+ var cose = require("./cose");
44
+ var safeJson = require("./safe-json");
45
+ var validateOpts = require("./validate-opts");
46
+ var { defineClass } = require("./framework-error");
47
+
48
+ var VcError = defineClass("VcError", { alwaysPermanent: true });
49
+
50
+ var VCDM_V2_CONTEXT = "https://www.w3.org/ns/credentials/v2";
51
+ var JOSE_TYP = "vc+jwt";
52
+ var COSE_TYP = "application/vc+cose";
53
+ var COSE_CONTENT_TYPE = "application/vc";
54
+ var HDR_COSE_TYP = 16; // allow:raw-byte-literal — COSE "typ" header label (RFC 9596)
55
+
56
+ // JOSE signature algorithms (final RFC 7518 / 8037), mapped to node
57
+ // verify parameters. ECDSA uses the IEEE-P1363 fixed-width encoding JOSE
58
+ // mandates (not ASN.1 DER). There is no signing default — the caller
59
+ // names the algorithm, mirroring b.cose.
60
+ var JOSE_ALGS = {
61
+ "ES256": { nodeHash: "sha256", dsaEncoding: "ieee-p1363" },
62
+ "ES384": { nodeHash: "sha384", dsaEncoding: "ieee-p1363" },
63
+ "ES512": { nodeHash: "sha512", dsaEncoding: "ieee-p1363" },
64
+ "EdDSA": { nodeHash: null },
65
+ };
66
+
67
+ function _b64urlJson(obj) {
68
+ return Buffer.from(JSON.stringify(obj), "utf8").toString("base64url");
69
+ }
70
+ function _toKey(key, kind) {
71
+ if (key && typeof key === "object" && typeof key.asymmetricKeyType === "string") return key;
72
+ try {
73
+ return kind === "private" ? nodeCrypto.createPrivateKey(key) : nodeCrypto.createPublicKey(key);
74
+ } catch (e) {
75
+ throw new VcError("vc/bad-key", "vc: could not load " + kind + " key: " + ((e && e.message) || e));
76
+ }
77
+ }
78
+
79
+ function _issuerId(cred) {
80
+ if (typeof cred.issuer === "string") return cred.issuer;
81
+ if (cred.issuer && typeof cred.issuer === "object" && typeof cred.issuer.id === "string") return cred.issuer.id;
82
+ return undefined;
83
+ }
84
+
85
+ // VCDM 2.0 structural rules; temporal checks only on verify.
86
+ function _validateVcdm(cred, opts) {
87
+ if (!cred || typeof cred !== "object" || Array.isArray(cred)) {
88
+ throw new VcError("vc/bad-credential", "vc: credential must be a JSON object");
89
+ }
90
+ var ctx = cred["@context"];
91
+ if (!Array.isArray(ctx) || ctx[0] !== VCDM_V2_CONTEXT) {
92
+ throw new VcError("vc/bad-context",
93
+ "vc: @context must be an array whose first element is '" + VCDM_V2_CONTEXT + "'");
94
+ }
95
+ var types = Array.isArray(cred.type) ? cred.type : [cred.type];
96
+ if (types.indexOf("VerifiableCredential") === -1) {
97
+ throw new VcError("vc/bad-type", "vc: type must include 'VerifiableCredential'");
98
+ }
99
+ if (_issuerId(cred) === undefined) {
100
+ throw new VcError("vc/no-issuer", "vc: issuer is required (a URL string or an object with an id)");
101
+ }
102
+ if (cred.credentialSubject === undefined || cred.credentialSubject === null) {
103
+ throw new VcError("vc/no-subject", "vc: credentialSubject is required");
104
+ }
105
+ // validFrom / validUntil, when present, MUST be valid XSD dateTimes
106
+ // (VCDM 2.0 §4.9). A malformed value fails closed at both issue and
107
+ // verify rather than silently skipping the window check.
108
+ var vf = _parseValidityField(cred, "validFrom");
109
+ var vu = _parseValidityField(cred, "validUntil");
110
+ if (opts && opts.temporal) {
111
+ var nowMs = opts.at.getTime();
112
+ if (vf !== null && nowMs < vf) {
113
+ throw new VcError("vc/not-yet-valid", "vc.verify: credential validFrom (" + cred.validFrom + ") is in the future");
114
+ }
115
+ if (vu !== null && nowMs > vu) {
116
+ throw new VcError("vc/expired", "vc.verify: credential validUntil (" + cred.validUntil + ") has passed");
117
+ }
118
+ }
119
+ }
120
+
121
+ function _parseValidityField(cred, name) {
122
+ if (cred[name] === undefined) return null;
123
+ if (typeof cred[name] !== "string") {
124
+ throw new VcError("vc/bad-validity", "vc: " + name + " must be an XSD dateTime string");
125
+ }
126
+ var ms = Date.parse(cred[name]);
127
+ if (!isFinite(ms)) {
128
+ throw new VcError("vc/bad-validity", "vc: " + name + " is not a valid dateTime: " + cred[name]);
129
+ }
130
+ return ms;
131
+ }
132
+
133
+ /**
134
+ * @primitive b.vc.issue
135
+ * @signature b.vc.issue(credential, opts)
136
+ * @since 0.12.39
137
+ * @status experimental
138
+ * @compliance gdpr, soc2
139
+ * @related b.vc.verify, b.cose.sign
140
+ *
141
+ * Validate a credential against the VCDM 2.0 structural rules and secure
142
+ * it. <code>securing: "jose"</code> returns a compact JWS string (media
143
+ * type <code>vc+jwt</code>) signed with an ES256/384/512 or EdDSA key;
144
+ * <code>securing: "cose"</code> returns COSE_Sign1 bytes (media type
145
+ * <code>application/vc+cose</code>) over <code>b.cose</code>, which also
146
+ * accepts <code>"ML-DSA-87"</code>. The credential itself is the signed
147
+ * payload — no JWT/CWT claims wrapper is added.
148
+ *
149
+ * @opts
150
+ * {
151
+ * securing: string, // "jose" (compact JWS) | "cose" (COSE_Sign1)
152
+ * alg: string, // JOSE: ES256/384/512 | EdDSA. COSE: + ML-DSA-87
153
+ * privateKey: object, // matching KeyObject or PEM
154
+ * kid: string, // optional key id (header)
155
+ * cty: string, // optional JOSE cty (e.g. "vc")
156
+ * }
157
+ *
158
+ * @example
159
+ * var jws = await b.vc.issue(credential, { securing: "jose", alg: "ES256", privateKey: key });
160
+ * // → a compact JWS string with typ "vc+jwt"
161
+ */
162
+ async function issue(credential, opts) {
163
+ validateOpts.requireObject(opts, "vc.issue", VcError);
164
+ validateOpts(opts, ["securing", "alg", "privateKey", "kid", "cty"], "vc.issue");
165
+ _validateVcdm(credential, null);
166
+ if (!opts.privateKey) throw new VcError("vc/no-key", "vc.issue: opts.privateKey is required");
167
+
168
+ if (opts.securing === "cose") {
169
+ var protectedHeaders = {};
170
+ protectedHeaders[HDR_COSE_TYP] = COSE_TYP;
171
+ return cose.sign(Buffer.from(JSON.stringify(credential), "utf8"), {
172
+ alg: opts.alg,
173
+ privateKey: opts.privateKey,
174
+ kid: opts.kid,
175
+ contentType: COSE_CONTENT_TYPE,
176
+ protectedHeaders: protectedHeaders,
177
+ });
178
+ }
179
+ if (opts.securing === "jose") {
180
+ var params = JOSE_ALGS[opts.alg];
181
+ if (!params) {
182
+ throw new VcError("vc/bad-alg", "vc.issue: JOSE securing requires alg ES256/384/512 or EdDSA (got " + opts.alg + ")");
183
+ }
184
+ var key = _toKey(opts.privateKey, "private");
185
+ var header = { alg: opts.alg, typ: JOSE_TYP };
186
+ if (typeof opts.kid === "string") header.kid = opts.kid;
187
+ if (typeof opts.cty === "string") header.cty = opts.cty;
188
+ var signingInput = _b64urlJson(header) + "." + _b64urlJson(credential);
189
+ var sig = params.nodeHash === null
190
+ ? nodeCrypto.sign(null, Buffer.from(signingInput, "ascii"), key)
191
+ : nodeCrypto.sign(params.nodeHash, Buffer.from(signingInput, "ascii"), { key: key, dsaEncoding: params.dsaEncoding });
192
+ return signingInput + "." + sig.toString("base64url");
193
+ }
194
+ throw new VcError("vc/bad-securing", "vc.issue: securing must be 'jose' or 'cose'");
195
+ }
196
+
197
+ function _verifyJose(token, opts) {
198
+ var parts = token.split(".");
199
+ if (parts.length !== 3) {
200
+ throw new VcError("vc/malformed", "vc.verify: not a compact JWS (expected three dot-separated segments)");
201
+ }
202
+ var header;
203
+ try { header = safeJson.parse(Buffer.from(parts[0], "base64url").toString("utf8")); }
204
+ catch (_e) { throw new VcError("vc/malformed", "vc.verify: JWS header is not valid base64url-JSON"); }
205
+ if (!header || header.typ !== JOSE_TYP) {
206
+ throw new VcError("vc/bad-typ", "vc.verify: JWS typ must be '" + JOSE_TYP + "'");
207
+ }
208
+ // crit-bypass defense (RFC 7515 §4.1.11): a `crit` header marks
209
+ // extensions the verifier MUST understand and process. This verifier
210
+ // implements no critical extensions, so any `crit` is refused rather
211
+ // than ignored — accepting it would mean honoring a credential under
212
+ // weaker semantics than the issuer marked mandatory.
213
+ if (header.crit !== undefined) {
214
+ throw new VcError("vc/crit-unsupported",
215
+ "vc.verify: JWS 'crit' header lists extensions this verifier does not support (RFC 7515 §4.1.11)");
216
+ }
217
+ if (header.alg === "none" || !JOSE_ALGS[header.alg]) {
218
+ throw new VcError("vc/bad-alg", "vc.verify: unsupported or unsecured JWS alg '" + header.alg + "'");
219
+ }
220
+ if (opts.algorithms.indexOf(header.alg) === -1) {
221
+ throw new VcError("vc/alg-not-allowed", "vc.verify: alg '" + header.alg + "' is not in the allowlist");
222
+ }
223
+ var params = JOSE_ALGS[header.alg];
224
+ var pub = opts.publicKey ? _toKey(opts.publicKey, "public") : _toKey(opts.keyResolver(header), "public");
225
+ var signingInput = parts[0] + "." + parts[1];
226
+ var sig = Buffer.from(parts[2], "base64url");
227
+ var ok = params.nodeHash === null
228
+ ? nodeCrypto.verify(null, Buffer.from(signingInput, "ascii"), pub, sig)
229
+ : nodeCrypto.verify(params.nodeHash, Buffer.from(signingInput, "ascii"), { key: pub, dsaEncoding: params.dsaEncoding }, sig);
230
+ if (!ok) throw new VcError("vc/bad-signature", "vc.verify: JWS signature did not verify");
231
+ var credential;
232
+ try { credential = safeJson.parse(Buffer.from(parts[1], "base64url").toString("utf8")); }
233
+ catch (_e) { throw new VcError("vc/malformed", "vc.verify: JWS payload is not valid base64url-JSON"); }
234
+ return { credential: credential, alg: header.alg };
235
+ }
236
+
237
+ async function _verifyCose(bytes, opts) {
238
+ var algorithms = opts.algorithms.filter(function (a) { return a in cose.ALGORITHMS; });
239
+ if (!algorithms.length) {
240
+ throw new VcError("vc/no-cose-alg", "vc.verify: opts.algorithms has no COSE algorithm for a vc+cose credential");
241
+ }
242
+ var out = await cose.verify(bytes, {
243
+ algorithms: algorithms,
244
+ publicKey: opts.publicKey,
245
+ keyResolver: opts.keyResolver,
246
+ });
247
+ var typ = out.protectedHeaders.get(HDR_COSE_TYP);
248
+ if (typ !== undefined && typ !== COSE_TYP) {
249
+ throw new VcError("vc/bad-typ", "vc.verify: COSE typ header is '" + typ + "', expected '" + COSE_TYP + "'");
250
+ }
251
+ var credential;
252
+ try { credential = safeJson.parse(out.payload.toString("utf8")); }
253
+ catch (_e) { throw new VcError("vc/malformed", "vc.verify: COSE payload is not valid JSON"); }
254
+ return { credential: credential, alg: out.alg };
255
+ }
256
+
257
+ /**
258
+ * @primitive b.vc.verify
259
+ * @signature b.vc.verify(secured, opts)
260
+ * @since 0.12.39
261
+ * @status experimental
262
+ * @compliance gdpr, soc2
263
+ * @related b.vc.issue, b.cose.verify
264
+ *
265
+ * Verify a secured verifiable credential and return the credential. The
266
+ * securing form is auto-detected (a compact-JWS string vs. COSE_Sign1
267
+ * bytes); the algorithm allowlist is mandatory and the JOSE
268
+ * <code>none</code> algorithm is always refused. After the signature,
269
+ * the VCDM 2.0 structural rules are re-checked and the
270
+ * <code>validFrom</code> / <code>validUntil</code> window is enforced
271
+ * against <code>opts.at</code> (default: now).
272
+ *
273
+ * @opts
274
+ * {
275
+ * algorithms: string[], // required — accepted alg names (allowlist)
276
+ * publicKey: object, // verification key (KeyObject / PEM)
277
+ * keyResolver: function, // (header) → key (alternative to publicKey)
278
+ * expectedIssuer: string, // require the credential issuer (id) to match
279
+ * at: Date, // validity instant (default: now); must be a valid Date
280
+ * }
281
+ *
282
+ * @example
283
+ * var out = await b.vc.verify(jws, { algorithms: ["ES256"], publicKey: issuerPub, expectedIssuer: "did:example:123" });
284
+ * // → { credential, securing: "jose", alg: "ES256", issuer: "did:example:123" }
285
+ */
286
+ async function verify(secured, opts) {
287
+ validateOpts.requireObject(opts, "vc.verify", VcError);
288
+ validateOpts(opts, ["algorithms", "publicKey", "keyResolver", "expectedIssuer", "at"], "vc.verify");
289
+ if (!Array.isArray(opts.algorithms) || opts.algorithms.length === 0) {
290
+ throw new VcError("vc/algorithms-required", "vc.verify: opts.algorithms is required (name the accepted algorithms)");
291
+ }
292
+ if (!opts.publicKey && typeof opts.keyResolver !== "function") {
293
+ throw new VcError("vc/no-key", "vc.verify: pass publicKey or keyResolver");
294
+ }
295
+ var at = new Date();
296
+ if (opts.at !== undefined && opts.at !== null) {
297
+ if (!(opts.at instanceof Date) || !isFinite(opts.at.getTime())) {
298
+ throw new VcError("vc/bad-at", "vc.verify: opts.at must be a valid Date");
299
+ }
300
+ at = opts.at;
301
+ }
302
+
303
+ var securing, result;
304
+ if (typeof secured === "string") {
305
+ securing = "jose";
306
+ result = _verifyJose(secured, opts);
307
+ } else if (Buffer.isBuffer(secured) || secured instanceof Uint8Array) {
308
+ securing = "cose";
309
+ result = await _verifyCose(Buffer.from(secured), opts);
310
+ } else {
311
+ throw new VcError("vc/bad-input", "vc.verify: secured must be a compact-JWS string or COSE_Sign1 bytes");
312
+ }
313
+
314
+ _validateVcdm(result.credential, { temporal: true, at: at });
315
+ var issuer = _issuerId(result.credential);
316
+ if (opts.expectedIssuer !== undefined && issuer !== opts.expectedIssuer) {
317
+ throw new VcError("vc/issuer-mismatch", "vc.verify: credential issuer does not match expectedIssuer");
318
+ }
319
+ return { credential: result.credential, securing: securing, alg: result.alg, issuer: issuer };
320
+ }
321
+
322
+ module.exports = {
323
+ issue: issue,
324
+ verify: verify,
325
+ JOSE_ALGS: JOSE_ALGS,
326
+ VCDM_V2_CONTEXT: VCDM_V2_CONTEXT,
327
+ VcError: VcError,
328
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.12.38",
3
+ "version": "0.12.40",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:5183e917-7caf-48d3-b87d-5efa87791e85",
5
+ "serialNumber": "urn:uuid:6a95f7d3-c5ac-4994-955e-1fb82f6bb11b",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-25T02:32:18.883Z",
8
+ "timestamp": "2026-05-25T04:51:15.499Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/core@0.12.38",
22
+ "bom-ref": "@blamejs/core@0.12.40",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.12.38",
25
+ "version": "0.12.40",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "The Node framework that owns its stack.",
29
- "purl": "pkg:npm/%40blamejs/core@0.12.38",
29
+ "purl": "pkg:npm/%40blamejs/core@0.12.40",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/core@0.12.38",
57
+ "ref": "@blamejs/core@0.12.40",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]