@blamejs/core 0.12.45 → 0.12.47

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.47 (2026-05-25) — **`b.cose.mac0` / `b.cose.macVerify0` — COSE_Mac0 (RFC 9052 §6.2).** Completes the COSE message-type set (COSE_Sign1 / COSE_Encrypt0 / COSE_Mac0) with single shared-key MACs. b.cose.mac0 produces a tagged COSE_Mac0 over a payload using HMAC-SHA-256/384/512 (the COSE-standard MAC algorithms; HMAC is symmetric, so its post-quantum strength is preserved). b.cose.macVerify0 recomputes the tag over the MAC_structure and compares it in constant time, with a mandatory algorithm allowlist. Use when both parties hold a shared key — e.g. an ECDH-derived key — and a non-repudiable signature is not wanted; detached payloads are supported (the proximity mdoc device-MAC variant and MACed CWTs are the consumers). Composes b.cbor + the framework's constant-time compare; no new runtime dependency. **Added:** *`b.cose.mac0(payload, opts)` / `b.cose.macVerify0(coseMac0, opts)`* — `mac0` emits a tagged COSE_Mac0 (tag 17) with `alg` (`HMAC-256/256` | `HMAC-384/384` | `HMAC-512/512`) in the protected header and the HMAC tag computed over the MAC_structure `["MAC0", protected, external_aad, payload]`; `detached: true` emits a nil payload. `macVerify0` reads the algorithm from the protected header (must be in the required `opts.algorithms` allowlist), recomputes the tag, and compares it constant-time — a wrong key, tampered tag, or `external_aad` mismatch is refused with `cose/bad-tag`; a detached payload is supplied via `opts.externalPayload`. `external_aad` binds context into the tag.
12
+
13
+ - v0.12.46 (2026-05-25) — **`b.mdoc.verifyDeviceAuth` — ISO 18013-5 mdoc device authentication.** Completes mdoc verification with the holder-binding half (ISO 18013-5 §9.1.3, signature variant). verifyIssuerSigned proves the data is issuer-signed; verifyDeviceAuth proves the presenter controls the device key the issuer bound into the MSO, so a captured issuer-signed document cannot be replayed by anyone else. The device's COSE_Sign1 (deviceSigned.deviceAuth.deviceSignature) is verified over the detached DeviceAuthentication structure ["DeviceAuthentication", SessionTranscript, DocType, DeviceNameSpacesBytes] using the device key from verifyIssuerSigned().deviceKey (now surfaced) and the operator-supplied SessionTranscript that binds the proof to this exact exchange (the presentation protocol — e.g. OpenID4VP — defines the transcript). Composes the v0.12.45 b.cose detached-payload verify + importKey. The MAC variant (deviceMac / COSE_Mac0, used in proximity flows with a reader ephemeral key) is deferred and refused with mdoc/device-mac-unsupported. No new runtime dependency. **Added:** *`b.mdoc.verifyDeviceAuth(opts)` + `deviceKey` on the verifyIssuerSigned result* — `verifyDeviceAuth({ deviceKey, deviceSigned, docType, sessionTranscript, algorithms })` imports the device key (a COSE_Key via `b.cose.importKey`, or a KeyObject), reconstructs the detached `DeviceAuthentication` payload, and verifies the `deviceSignature` COSE_Sign1 against the mandatory algorithm allowlist — a mismatched `sessionTranscript` or `docType` fails the signature. `verifyIssuerSigned` now returns `deviceKey` (the MSO `deviceKeyInfo.deviceKey`) so the two checks chain. The MAC variant (`deviceMac`) is refused with `mdoc/device-mac-unsupported` pending COSE_Mac0 + reader-key support.
14
+
11
15
  - v0.12.45 (2026-05-25) — **`b.cose` adds detached-payload sign/verify + `b.cose.importKey` (COSE_Key).** Two RFC 9052 / 9053 completions to the COSE substrate, both useable today and the prerequisites for mdoc device authentication and C2PA claim verification. Detached payloads (RFC 9052 §4.1): b.cose.sign with detached:true emits a COSE_Sign1 whose payload slot is nil — the signature still covers the payload, and the caller transmits it out of band; b.cose.verify takes the payload back as opts.externalPayload and binds it into the Sig_structure. A detached token verified without externalPayload is refused, and supplying externalPayload for an attached token is refused as ambiguous. COSE_Key import (RFC 9052 §7): b.cose.importKey turns a COSE_Key CBOR map into a node:crypto public KeyObject for b.cose.verify, accepting EC2 (P-256 / P-384 / P-521) and OKP (Ed25519) with the curve allowlisted so an unexpected key type is refused. No new runtime dependency. **Added:** *Detached COSE_Sign1 payloads + `b.cose.importKey(coseKey)`* — `b.cose.sign(payload, { detached: true })` emits a nil-payload COSE_Sign1 (the signature covers the payload regardless); `b.cose.verify(coseSign1, { externalPayload })` reconstructs the Sig_structure from the supplied payload, refusing a detached token with no `externalPayload` (`cose/detached-no-payload`) and refusing `externalPayload` on an attached token (`cose/payload-ambiguous`). `b.cose.importKey(coseKey)` maps a COSE_Key map (`kty` 2/EC2 with `crv` P-256/384/521, or `kty` 1/OKP with Ed25519) to a public KeyObject, allowlisting `kty`/`crv` and refusing anything else with `cose/unsupported-key` — the verification key embedded in an mdoc MSO or COSE_Key header is consumed this way.
