@blamejs/core 0.12.44 → 0.12.46
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 +4 -0
- package/README.md +2 -2
- package/lib/cose.js +95 -10
- package/lib/mdoc.js +122 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
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.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.
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
11
15
|
- 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`.
|
|
12
16
|
|
|
13
17
|
- v0.12.43 (2026-05-25) — **`b.crypto.selfTest` — FIPS 140-3-style power-on self-test for the crypto stack.** A power-on self-test over the framework's cryptographic primitives — the integrity check a FIPS 140-3-validated module runs at start-up. The hash / XOF checks are known-answer tests against NIST FIPS 202 published vectors (SHA3-256 / SHA3-512 / SHAKE256), so they confirm the framework's hashing matches the standard rather than merely itself; the AEAD check round-trips XChaCha20-Poly1305 and confirms a tampered ciphertext is rejected; and the post-quantum checks run a pairwise-consistency + negative test for ML-KEM-1024, ML-DSA-87, and SLH-DSA-SHAKE-256f (a fresh keypair must encaps/decaps and sign/verify consistently and reject a tampered signature — FIPS 140-3 §10.3 pairwise consistency, since the runtime exposes no seed-injection API for a fixed-seed KAT). selfTest returns a structured report and, by default, throws on any failure so a broken crypto stack fails closed at boot rather than silently producing bad output. Operators in regulated deployments can run it at start-up as a self-integrity gate. **Added:** *`b.crypto.selfTest(opts?)`* — Runs eight checks — SHA3-512 / SHA3-256 / SHAKE256 known-answer tests (NIST FIPS 202), HMAC-SHA3-512 determinism, XChaCha20-Poly1305 round-trip + tamper-detect, and ML-KEM-1024 / ML-DSA-87 / SLH-DSA-SHAKE-256f pairwise-consistency + negative tests — and returns `{ ok, results: [{ name, ok, detail? }], failures, ranAt }`. Throws `crypto/self-test-failed` (with the report attached) on any failure unless `opts.throwOnFailure` is `false`. Exercises the framework's real primitive paths so a self-test failure means the shipped crypto is broken.
|
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 + COSE_Encrypt0 (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 / C2PA
|
|
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
|
|
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
|
|
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
|
@@ -143,6 +143,7 @@ function _toBeSigned(protectedBstr, externalAad, payload) {
|
|
|
143
143
|
* externalAad?: Buffer, // default empty — bound into the signature
|
|
144
144
|
* unprotectedHeaders?: object, // extra unprotected map entries (numeric keys)
|
|
145
145
|
* protectedHeaders?: object, // extra INTEGRITY-PROTECTED map entries (numeric keys); label 1 (alg) is reserved
|
|
146
|
+
* detached?: boolean, // emit a nil payload (RFC 9052 §4.1) — signature still covers it; caller transmits the payload separately
|
|
146
147
|
* }
|
|
147
148
|
*
|
|
148
149
|
* @example
|
|
@@ -152,7 +153,7 @@ function _toBeSigned(protectedBstr, externalAad, payload) {
|
|
|
152
153
|
*/
|
|
153
154
|
async function sign(payload, opts) {
|
|
154
155
|
validateOpts.requireObject(opts, "cose.sign", CoseError);
|
|
155
|
-
validateOpts(opts, ["alg", "privateKey", "kid", "contentType", "externalAad", "unprotectedHeaders", "protectedHeaders"], "cose.sign");
|
|
156
|
+
validateOpts(opts, ["alg", "privateKey", "kid", "contentType", "externalAad", "unprotectedHeaders", "protectedHeaders", "detached"], "cose.sign");
|
|
156
157
|
if (SIGNABLE.indexOf(opts.alg) === -1) {
|
|
157
158
|
throw new CoseError("cose/unsignable-alg",
|
|
158
159
|
"cose.sign: alg must be one of " + SIGNABLE.join(" / ") +
|
|
@@ -213,7 +214,10 @@ async function sign(payload, opts) {
|
|
|
213
214
|
? nodeCrypto.sign(null, toBeSigned, key)
|
|
214
215
|
: nodeCrypto.sign(params.nodeAlg, toBeSigned, { key: key, dsaEncoding: params.dsaEncoding });
|
|
215
216
|
|
|
216
|
-
|
|
217
|
+
// Detached payload (RFC 9052 §4.1): the COSE_Sign1 carries nil in the
|
|
218
|
+
// payload slot; the signature still covers the payload (above), and the
|
|
219
|
+
// caller transmits / re-supplies it out of band as externalPayload.
|
|
220
|
+
var sign1 = [protectedBstr, unprot, opts.detached ? null : payloadBytes, signature];
|
|
217
221
|
return cbor.encode(new cbor.Tag(COSE_SIGN1_TAG, sign1));
|
|
218
222
|
}
|
|
219
223
|
|
|
@@ -237,6 +241,7 @@ async function sign(payload, opts) {
|
|
|
237
241
|
* publicKey?: object, // the verification key (KeyObject / PEM)
|
|
238
242
|
* keyResolver?: function, // (protectedHeaders, unprotectedHeaders) → key
|
|
239
243
|
* externalAad?: Buffer, // must match what was signed
|
|
244
|
+
* externalPayload?: Buffer, // required when the COSE_Sign1 payload is detached (nil); bound into the Sig_structure
|
|
240
245
|
* maxBytes?: number, // forwarded to b.cbor.decode
|
|
241
246
|
* maxDepth?: number,
|
|
242
247
|
* }
|
|
@@ -247,7 +252,7 @@ async function sign(payload, opts) {
|
|
|
247
252
|
*/
|
|
248
253
|
async function verify(coseSign1, opts) {
|
|
249
254
|
validateOpts.requireObject(opts, "cose.verify", CoseError);
|
|
250
|
-
validateOpts(opts, ["algorithms", "publicKey", "keyResolver", "externalAad", "maxBytes", "maxDepth"], "cose.verify");
|
|
255
|
+
validateOpts(opts, ["algorithms", "publicKey", "keyResolver", "externalAad", "externalPayload", "maxBytes", "maxDepth"], "cose.verify");
|
|
251
256
|
if (!Array.isArray(opts.algorithms) || opts.algorithms.length === 0) {
|
|
252
257
|
throw new CoseError("cose/algorithms-required",
|
|
253
258
|
"cose.verify: opts.algorithms is required (no defaults — name the accepted algorithms)");
|
|
@@ -278,14 +283,23 @@ async function verify(coseSign1, opts) {
|
|
|
278
283
|
if (!Buffer.isBuffer(protectedBstr) || !Buffer.isBuffer(signature)) {
|
|
279
284
|
throw new CoseError("cose/malformed", "cose.verify: protected header and signature must be byte strings");
|
|
280
285
|
}
|
|
286
|
+
// Detached payload (RFC 9052 §4.1): a nil payload slot means the caller
|
|
287
|
+
// must supply the payload out of band via opts.externalPayload, which
|
|
288
|
+
// is then bound into the Sig_structure. Supplying externalPayload for
|
|
289
|
+
// an attached (non-nil) token is ambiguous and refused.
|
|
281
290
|
if (payload === null || payload === undefined) {
|
|
282
|
-
|
|
283
|
-
"cose
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
291
|
+
if (opts.externalPayload == null) {
|
|
292
|
+
throw new CoseError("cose/detached-no-payload",
|
|
293
|
+
"cose.verify: COSE_Sign1 has a detached (nil) payload — pass opts.externalPayload to verify it");
|
|
294
|
+
}
|
|
295
|
+
payload = _bstr(opts.externalPayload);
|
|
296
|
+
} else if (opts.externalPayload != null) {
|
|
297
|
+
throw new CoseError("cose/payload-ambiguous",
|
|
298
|
+
"cose.verify: opts.externalPayload was supplied but the COSE_Sign1 carries an attached payload");
|
|
299
|
+
} else if (!Buffer.isBuffer(payload)) {
|
|
300
|
+
// COSE_Sign1 payload is a bstr (RFC 9052 §4.2) — refuse a non-byte
|
|
301
|
+
// payload rather than return a value that violates the documented
|
|
302
|
+
// { payload: Buffer } shape.
|
|
289
303
|
throw new CoseError("cose/malformed", "cose.verify: payload must be a byte string (bstr)");
|
|
290
304
|
}
|
|
291
305
|
// The unprotected header is a CBOR map — refuse a non-map rather
|
|
@@ -534,11 +548,82 @@ function decrypt0(coseEncrypt0, opts) {
|
|
|
534
548
|
return { plaintext: pt, alg: algName, protectedHeaders: protMap, unprotectedHeaders: unprotected };
|
|
535
549
|
}
|
|
536
550
|
|
|
551
|
+
// ---- COSE_Key (RFC 9052 §7 / RFC 9053 §7) → KeyObject ----
|
|
552
|
+
|
|
553
|
+
// COSE_Key EC2 curve identifiers (RFC 9053 §7.1) → JWK crv names. Only
|
|
554
|
+
// the curves b.cose.verify has an algorithm for are accepted: P-256
|
|
555
|
+
// (ES256), P-384 (ES384), P-521 (ES512). secp256k1 is intentionally
|
|
556
|
+
// absent — there is no ES256K path here, so importing one would let a
|
|
557
|
+
// secp256k1 key be verified under ES256, breaking the COSE alg/curve
|
|
558
|
+
// binding (RFC 9053). Re-add with an explicit ES256K algorithm.
|
|
559
|
+
var COSE_EC2_CRV = { 1: "P-256", 2: "P-384", 3: "P-521" };
|
|
560
|
+
var COSE_KTY_OKP = 1;
|
|
561
|
+
var COSE_KTY_EC2 = 2;
|
|
562
|
+
var COSE_OKP_ED25519 = 6; // allow:raw-byte-literal — COSE OKP Ed25519 crv id (RFC 9053)
|
|
563
|
+
|
|
564
|
+
function _coseKeyBytes(v, what) {
|
|
565
|
+
if (Buffer.isBuffer(v)) return v;
|
|
566
|
+
if (v instanceof Uint8Array) return Buffer.from(v);
|
|
567
|
+
throw new CoseError("cose/bad-cose-key", "cose.importKey: COSE_Key " + what + " must be a byte string");
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* @primitive b.cose.importKey
|
|
572
|
+
* @signature b.cose.importKey(coseKey)
|
|
573
|
+
* @since 0.12.45
|
|
574
|
+
* @status stable
|
|
575
|
+
* @related b.cose.verify, b.cbor.decode
|
|
576
|
+
*
|
|
577
|
+
* Import a COSE_Key (RFC 9052 §7) — a CBOR map keyed by integer labels —
|
|
578
|
+
* as a <code>node:crypto</code> public KeyObject for
|
|
579
|
+
* <code>b.cose.verify</code>. Accepts the EC2 (<code>kty</code> 2:
|
|
580
|
+
* P-256 / P-384 / P-521) and OKP (<code>kty</code> 1: Ed25519) key
|
|
581
|
+
* types — the curves <code>b.cose.verify</code> has an algorithm for;
|
|
582
|
+
* the curve is allowlisted, so an unexpected key type (including
|
|
583
|
+
* secp256k1, which has no ES256K path here) is refused rather than
|
|
584
|
+
* imported. The verification key embedded in an mdoc MSO or a COSE_Key
|
|
585
|
+
* header is consumed this way.
|
|
586
|
+
*
|
|
587
|
+
* @example
|
|
588
|
+
* var key = b.cose.importKey(coseKeyMap); // → public KeyObject
|
|
589
|
+
* var out = await b.cose.verify(sign1, { algorithms: ["ES256"], publicKey: key });
|
|
590
|
+
*/
|
|
591
|
+
function importKey(coseKey) {
|
|
592
|
+
if (!(coseKey instanceof Map)) {
|
|
593
|
+
if (coseKey && typeof coseKey === "object" && !Array.isArray(coseKey)) {
|
|
594
|
+
var m = new Map();
|
|
595
|
+
Object.keys(coseKey).forEach(function (k) { m.set(Number(k), coseKey[k]); });
|
|
596
|
+
coseKey = m;
|
|
597
|
+
} else {
|
|
598
|
+
throw new CoseError("cose/bad-cose-key", "cose.importKey: expected a COSE_Key map");
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
var kty = coseKey.get(1);
|
|
602
|
+
var x = _coseKeyBytes(coseKey.get(-2), "x");
|
|
603
|
+
var jwk;
|
|
604
|
+
if (kty === COSE_KTY_OKP) {
|
|
605
|
+
if (coseKey.get(-1) !== COSE_OKP_ED25519) {
|
|
606
|
+
throw new CoseError("cose/unsupported-key", "cose.importKey: only OKP curve Ed25519 is supported");
|
|
607
|
+
}
|
|
608
|
+
jwk = { kty: "OKP", crv: "Ed25519", x: x.toString("base64url") };
|
|
609
|
+
} else if (kty === COSE_KTY_EC2) {
|
|
610
|
+
var crvName = COSE_EC2_CRV[coseKey.get(-1)];
|
|
611
|
+
if (!crvName) throw new CoseError("cose/unsupported-key", "cose.importKey: unsupported EC2 curve id " + coseKey.get(-1));
|
|
612
|
+
var y = _coseKeyBytes(coseKey.get(-3), "y");
|
|
613
|
+
jwk = { kty: "EC", crv: crvName, x: x.toString("base64url"), y: y.toString("base64url") };
|
|
614
|
+
} else {
|
|
615
|
+
throw new CoseError("cose/unsupported-key", "cose.importKey: kty must be OKP (1) or EC2 (2), got " + kty);
|
|
616
|
+
}
|
|
617
|
+
try { return nodeCrypto.createPublicKey({ key: jwk, format: "jwk" }); }
|
|
618
|
+
catch (e) { throw new CoseError("cose/bad-cose-key", "cose.importKey: could not import COSE_Key: " + ((e && e.message) || e)); }
|
|
619
|
+
}
|
|
620
|
+
|
|
537
621
|
module.exports = {
|
|
538
622
|
sign: sign,
|
|
539
623
|
verify: verify,
|
|
540
624
|
encrypt0: encrypt0,
|
|
541
625
|
decrypt0: decrypt0,
|
|
626
|
+
importKey: importKey,
|
|
542
627
|
ALGORITHMS: ALG_NAME_TO_ID,
|
|
543
628
|
AEAD_ALGORITHMS: AEAD_NAME_TO_ID,
|
|
544
629
|
COSE_SIGN1_TAG: COSE_SIGN1_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
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:
|
|
5
|
+
"serialNumber": "urn:uuid:059bfa5a-606e-407b-a262-1ee1d810affc",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-25T10:03:10.245Z",
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.12.46",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.12.
|
|
25
|
+
"version": "0.12.46",
|
|
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.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.12.46",
|
|
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.
|
|
57
|
+
"ref": "@blamejs/core@0.12.46",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|