12
16
 
13
17
  - v0.12.44 (2026-05-25) — **`b.did` adds the did:jwk method.** Completes b.did's method set with did:jwk alongside did:key and did:web. did:jwk encodes a public key as a base64url-encoded JWK directly in the identifier, so resolution is deterministic and offline — the same self-contained shape as did:key but in JWK form, which is what OpenID4VCI and the EU Digital Identity Wallet ecosystem commonly use. b.did.resolve("did:jwk:…") returns the verification key as a node:crypto KeyObject (kty/crv allowlisted — Ed25519 / P-256 / P-384 / secp256k1 — so an unexpected key type is refused, not blindly imported), and b.did.keyToDid(publicKey, { method: "jwk" }) produces a did:jwk from a key (the private member is stripped). No new runtime dependency. **Added:** *did:jwk in `b.did.resolve` / `b.did.keyToDid`* — `resolve` decodes the base64url JWK (bounded via `b.safeJson`), allowlists its `kty`/`crv`, and returns `{ didDocument, verificationMethods: [{ publicKey, … }] }` with the key as a KeyObject ready for `b.vc` / `b.mdoc` / `b.scitt`; `keyToDid(publicKey, { method: "jwk" })` encodes a public key as `did:jwk:<base64url-JWK>` (default remains `did:key`). Malformed base64url-JSON is refused with `did/bad-jwk` and an unsupported key type with `did/unsupported-key`.
package/README.md CHANGED
@@ -127,13 +127,13 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
127
127
  - **JSON / SQL / schema** — `b.safeJson` (with `maxKeys` cap defending CVE-2026-21717 V8 HashDoS), `b.safeBuffer`, `b.safeSql`, `b.safeSchema`
128
128
  - **URL + path** — `b.safeUrl` (IDN mixed-script / homograph refuse); `b.safeJsonPath` (refuses filter `?(...)`, deep-scan `$..`, script-shape `(@.x)` for safe Postgres JSONB ops)
129
129
  - **Binary codec** — `b.cbor` bounded deterministic CBOR (RFC 8949 §4.2): depth/size caps, indefinite-length + reserved-info + tag + duplicate-key refusal, `requireDeterministic` canonical-form check; the in-tree substrate under COSE / CWT / SCITT / WebAuthn attestation
130
- - **COSE signing + encryption** — `b.cose` COSE_Sign1 sign/verify (attached or detached payload) + COSE_Encrypt0 + `importKey` (COSE_Key → KeyObject) (RFC 9052) over `b.cbor`: classical ES256/384/512 + EdDSA (final COSE ids, interoperable today) plus ML-DSA-87 (PQC-forward, draft id); bounded + alg-allowlisted + crit-bypass-checked verification; single-recipient AEAD (ChaCha20/Poly1305 default, AES-GCM opt-in) with Enc_structure-bound AAD; the signed-statement substrate under SCITT / CWT / mdoc / C2PA
130
+ - **COSE messages** — `b.cose` the full RFC 9052 message-type set over `b.cbor`: COSE_Sign1 sign/verify (attached or detached payload), COSE_Encrypt0 single-recipient AEAD, COSE_Mac0 shared-key HMAC (mac0/macVerify0), plus `importKey` (COSE_Key → KeyObject). Signatures use classical ES256/384/512 + EdDSA (final COSE ids, interoperable today) plus ML-DSA-87 (PQC-forward, draft id); bounded + alg-allowlisted + crit-bypass-checked verification; AEAD ChaCha20/Poly1305 default (AES-GCM opt-in); the signed-statement substrate under SCITT / CWT / mdoc / C2PA
131
131
  - **CBOR Web Token** — `b.cwt` CWT sign/verify (RFC 8392) over `b.cose`: standard-claim mapping (iss/sub/aud/exp/nbf/iat/cti) + `exp`/`nbf` clock-skew enforcement + `iss`/`aud` matching; the CBOR-native JWT for constrained / IoT / FIDO / verifiable-credential contexts
132
132
  - **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
133
133
  - **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
134
134
  - **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
135
135
  - **Verifiable Credentials** — `b.vc` W3C Verifiable Credentials Data Model 2.0 (VC-JOSE-COSE): `issue` / `verify` a signed credential, and `present` / `verifyPresentation` a holder-signed Verifiable Presentation wrapping credentials (with `nonce`/`audience` holder-binding) — as a compact JWS (`vc+jwt` / `vp+jwt`, ES256/384/512 + EdDSA) or a COSE_Sign1 (`vc+cose` / `vp+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`
136
- - **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`
136
+ - **Mobile credentials (mDL)** — `b.mdoc` ISO/IEC 18013-5 verification: `verifyIssuerSigned` checks the COSE_Sign1 IssuerAuth (issuer cert from the `x5chain` header), the MSO validity window, and every disclosed element's digest against the MSO `valueDigests` (selective-disclosure integrity), with optional issuer-chain verification; `verifyDeviceAuth` proves holder binding (§9.1.3 signature variant) — the device COSE_Sign1 over the `DeviceAuthentication` structure with the MSO device key + protocol `sessionTranscript`. The ISO credential ecosystem alongside `b.vc` and `b.auth.sdJwtVc`. Composes `b.cose` + `b.cbor`
137
137
  - **Decentralized Identifiers** — `b.did` W3C DID resolution (DID Core 1.0): `resolve` a `did:key` / `did:jwk` (deterministic, offline — Ed25519 / P-256 / P-384 / secp256k1) or `did:web` (operator-fetched document) to `node:crypto` verification keys, so a credential's issuer DID resolves to the key that verifies it (`b.vc` / `b.mdoc` / `b.scitt`). `keyToDid` names a key as a `did:key` or `did:jwk`; document/JWK keys are kty/crv-allowlisted before import
138
138
  - **Document parsers** — `b.parsers` (XML / TOML / YAML / .env); `b.config` (schema-validated env)
139
139
  - **File-type detection** — `b.fileType` magic-byte content classification with deny-on-upload categories (image / document / archive / executable / etc.)
package/lib/cose.js CHANGED
@@ -53,6 +53,7 @@
53
53
 
54
54
  var nodeCrypto = require("node:crypto");
55
55
  var cbor = require("./cbor");
56
+ var bCrypto = require("./crypto");
56
57
  var validateOpts = require("./validate-opts");
57
58
  var { defineClass } = require("./framework-error");
58
59
 
@@ -548,6 +549,190 @@ function decrypt0(coseEncrypt0, opts) {
548
549
  return { plaintext: pt, alg: algName, protectedHeaders: protMap, unprotectedHeaders: unprotected };
549
550
  }
550
551
 
552
+ // ---- COSE_Mac0 (RFC 9052 §6.2) — single shared-key MAC ----
553
+
554
+ var COSE_MAC0_TAG = 17; // allow:raw-byte-literal — RFC 9052 COSE_Mac0 CBOR tag
555
+ // HMAC algorithms (RFC 9053 §3.1). Only the full-length tags are offered —
556
+ // the truncated HMAC 256/64 (id 4) is omitted. HMAC is symmetric, so its
557
+ // post-quantum strength is fine; these are the COSE-standard MAC algs.
558
+ var HMAC_NAME_TO_ID = { "HMAC-256/256": 5, "HMAC-384/384": 6, "HMAC-512/512": 7 }; // allow:raw-byte-literal — COSE HMAC algorithm ids (RFC 9053)
559
+ var HMAC_ID_TO_NAME = {};
560
+ Object.keys(HMAC_NAME_TO_ID).forEach(function (k) { HMAC_ID_TO_NAME[HMAC_NAME_TO_ID[k]] = k; });
561
+ function _hmacHash(algId) {
562
+ switch (algId) {
563
+ case 5: return "sha256";
564
+ case 6: return "sha384";
565
+ case 7: return "sha512";
566
+ default: throw new CoseError("cose/unknown-alg", "cose: unrecognized HMAC COSE alg id " + algId);
567
+ }
568
+ }
569
+
570
+ // MAC_structure (§6.3) = [ "MAC0", body_protected (bstr), external_aad (bstr), payload (bstr) ].
571
+ function _macStructure(protectedBstr, externalAad, payload) {
572
+ return cbor.encode(["MAC0", protectedBstr, externalAad, payload]);
573
+ }
574
+
575
+ /**
576
+ * @primitive b.cose.mac0
577
+ * @signature b.cose.mac0(payload, opts)
578
+ * @since 0.12.47
579
+ * @status stable
580
+ * @related b.cose.macVerify0, b.cose.sign
581
+ *
582
+ * Produce a tagged COSE_Mac0 (RFC 9052 §6.2) — a single shared-key MAC
583
+ * over <code>payload</code>. The MAC is HMAC-SHA-256 / 384 / 512 (the
584
+ * COSE-standard MAC algorithms; HMAC is symmetric, so post-quantum
585
+ * strength is preserved). Use when both parties hold a shared key (e.g.
586
+ * an ECDH-derived key) and a non-repudiable signature is not wanted.
587
+ * <code>detached: true</code> emits a nil payload, verified later with
588
+ * <code>opts.externalPayload</code>.
589
+ *
590
+ * @opts
591
+ * {
592
+ * alg: string, // "HMAC-256/256" | "HMAC-384/384" | "HMAC-512/512"
593
+ * key: Buffer, // shared symmetric key
594
+ * externalAad?: Buffer, // bound into the MAC
595
+ * detached?: boolean, // emit a nil payload (caller re-supplies it on verify)
596
+ * unprotectedHeaders?: object,
597
+ * }
598
+ *
599
+ * @example
600
+ * var mac = b.cose.mac0(Buffer.from("data"), { alg: "HMAC-256/256", key: sharedKey });
601
+ */
602
+ function mac0(payload, opts) {
603
+ validateOpts.requireObject(opts, "cose.mac0", CoseError);
604
+ validateOpts(opts, ["alg", "key", "externalAad", "detached", "unprotectedHeaders"], "cose.mac0");
605
+ if (!(opts.alg in HMAC_NAME_TO_ID)) {
606
+ throw new CoseError("cose/unsignable-alg", "cose.mac0: alg must be one of " + Object.keys(HMAC_NAME_TO_ID).join(" / "));
607
+ }
608
+ var key = _bstr(opts.key);
609
+ var algId = HMAC_NAME_TO_ID[opts.alg];
610
+ var protMap = new Map();
611
+ protMap.set(HDR_ALG, algId);
612
+ var protectedBstr = cbor.encode(protMap);
613
+
614
+ var unprot = new Map();
615
+ if (opts.unprotectedHeaders && typeof opts.unprotectedHeaders === "object") {
616
+ var uk = Object.keys(opts.unprotectedHeaders);
617
+ for (var i = 0; i < uk.length; i++) unprot.set(Number(uk[i]), opts.unprotectedHeaders[uk[i]]);
618
+ }
619
+
620
+ var payloadBytes = _bstr(payload);
621
+ var externalAad = opts.externalAad == null ? Buffer.alloc(0) : _bstr(opts.externalAad);
622
+ var tag = nodeCrypto.createHmac(_hmacHash(algId), key).update(_macStructure(protectedBstr, externalAad, payloadBytes)).digest();
623
+
624
+ var mac0arr = [protectedBstr, unprot, opts.detached ? null : payloadBytes, tag];
625
+ return cbor.encode(new cbor.Tag(COSE_MAC0_TAG, mac0arr));
626
+ }
627
+
628
+ /**
629
+ * @primitive b.cose.macVerify0
630
+ * @signature b.cose.macVerify0(coseMac0, opts)
631
+ * @since 0.12.47
632
+ * @status stable
633
+ * @related b.cose.mac0
634
+ *
635
+ * Verify a COSE_Mac0 (RFC 9052 §6.2) and return its payload. The HMAC
636
+ * tag is recomputed over the MAC_structure and compared in constant
637
+ * time; the <code>alg</code> from the protected header must be in
638
+ * <code>opts.algorithms</code>. A detached (nil) payload is supplied via
639
+ * <code>opts.externalPayload</code>.
640
+ *
641
+ * @opts
642
+ * {
643
+ * algorithms: string[], // required — accepted HMAC alg names (allowlist)
644
+ * key: Buffer, // the shared symmetric key
645
+ * externalAad?: Buffer,
646
+ * externalPayload?: Buffer, // required for a detached payload
647
+ * maxBytes?: number,
648
+ * maxDepth?: number,
649
+ * }
650
+ *
651
+ * @example
652
+ * var out = b.cose.macVerify0(mac, { algorithms: ["HMAC-256/256"], key: sharedKey });
653
+ * // → { payload: <Buffer>, alg: "HMAC-256/256", protectedHeaders: Map, unprotectedHeaders: Map }
654
+ */
655
+ function macVerify0(coseMac0, opts) {
656
+ validateOpts.requireObject(opts, "cose.macVerify0", CoseError);
657
+ validateOpts(opts, ["algorithms", "key", "externalAad", "externalPayload", "maxBytes", "maxDepth"], "cose.macVerify0");
658
+ if (!Array.isArray(opts.algorithms) || opts.algorithms.length === 0) {
659
+ throw new CoseError("cose/algorithms-required", "cose.macVerify0: opts.algorithms is required");
660
+ }
661
+ for (var ai = 0; ai < opts.algorithms.length; ai++) {
662
+ if (!(opts.algorithms[ai] in HMAC_NAME_TO_ID)) {
663
+ throw new CoseError("cose/unknown-alg", "cose.macVerify0: unknown algorithm '" + opts.algorithms[ai] + "'");
664
+ }
665
+ }
666
+ if (opts.key == null) throw new CoseError("cose/no-key", "cose.macVerify0: opts.key is required");
667
+ var key = _bstr(opts.key);
668
+
669
+ var decoded = cbor.decode(_bstr(coseMac0), { allowedTags: [COSE_MAC0_TAG], maxBytes: opts.maxBytes, maxDepth: opts.maxDepth });
670
+ var arr = (decoded instanceof cbor.Tag && decoded.tag === COSE_MAC0_TAG) ? decoded.value : decoded;
671
+ if (!Array.isArray(arr) || arr.length !== 4) {
672
+ throw new CoseError("cose/malformed", "cose.macVerify0: not a COSE_Mac0 (expected a 4-element array)");
673
+ }
674
+ var protectedBstr = arr[0];
675
+ var unprotected = arr[1];
676
+ var payload = arr[2];
677
+ var tag = arr[3];
678
+ if (!Buffer.isBuffer(protectedBstr) || !Buffer.isBuffer(tag)) {
679
+ throw new CoseError("cose/malformed", "cose.macVerify0: protected header and tag must be byte strings");
680
+ }
681
+ if (!(unprotected instanceof Map)) {
682
+ throw new CoseError("cose/malformed", "cose.macVerify0: unprotected header must be a CBOR map");
683
+ }
684
+ if (payload === null || payload === undefined) {
685
+ if (opts.externalPayload == null) {
686
+ throw new CoseError("cose/detached-no-payload", "cose.macVerify0: detached (nil) payload — pass opts.externalPayload");
687
+ }
688
+ payload = _bstr(opts.externalPayload);
689
+ } else if (opts.externalPayload != null) {
690
+ throw new CoseError("cose/payload-ambiguous", "cose.macVerify0: externalPayload supplied but the COSE_Mac0 carries an attached payload");
691
+ } else if (!Buffer.isBuffer(payload)) {
692
+ throw new CoseError("cose/malformed", "cose.macVerify0: payload must be a byte string (bstr)");
693
+ }
694
+
695
+ var protMap = protectedBstr.length === 0 ? new Map()
696
+ : cbor.decode(protectedBstr, { maxBytes: opts.maxBytes, maxDepth: opts.maxDepth });
697
+ if (!(protMap instanceof Map)) {
698
+ throw new CoseError("cose/malformed", "cose.macVerify0: protected header is not a CBOR map");
699
+ }
700
+ // crit-bypass defense (RFC 9052 §3.1) — same as b.cose.verify: every
701
+ // label a crit array names must be one this verifier understands AND
702
+ // be present in the protected header.
703
+ if (protMap.has(HDR_CRIT)) {
704
+ var crit = protMap.get(HDR_CRIT);
705
+ if (!Array.isArray(crit)) {
706
+ throw new CoseError("cose/bad-crit", "cose.macVerify0: crit (label 2) must be an array");
707
+ }
708
+ for (var ci = 0; ci < crit.length; ci++) {
709
+ if (UNDERSTOOD_LABELS.indexOf(crit[ci]) === -1) {
710
+ throw new CoseError("cose/crit-unknown",
711
+ "cose.macVerify0: crit lists header label " + crit[ci] + " which is not understood (RFC 9052 §3.1)");
712
+ }
713
+ if (!protMap.has(crit[ci])) {
714
+ throw new CoseError("cose/crit-absent",
715
+ "cose.macVerify0: crit lists label " + crit[ci] + " not present in the protected header");
716
+ }
717
+ }
718
+ }
719
+ var algId = protMap.get(HDR_ALG);
720
+ var algName = HMAC_ID_TO_NAME[algId];
721
+ if (algName === undefined) {
722
+ throw new CoseError("cose/unknown-alg", "cose.macVerify0: unrecognized protected MAC alg id " + algId);
723
+ }
724
+ if (opts.algorithms.indexOf(algName) === -1) {
725
+ throw new CoseError("cose/alg-not-allowed", "cose.macVerify0: alg '" + algName + "' is not in the allowlist");
726
+ }
727
+
728
+ var externalAad = opts.externalAad == null ? Buffer.alloc(0) : _bstr(opts.externalAad);
729
+ var expected = nodeCrypto.createHmac(_hmacHash(algId), key).update(_macStructure(protectedBstr, externalAad, payload)).digest();
730
+ if (!bCrypto.timingSafeEqual(expected, tag)) {
731
+ throw new CoseError("cose/bad-tag", "cose.macVerify0: MAC tag verification failed");
732
+ }
733
+ return { payload: payload, alg: algName, protectedHeaders: protMap, unprotectedHeaders: unprotected };
734
+ }
735
+
551
736
  // ---- COSE_Key (RFC 9052 §7 / RFC 9053 §7) → KeyObject ----
552
737
 
553
738
  // COSE_Key EC2 curve identifiers (RFC 9053 §7.1) → JWK crv names. Only
@@ -623,8 +808,12 @@ module.exports = {
623
808
  verify: verify,
624
809
  encrypt0: encrypt0,
625
810
  decrypt0: decrypt0,
811
+ mac0: mac0,
812
+ macVerify0: macVerify0,
626
813
  importKey: importKey,
627
814
  ALGORITHMS: ALG_NAME_TO_ID,
815
+ MAC_ALGORITHMS: HMAC_NAME_TO_ID,
816
+ COSE_MAC0_TAG: COSE_MAC0_TAG,
628
817
  AEAD_ALGORITHMS: AEAD_NAME_TO_ID,
629
818
  COSE_SIGN1_TAG: COSE_SIGN1_TAG,
630
819
  COSE_ENCRYPT0_TAG: COSE_ENCRYPT0_TAG,
package/lib/mdoc.js CHANGED
@@ -251,17 +251,138 @@ async function verifyIssuerSigned(issuerSigned, opts) {
251
251
  _verifyChain(chain, anchors, at);
252
252
  }
253
253
 
254
+ // The device key (MSO deviceKeyInfo.deviceKey, a COSE_Key) binds the
255
+ // holder — surfaced for b.mdoc.verifyDeviceAuth.
256
+ var deviceKeyInfo = _mapGet(mso, "deviceKeyInfo");
257
+ var deviceKey = deviceKeyInfo ? _mapGet(deviceKeyInfo, "deviceKey") : undefined;
258
+
254
259
  return {
255
260
  docType: docType,
256
261
  version: _mapGet(mso, "version"),
257
262
  digestAlgorithm: digestAlgName,
258
263
  validityInfo: { validFrom: new Date(validFromMs), validUntil: new Date(validUntilMs) },
259
264
  namespaces: out,
265
+ deviceKey: deviceKey,
260
266
  signerCert: signerCert.toString(),
261
267
  alg: verified.alg,
262
268
  };
263
269
  }
264
270
 
271
+ /**
272
+ * @primitive b.mdoc.verifyDeviceAuth
273
+ * @signature b.mdoc.verifyDeviceAuth(opts)
274
+ * @since 0.12.46
275
+ * @status experimental
276
+ * @compliance gdpr, soc2
277
+ * @related b.mdoc.verifyIssuerSigned, b.cose.verify
278
+ *
279
+ * Verify the device-authentication half of an ISO 18013-5 mdoc (§9.1.3,
280
+ * signature variant) — the proof that the holder controls the device key
281
+ * the issuer bound into the MSO, which stops a captured issuer-signed
282
+ * document from being replayed by anyone else. The device's COSE_Sign1
283
+ * (<code>deviceSigned.deviceAuth.deviceSignature</code>) is verified over
284
+ * the detached DeviceAuthentication structure
285
+ * (<code>["DeviceAuthentication", SessionTranscript, DocType,
286
+ * DeviceNameSpacesBytes]</code>) with the device key from the issuer-signed
287
+ * MSO (<code>verifyIssuerSigned(...).deviceKey</code>). The
288
+ * <code>sessionTranscript</code> binds the proof to this exact exchange
289
+ * and is supplied by the operator (the presentation protocol — e.g.
290
+ * OpenID4VP — defines it). The MAC variant (<code>deviceMac</code> /
291
+ * COSE_Mac0, used in proximity flows with a reader ephemeral key) is not
292
+ * yet supported and is refused with <code>mdoc/device-mac-unsupported</code>.
293
+ *
294
+ * @opts
295
+ * {
296
+ * deviceKey: object, // COSE_Key (from verifyIssuerSigned().deviceKey) or a KeyObject / PEM
297
+ * deviceSigned: object, // the DeviceSigned structure (CBOR bytes or decoded)
298
+ * docType: string, // the document type (must match the issuer-signed docType)
299
+ * sessionTranscript: any, // the SessionTranscript (CBOR bytes or decoded) bound by the protocol
300
+ * algorithms: string[], // required — accepted COSE alg names (ES256/384/512, EdDSA)
301
+ * maxBytes: number, // forwarded to b.cbor.decode
302
+ * maxDepth: number,
303
+ * }
304
+ *
305
+ * @example
306
+ * var issuer = await b.mdoc.verifyIssuerSigned(issuerSignedBytes, { algorithms: ["ES256"] });
307
+ * var dev = await b.mdoc.verifyDeviceAuth({ deviceKey: issuer.deviceKey, deviceSigned: deviceSignedBytes, docType: issuer.docType, sessionTranscript: transcript, algorithms: ["ES256"] });
308
+ * // → { docType, alg, deviceNamespaces }
309
+ */
310
+ async function verifyDeviceAuth(opts) {
311
+ validateOpts.requireObject(opts, "mdoc.verifyDeviceAuth", MdocError);
312
+ validateOpts(opts, ["deviceKey", "deviceSigned", "docType", "sessionTranscript", "algorithms", "maxBytes", "maxDepth"], "mdoc.verifyDeviceAuth");
313
+ if (!Array.isArray(opts.algorithms) || opts.algorithms.length === 0) {
314
+ throw new MdocError("mdoc/algorithms-required", "mdoc.verifyDeviceAuth: opts.algorithms is required");
315
+ }
316
+ if (typeof opts.docType !== "string" || !opts.docType) {
317
+ throw new MdocError("mdoc/bad-input", "mdoc.verifyDeviceAuth: opts.docType is required");
318
+ }
319
+ if (opts.sessionTranscript === undefined || opts.sessionTranscript === null) {
320
+ throw new MdocError("mdoc/no-session-transcript", "mdoc.verifyDeviceAuth: opts.sessionTranscript is required (the protocol-bound transcript)");
321
+ }
322
+ var decodeOpts = { allowedTags: ALLOWED_TAGS, maxBytes: opts.maxBytes, maxDepth: opts.maxDepth };
323
+
324
+ // Device key → KeyObject. Accept a COSE_Key (Map/object) via importKey,
325
+ // or an already-loaded KeyObject / PEM.
326
+ var deviceKeyObj;
327
+ if (opts.deviceKey && typeof opts.deviceKey === "object" && typeof opts.deviceKey.asymmetricKeyType === "string") {
328
+ deviceKeyObj = opts.deviceKey;
329
+ } else if (opts.deviceKey instanceof Map || (opts.deviceKey && typeof opts.deviceKey === "object")) {
330
+ deviceKeyObj = cose.importKey(opts.deviceKey);
331
+ } else if (typeof opts.deviceKey === "string") {
332
+ deviceKeyObj = opts.deviceKey; // PEM, resolved by b.cose
333
+ } else {
334
+ throw new MdocError("mdoc/no-device-key", "mdoc.verifyDeviceAuth: opts.deviceKey is required (a COSE_Key or KeyObject)");
335
+ }
336
+
337
+ var ds = (Buffer.isBuffer(opts.deviceSigned) || opts.deviceSigned instanceof Uint8Array)
338
+ ? cbor.decode(_bytes(opts.deviceSigned, "deviceSigned"), decodeOpts) : opts.deviceSigned;
339
+ var deviceNameSpaces = _mapGet(ds, "nameSpaces");
340
+ var deviceAuth = _mapGet(ds, "deviceAuth");
341
+ if (!deviceNameSpaces || !deviceAuth) {
342
+ throw new MdocError("mdoc/malformed", "mdoc.verifyDeviceAuth: deviceSigned must have nameSpaces + deviceAuth");
343
+ }
344
+ if (!(deviceNameSpaces instanceof cbor.Tag) || deviceNameSpaces.tag !== TAG_ENCODED_CBOR) {
345
+ throw new MdocError("mdoc/malformed", "mdoc.verifyDeviceAuth: deviceSigned.nameSpaces must be a Tag-24 DeviceNameSpacesBytes");
346
+ }
347
+ var deviceSignature = _mapGet(deviceAuth, "deviceSignature");
348
+ if (!deviceSignature) {
349
+ if (_mapGet(deviceAuth, "deviceMac") !== undefined) {
350
+ throw new MdocError("mdoc/device-mac-unsupported",
351
+ "mdoc.verifyDeviceAuth: the MAC variant (deviceMac / COSE_Mac0) is not supported — only deviceSignature");
352
+ }
353
+ throw new MdocError("mdoc/no-device-signature", "mdoc.verifyDeviceAuth: deviceAuth has no deviceSignature");
354
+ }
355
+
356
+ var st = (Buffer.isBuffer(opts.sessionTranscript) || opts.sessionTranscript instanceof Uint8Array)
357
+ ? cbor.decode(_bytes(opts.sessionTranscript, "sessionTranscript"), decodeOpts) : opts.sessionTranscript;
358
+
359
+ // DeviceAuthentication (ISO 18013-5 §9.1.3.4); the detached payload is
360
+ // its Tag-24-wrapped CBOR.
361
+ var deviceAuthentication = ["DeviceAuthentication", st, opts.docType, deviceNameSpaces];
362
+ var deviceAuthBytes = cbor.encode(new cbor.Tag(TAG_ENCODED_CBOR, cbor.encode(deviceAuthentication)));
363
+
364
+ var coseBytes = Array.isArray(deviceSignature) ? cbor.encode(deviceSignature) : _bytes(deviceSignature, "deviceSignature");
365
+ var out = await cose.verify(coseBytes, {
366
+ algorithms: opts.algorithms,
367
+ keyResolver: function () { return deviceKeyObj; },
368
+ externalPayload: deviceAuthBytes,
369
+ maxBytes: opts.maxBytes,
370
+ maxDepth: opts.maxDepth,
371
+ });
372
+
373
+ var deviceNamespaces = {};
374
+ try {
375
+ var dns = cbor.decode(deviceNameSpaces.value, decodeOpts);
376
+ if (dns instanceof Map) {
377
+ dns.forEach(function (items, ns) {
378
+ deviceNamespaces[ns] = items instanceof Map ? Object.fromEntries(items) : items;
379
+ });
380
+ }
381
+ } catch (_e) { /* device-released namespaces are optional + advisory */ }
382
+
383
+ return { docType: opts.docType, alg: out.alg, deviceNamespaces: deviceNamespaces };
384
+ }
385
+
265
386
  // Verify the leaf (chain[0]) chains to a supplied anchor and every cert
266
387
  // is valid at `at`. Intermediates in the x5chain are consulted.
267
388
  function _verifyChain(chainDer, anchorsPem, at) {
@@ -300,6 +421,7 @@ function _assertValidAt(cert, atMs) {
300
421
 
301
422
  module.exports = {
302
423
  verifyIssuerSigned: verifyIssuerSigned,
424
+ verifyDeviceAuth: verifyDeviceAuth,
303
425
  DIGEST_ALGS: DIGEST_ALGS,
304
426
  MdocError: MdocError,
305
427
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.12.45",
3
+ "version": "0.12.47",
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:f35a1ab6-b9b1-49fa-af2e-e468ce543070",
5
+ "serialNumber": "urn:uuid:4b602bbd-c8e6-4e4a-8b26-b0c570144b86",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-25T09:29:59.667Z",
8
+ "timestamp": "2026-05-25T11:11:40.479Z",
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.45",
22
+ "bom-ref": "@blamejs/core@0.12.47",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.12.45",
25
+ "version": "0.12.47",
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.45",
29
+ "purl": "pkg:npm/%40blamejs/core@0.12.47",
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.45",
57
+ "ref": "@blamejs/core@0.12.47",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]