@blamejs/core 0.12.33 → 0.12.35
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 -0
- package/index.js +2 -0
- package/lib/cwt.js +244 -0
- package/lib/eat.js +240 -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.35 (2026-05-24) — **`b.eat` — Entity Attestation Token (RFC 9711) over `b.cwt`.** An EAT is the token a Relying Party asks a device or software entity to produce to prove what it is and what state it is in — a freshness nonce, a Universal Entity ID, OEM / hardware identifiers, debug status, software measurements, and nested submodule attestations. `b.eat` is the RFC 9711 profile over the v0.12.34 `b.cwt`: it maps the EAT claim names to their IANA CWT claim-key integer labels and adds the attestation-specific verification on top of the CWT signature + time checks. The central control is the verifier-nonce binding: when the Relying Party supplies a fresh `expectedNonce`, the token's `eat_nonce` (claim 10) must match it (constant-time compare) — without it a captured attestation replays forever. `verify` also enforces a debug-status policy (`requireDebugDisabled` refuses an `enabled` or absent `dbgstat`) and pins the `eat_profile`. RFC 9711 is a finalized standard; signing follows `b.cwt` / `b.cose` (ES256/384/512 + EdDSA interoperable today, ML-DSA-87 PQC-forward). **Added:** *`b.eat.sign(claims, opts)` / `b.eat.verify(eat, opts)`* — `sign` maps EAT claim names (`nonce`, `ueid`, `oemid`, `hwmodel`, `dbgstat`, `eat_profile`, `swname`/`swversion`, `measurements`, `submods`, …) to their RFC 9711 integer labels and accepts the `dbgstat` enum by name (`disabled-since-boot` → 2); standard CWT claims (`iss` / `exp` / …) pass through. `verify` returns `{ claims, raw, alg, protectedHeaders }` with the labels mapped back to friendly names and `dbgstat` decoded to its enum name. Attestation enforcement: `expectedNonce` requires a matching `eat_nonce` (refused `eat/nonce-mismatch`, missing `eat/nonce-missing` — `eat_nonce` may be a single byte string or an array for multiple verifiers), `requireDebugDisabled` refuses a non-disabled `dbgstat` (`eat/debug-not-disabled`), and `expectedProfile` pins `eat_profile`. The signature, algorithm allowlist, and `exp`/`nbf` checks delegate to `b.cwt` / `b.cose`. · *`b.cwt.sign` accepts a `Map`* — `b.cwt.sign` now takes either a plain object (string keys, standard claims mapped by name) or a `Map`, which preserves integer claim keys verbatim — profiles like `b.eat` resolve their claim names to integer labels and pass them through without the keys being stringified. The plain-object path is unchanged.
|
|
12
|
+
|
|
13
|
+
- v0.12.34 (2026-05-24) — **`b.cwt` — CBOR Web Token (RFC 8392) sign / verify over `b.cose`.** A CWT is the CBOR-native counterpart to JWT — a signed claims set for constrained / IoT, FIDO attestation, and verifiable-credential contexts. `b.cwt` composes the v0.12.33 `b.cose` (COSE_Sign1 signature + mandatory algorithm allowlist) and v0.12.32 `b.cbor` (deterministic claims encoding) and layers the standard-claim handling on top: `sign` takes a friendly claims object, maps the standard claims to their RFC 8392 §3.1.1 integer labels (iss=1, sub=2, aud=3, exp=4, nbf=5, iat=6, cti=7), and signs; `verify` checks the COSE signature, decodes the claims, and enforces the time + identity claims — a passed `exp` (with clock-skew tolerance), a future `nbf`, and an `iss` / `aud` mismatch against the expected values are each refused. Signing algorithms follow `b.cose`: classical ES256/384/512 + EdDSA (final COSE ids, interoperable today) and ML-DSA-87 (PQC-forward). RFC 8392 is a finalized standard, so CWTs produced here interoperate with other COSE/CWT implementations. **Added:** *`b.cwt.sign(claims, opts)` / `b.cwt.verify(cwt, opts)`* — `sign` maps standard claim names to integer labels and keeps custom claims verbatim; `exp` / `nbf` / `iat` must be non-negative integer NumericDates. `opts.tagged` wraps the COSE_Sign1 in the CWT CBOR tag 61 (RFC 8392 §6); `verify` accepts tagged or bare input. `verify` returns `{ claims, raw, alg, protectedHeaders }` — `claims` is the friendly object (labels mapped back to names), `raw` the integer-keyed Map. Standard-claim enforcement: `exp` past `now + clockSkewSec` (default 60s) is refused with `cwt/expired`, `nbf` beyond `now - skew` with `cwt/not-yet-valid`, and `expectedIssuer` / `expectedAudience` mismatches with `cwt/issuer-mismatch` / `cwt/audience-mismatch` (aud may be a single value or an array). `opts.now` overrides the clock for testing. The signature itself is verified by `b.cose.verify`, so a tampered token fails there.
|
|
14
|
+
|
|
11
15
|
- v0.12.33 (2026-05-24) — **`b.cose` — COSE_Sign1 sign / verify (RFC 9052) over the in-tree CBOR codec.** COSE is the signed-statement substrate under SCITT, CWT, and C2PA — the CBOR-native counterpart to JWS. `b.cose` ships COSE_Sign1 signing and verification composing the v0.12.32 `b.cbor` codec for the deterministic Sig_structure encoding. It signs with the classical COSE algorithms that interoperate today — ES256 / ES384 / ES512 (ECDSA) and EdDSA (Ed25519), all with final IANA algorithm ids (RFC 9053) — and with ML-DSA-87 (FIPS 204) for PQC-forward deployments. Verification accepts the same set, so the framework both produces COSE other implementations read today and consumes third-party COSE. There is no classical default: the caller names the algorithm and supplies the key. **Added:** *`b.cose.sign(payload, opts)` / `b.cose.verify(coseSign1, opts)`* — `sign` produces a tagged COSE_Sign1 with `alg` in the integrity-protected header; `verify` returns `{ payload, alg, protectedHeaders, unprotectedHeaders }`. The Sig_structure (`["Signature1", protected, external_aad, payload]`) is deterministically CBOR-encoded; ECDSA signatures use the IEEE-P1363 fixed-width encoding COSE mandates (RFC 9053 §2.1), not ASN.1 DER. `external_aad` is bound into the signature. v1 is single-signer with an attached payload; detached payload, COSE_Sign (multi-signer), COSE_Mac0, and COSE_Encrypt are deferred-with-condition (operator demand). **Security:** *Bounded, alg-allowlisted, crit-checked verification* — `verify` decodes the COSE_Sign1 bytes AND the protected-header bstr through the bounded `b.cbor.decode` (depth + size caps, indefinite-length / tag / duplicate-key refusal). `opts.algorithms` is a required allowlist (no defaults — name the accepted algorithms). A `crit` header (label 2) listing a header label the verifier does not understand is refused (RFC 9052 §3.1 crit-bypass defense), as is a `crit` label absent from the protected header. The COSE algorithm switch refuses any unrecognized id at the default branch. · *ML-DSA-87 COSE algorithm id is a non-final draft* — ML-DSA-87 uses COSE algorithm id `-50`, a requested (non-final) IANA assignment from draft-ietf-cose-dilithium — an ML-DSA-87 COSE_Sign1 is not yet broadly interoperable and the id may change; it is pinned deliberately with the re-open condition being IANA finalization. SLH-DSA-SHAKE-256f has no registered COSE algorithm id at all and cannot be represented in COSE. The COSE_Sign1 mechanism and the classical algorithms are stable; ML-DSA-87 is the forward-looking opt-in.
|
|
12
16
|
|
|
13
17
|
- v0.12.32 (2026-05-24) — **`b.cbor` — bounded, deterministic in-tree CBOR codec (RFC 8949).** CBOR is the binary serialization underneath COSE (RFC 9052), CWT, SCITT, and WebAuthn attestation — a foundational substrate the framework needs in-tree to build signed-statement primitives without a third-party parser. `b.cbor` is that codec, bounded by default like every parser the framework ships: a binary decoder is attack surface, so the defaults refuse the shapes a hostile encoder uses to exhaust memory or stack. The encoder emits Deterministically Encoded CBOR (RFC 8949 §4.2) — shortest-form heads, definite lengths, map keys sorted by encoded bytes, no indefinite-length items — so two semantically-equal values encode to byte-identical output, the property COSE signatures and SCITT receipts depend on. **Added:** *`b.cbor.encode(value, opts?)` / `b.cbor.decode(buffer, opts?)` / `b.cbor.Tag`* — `encode` produces deterministic CBOR from numbers (integers + float64), bigint (64-bit range), strings, `Buffer` / `Uint8Array`, arrays, `Map` or plain objects, `b.cbor.Tag`, and the simple values. `decode` returns the value with maps decoded to a `Map` (CBOR keys may be integers — COSE header labels are) and byte strings to `Buffer`. `b.cbor.Tag(tag, value)` carries a major-type-6 tagged item. `decode(buf, { requireDeterministic: true })` additionally asserts the input was itself canonically encoded (decode → re-encode → byte-compare), refusing a non-canonical re-encoding on a signature-verify path where it would be a malleability vector. **Security:** *Bounded-by-default decoder* — `maxDepth` (default 64, ceiling 256) caps nesting against stack exhaustion; `maxBytes` (default 16 MiB, ceiling 64 MiB) caps total input, and a declared string / array / map length exceeding the remaining bytes is refused before any allocation (no length-prefix memory bomb). Indefinite-length items (additional-info 31) are refused — a streaming-complexity / DoS vector forbidden by deterministic encoding. Reserved additional-info (28–30) is refused. Tags are refused unless allowlisted via `allowedTags` (a tag triggers semantic reprocessing — an un-vetted tag is a confused-deputy vector). Duplicate map keys (RFC 8949 §5.6) and trailing bytes after the data item are refused.
|
package/README.md
CHANGED
|
@@ -127,6 +127,8 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
127
127
|
- **URL + path** — `b.safeUrl` (IDN mixed-script / homograph refuse); `b.safeJsonPath` (refuses filter `?(...)`, deep-scan `$..`, script-shape `(@.x)` for safe Postgres JSONB ops)
|
|
128
128
|
- **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
|
|
129
129
|
- **COSE signing** — `b.cose` COSE_Sign1 sign/verify (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; the signed-statement substrate under SCITT / CWT / C2PA
|
|
130
|
+
- **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
|
|
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
|
|
130
132
|
- **Document parsers** — `b.parsers` (XML / TOML / YAML / .env); `b.config` (schema-validated env)
|
|
131
133
|
- **File-type detection** — `b.fileType` magic-byte content classification with deny-on-upload categories (image / document / archive / executable / etc.)
|
|
132
134
|
### Content-safety gates
|
package/index.js
CHANGED
|
@@ -457,6 +457,8 @@ module.exports = {
|
|
|
457
457
|
jose: { jwe: { experimental: require("./lib/jose-jwe-experimental") } },
|
|
458
458
|
cbor: require("./lib/cbor"),
|
|
459
459
|
cose: require("./lib/cose"),
|
|
460
|
+
cwt: require("./lib/cwt"),
|
|
461
|
+
eat: require("./lib/eat"),
|
|
460
462
|
queue: queue,
|
|
461
463
|
logStream: logStream,
|
|
462
464
|
redact: redact,
|
package/lib/cwt.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.cwt
|
|
4
|
+
* @nav Crypto
|
|
5
|
+
* @title CBOR Web Token (CWT)
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* RFC 8392 CBOR Web Token — the CBOR-native counterpart to JWT, a
|
|
9
|
+
* signed claims set for constrained / IoT, FIDO attestation, and
|
|
10
|
+
* verifiable-credential contexts. A CWT is a COSE_Sign1
|
|
11
|
+
* (<code>b.cose</code>) whose payload is a deterministically-encoded
|
|
12
|
+
* CBOR claims map (<code>b.cbor</code>) — this module composes both
|
|
13
|
+
* and layers the standard-claim handling on top.
|
|
14
|
+
*
|
|
15
|
+
* <code>b.cwt.sign(claims, opts)</code> accepts a friendly claims
|
|
16
|
+
* object; the standard claims are mapped to their RFC 8392 §3.1.1
|
|
17
|
+
* integer labels (<code>iss</code>=1, <code>sub</code>=2,
|
|
18
|
+
* <code>aud</code>=3, <code>exp</code>=4, <code>nbf</code>=5,
|
|
19
|
+
* <code>iat</code>=6, <code>cti</code>=7) and any other key is kept
|
|
20
|
+
* verbatim. <code>b.cwt.verify(cwt, opts)</code> verifies the COSE
|
|
21
|
+
* signature (delegating the mandatory algorithm allowlist to
|
|
22
|
+
* <code>b.cose.verify</code>), decodes the claims, and enforces the
|
|
23
|
+
* time + identity claims: a passed <code>exp</code>, a future
|
|
24
|
+
* <code>nbf</code>, an <code>iss</code> / <code>aud</code> mismatch
|
|
25
|
+
* against the expected values are each refused.
|
|
26
|
+
*
|
|
27
|
+
* Signing algorithms follow <code>b.cose</code>: the classical
|
|
28
|
+
* ES256/384/512 + EdDSA (final COSE ids, interoperable today) and
|
|
29
|
+
* ML-DSA-87 (PQC-forward). The optional CWT CBOR tag (61, RFC 8392
|
|
30
|
+
* §6) wraps the COSE_Sign1 when <code>opts.tagged</code> is set;
|
|
31
|
+
* <code>verify</code> accepts tagged and untagged input.
|
|
32
|
+
*
|
|
33
|
+
* @card
|
|
34
|
+
* RFC 8392 CBOR Web Token — sign / verify a CBOR claims set as a
|
|
35
|
+
* COSE_Sign1, with standard-claim mapping + exp / nbf / iss / aud
|
|
36
|
+
* enforcement. Composes b.cose + b.cbor.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
var cose = require("./cose");
|
|
40
|
+
var cbor = require("./cbor");
|
|
41
|
+
var C = require("./constants");
|
|
42
|
+
var validateOpts = require("./validate-opts");
|
|
43
|
+
var { defineClass } = require("./framework-error");
|
|
44
|
+
|
|
45
|
+
var CwtError = defineClass("CwtError", { alwaysPermanent: true });
|
|
46
|
+
|
|
47
|
+
// RFC 8392 §3.1.1 standard claim labels.
|
|
48
|
+
var STD = { iss: 1, sub: 2, aud: 3, exp: 4, nbf: 5, iat: 6, cti: 7 };
|
|
49
|
+
var STD_BY_LABEL = {};
|
|
50
|
+
Object.keys(STD).forEach(function (k) { STD_BY_LABEL[STD[k]] = k; });
|
|
51
|
+
|
|
52
|
+
var NUMERIC_DATE_CLAIMS = { exp: true, nbf: true, iat: true };
|
|
53
|
+
|
|
54
|
+
// CWT CBOR tag (RFC 8392 §6) — 61, encoded as the 2-byte head 0xd8 0x3d.
|
|
55
|
+
var CWT_TAG_PREFIX = Buffer.from([0xd8, 0x3d]); // allow:raw-byte-literal — CBOR tag-61 head (0xd8=tag 1-byte arg, 0x3d=61)
|
|
56
|
+
|
|
57
|
+
function _nowSec(opts) {
|
|
58
|
+
var ms = (opts && typeof opts.now === "number") ? opts.now : Date.now();
|
|
59
|
+
return Math.floor(ms / C.TIME.seconds(1));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Read a leading CBOR tag head (major type 6) in any of its encodings;
|
|
63
|
+
// returns { tag, len } or null if the buffer doesn't start with a tag.
|
|
64
|
+
function _readTagHead(buf) {
|
|
65
|
+
if (buf.length < 1 || (buf[0] >> 5) !== 6) return null; // allow:raw-byte-literal — CBOR major-type 6 (tag) shift
|
|
66
|
+
var ai = buf[0] & 0x1f;
|
|
67
|
+
if (ai < 24) return { tag: ai, len: 1 };
|
|
68
|
+
if (ai === 24) return buf.length >= 2 ? { tag: buf[1], len: 2 } : null; // allow:raw-byte-literal — CBOR additional-info threshold (RFC 8949 §3), not a size
|
|
69
|
+
if (ai === 25) return buf.length >= 3 ? { tag: buf.readUInt16BE(1), len: 3 } : null;
|
|
70
|
+
if (ai === 26) return buf.length >= 5 ? { tag: buf.readUInt32BE(1), len: 5 } : null;
|
|
71
|
+
if (ai === 27) return buf.length >= 9 ? { tag: Number(buf.readBigUInt64BE(1)), len: 9 } : null;
|
|
72
|
+
return null; // reserved / indefinite — not a tag head we accept
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @primitive b.cwt.sign
|
|
77
|
+
* @signature b.cwt.sign(claims, opts)
|
|
78
|
+
* @since 0.12.34
|
|
79
|
+
* @status stable
|
|
80
|
+
* @related b.cwt.verify, b.cose.sign
|
|
81
|
+
*
|
|
82
|
+
* Sign a claims set into a CWT (a COSE_Sign1 over the CBOR-encoded
|
|
83
|
+
* claims). Standard claims are mapped to their integer labels; custom
|
|
84
|
+
* claims (string or integer keys) are kept as given. <code>exp</code>
|
|
85
|
+
* / <code>nbf</code> / <code>iat</code> must be integer NumericDates
|
|
86
|
+
* (seconds since the epoch).
|
|
87
|
+
*
|
|
88
|
+
* @opts
|
|
89
|
+
* {
|
|
90
|
+
* alg: string, // COSE signing alg (ES256 / EdDSA / ML-DSA-87 / …)
|
|
91
|
+
* privateKey: object, // signing key (per b.cose.sign)
|
|
92
|
+
* kid?: string, // COSE kid header
|
|
93
|
+
* tagged?: boolean, // wrap in CWT CBOR tag 61 (default false)
|
|
94
|
+
* externalAad?: Buffer, // bound into the COSE signature
|
|
95
|
+
* }
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* var cwt = await b.cwt.sign(
|
|
99
|
+
* { iss: "issuer.example", sub: "device-42", exp: Math.floor(Date.now()/1000) + 3600, scope: "telemetry" },
|
|
100
|
+
* { alg: "ES256", privateKey: ecKey, kid: "k1" });
|
|
101
|
+
*/
|
|
102
|
+
async function sign(claims, opts) {
|
|
103
|
+
if (!claims || typeof claims !== "object" || Array.isArray(claims)) {
|
|
104
|
+
throw new CwtError("cwt/bad-claims", "cwt.sign: claims must be a plain object or a Map");
|
|
105
|
+
}
|
|
106
|
+
validateOpts.requireObject(opts, "cwt.sign", CwtError);
|
|
107
|
+
validateOpts(opts, ["alg", "privateKey", "kid", "tagged", "externalAad"], "cwt.sign");
|
|
108
|
+
|
|
109
|
+
// Accept a plain object (string keys) OR a Map. A Map preserves
|
|
110
|
+
// INTEGER claim keys verbatim — profiles like b.eat pass their
|
|
111
|
+
// already-resolved integer labels through and must not have them
|
|
112
|
+
// stringified.
|
|
113
|
+
var source = (claims instanceof Map)
|
|
114
|
+
? claims
|
|
115
|
+
: new Map(Object.keys(claims).map(function (k) { return [k, claims[k]]; }));
|
|
116
|
+
|
|
117
|
+
var map = new Map();
|
|
118
|
+
source.forEach(function (value, name) {
|
|
119
|
+
if (typeof name === "string" && NUMERIC_DATE_CLAIMS[name] &&
|
|
120
|
+
(typeof value !== "number" || !Number.isInteger(value) || value < 0)) {
|
|
121
|
+
throw new CwtError("cwt/bad-numeric-date",
|
|
122
|
+
"cwt.sign: claim '" + name + "' must be a non-negative integer NumericDate (seconds)");
|
|
123
|
+
}
|
|
124
|
+
map.set((typeof name === "string" && Object.prototype.hasOwnProperty.call(STD, name)) ? STD[name] : name, value);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
var claimsCbor = cbor.encode(map);
|
|
128
|
+
var coseSign1 = await cose.sign(claimsCbor, {
|
|
129
|
+
alg: opts.alg, privateKey: opts.privateKey, kid: opts.kid, externalAad: opts.externalAad,
|
|
130
|
+
});
|
|
131
|
+
return opts.tagged === true ? Buffer.concat([CWT_TAG_PREFIX, coseSign1]) : coseSign1;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* @primitive b.cwt.verify
|
|
136
|
+
* @signature b.cwt.verify(cwt, opts)
|
|
137
|
+
* @since 0.12.34
|
|
138
|
+
* @status stable
|
|
139
|
+
* @related b.cwt.sign, b.cose.verify
|
|
140
|
+
*
|
|
141
|
+
* Verify a CWT and return its claims. The COSE signature is checked
|
|
142
|
+
* via <code>b.cose.verify</code> (mandatory <code>algorithms</code>
|
|
143
|
+
* allowlist), then the standard time / identity claims are enforced:
|
|
144
|
+
* a passed <code>exp</code> (with <code>clockSkewSec</code> tolerance),
|
|
145
|
+
* a not-yet-valid <code>nbf</code>, and — when requested — an
|
|
146
|
+
* <code>iss</code> / <code>aud</code> mismatch are refused. Accepts a
|
|
147
|
+
* CWT-tag-61-wrapped or bare COSE_Sign1.
|
|
148
|
+
*
|
|
149
|
+
* @opts
|
|
150
|
+
* {
|
|
151
|
+
* algorithms: string[], // required — accepted COSE algs (allowlist)
|
|
152
|
+
* publicKey?: object, // verification key (per b.cose.verify)
|
|
153
|
+
* keyResolver?: function,
|
|
154
|
+
* expectedIssuer?: string, // require iss === this
|
|
155
|
+
* expectedAudience?: string, // require aud to include this
|
|
156
|
+
* clockSkewSec?: number, // default 60
|
|
157
|
+
* now?: number, // override clock (ms) for testing
|
|
158
|
+
* externalAad?: Buffer,
|
|
159
|
+
* }
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* var out = await b.cwt.verify(cwt, { algorithms: ["ES256"], publicKey: pub, expectedIssuer: "issuer.example" });
|
|
163
|
+
* // → { claims: { iss, sub, exp, scope }, raw: Map, protectedHeaders: Map }
|
|
164
|
+
*/
|
|
165
|
+
async function verify(cwt, opts) {
|
|
166
|
+
if (!Buffer.isBuffer(cwt) && !(cwt instanceof Uint8Array)) {
|
|
167
|
+
throw new CwtError("cwt/bad-input", "cwt.verify: cwt must be a Buffer / Uint8Array");
|
|
168
|
+
}
|
|
169
|
+
validateOpts.requireObject(opts, "cwt.verify", CwtError);
|
|
170
|
+
validateOpts(opts, [
|
|
171
|
+
"algorithms", "publicKey", "keyResolver", "expectedIssuer",
|
|
172
|
+
"expectedAudience", "clockSkewSec", "now", "externalAad",
|
|
173
|
+
], "cwt.verify");
|
|
174
|
+
|
|
175
|
+
// Strip the optional CWT tag-61 wrapper to recover the COSE_Sign1.
|
|
176
|
+
// Read the tag head generically (1 / 2 / 3 / 5 / 9-byte argument
|
|
177
|
+
// forms) rather than matching only the minimal 0xd8 0x3d encoding —
|
|
178
|
+
// an external CBOR encoder may emit a non-minimal but valid tag 61.
|
|
179
|
+
var coseBytes = Buffer.from(cwt);
|
|
180
|
+
var head = _readTagHead(coseBytes);
|
|
181
|
+
if (head && head.tag === 61) coseBytes = coseBytes.subarray(head.len); // allow:raw-byte-literal — CWT CBOR tag number (RFC 8392 §6)
|
|
182
|
+
|
|
183
|
+
var verified = await cose.verify(coseBytes, {
|
|
184
|
+
algorithms: opts.algorithms, publicKey: opts.publicKey,
|
|
185
|
+
keyResolver: opts.keyResolver, externalAad: opts.externalAad,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
var raw = cbor.decode(verified.payload);
|
|
189
|
+
if (!(raw instanceof Map)) {
|
|
190
|
+
throw new CwtError("cwt/bad-claims", "cwt.verify: claims payload is not a CBOR map");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Time claims (NumericDate, seconds). Skew tolerance both directions.
|
|
194
|
+
var skew = (typeof opts.clockSkewSec === "number" && opts.clockSkewSec >= 0) ? opts.clockSkewSec : 60; // allow:numeric-opt-Infinity — clamped non-negative, else default / allow:raw-time-literal — clock-skew in seconds (NumericDate units), not a ms duration
|
|
195
|
+
var now = _nowSec(opts);
|
|
196
|
+
// A present exp / nbf MUST be a well-formed NumericDate — a non-numeric
|
|
197
|
+
// value would otherwise bypass the time check entirely (a token could
|
|
198
|
+
// carry exp: "whenever" and never expire). Refuse the malformed claim.
|
|
199
|
+
if (raw.has(STD.exp)) {
|
|
200
|
+
var exp = raw.get(STD.exp);
|
|
201
|
+
if (typeof exp !== "number" || !isFinite(exp)) {
|
|
202
|
+
throw new CwtError("cwt/malformed-claim", "cwt.verify: exp claim is present but not a numeric NumericDate");
|
|
203
|
+
}
|
|
204
|
+
if (now > exp + skew) {
|
|
205
|
+
throw new CwtError("cwt/expired", "cwt.verify: token expired (exp " + exp + " < now " + now + ")");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (raw.has(STD.nbf)) {
|
|
209
|
+
var nbf = raw.get(STD.nbf);
|
|
210
|
+
if (typeof nbf !== "number" || !isFinite(nbf)) {
|
|
211
|
+
throw new CwtError("cwt/malformed-claim", "cwt.verify: nbf claim is present but not a numeric NumericDate");
|
|
212
|
+
}
|
|
213
|
+
if (now < nbf - skew) {
|
|
214
|
+
throw new CwtError("cwt/not-yet-valid", "cwt.verify: token not yet valid (nbf " + nbf + " > now " + now + ")");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (opts.expectedIssuer != null) {
|
|
218
|
+
if (raw.get(STD.iss) !== opts.expectedIssuer) {
|
|
219
|
+
throw new CwtError("cwt/issuer-mismatch", "cwt.verify: iss does not match expectedIssuer");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (opts.expectedAudience != null) {
|
|
223
|
+
var aud = raw.get(STD.aud);
|
|
224
|
+
var audOk = Array.isArray(aud) ? aud.indexOf(opts.expectedAudience) !== -1 : aud === opts.expectedAudience;
|
|
225
|
+
if (!audOk) {
|
|
226
|
+
throw new CwtError("cwt/audience-mismatch", "cwt.verify: aud does not include expectedAudience");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Build a friendly claims object (standard labels → names).
|
|
231
|
+
var claims = {};
|
|
232
|
+
raw.forEach(function (v, k) {
|
|
233
|
+
claims[Object.prototype.hasOwnProperty.call(STD_BY_LABEL, k) ? STD_BY_LABEL[k] : k] = v;
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
return { claims: claims, raw: raw, alg: verified.alg, protectedHeaders: verified.protectedHeaders };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
module.exports = {
|
|
240
|
+
sign: sign,
|
|
241
|
+
verify: verify,
|
|
242
|
+
CLAIM_LABELS: STD,
|
|
243
|
+
CwtError: CwtError,
|
|
244
|
+
};
|
package/lib/eat.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.eat
|
|
4
|
+
* @nav Crypto
|
|
5
|
+
* @title Entity Attestation Token (EAT)
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* RFC 9711 Entity Attestation Token — a CWT (or JWT) profile that
|
|
9
|
+
* carries attestation claims describing the state of a device or
|
|
10
|
+
* software entity: a freshness nonce, a Universal Entity ID, OEM /
|
|
11
|
+
* hardware identifiers, debug status, software measurements, and
|
|
12
|
+
* nested submodule attestations. EAT is the token a Relying Party
|
|
13
|
+
* asks a device to produce to prove what it is and what state it is
|
|
14
|
+
* in. This module is the EAT profile over <code>b.cwt</code> — it
|
|
15
|
+
* maps the RFC 9711 claim names to their CWT claim-key integer
|
|
16
|
+
* labels and adds the attestation-specific verification.
|
|
17
|
+
*
|
|
18
|
+
* <code>b.eat.sign(claims, opts)</code> takes a friendly claims
|
|
19
|
+
* object (<code>nonce</code>, <code>ueid</code>, <code>oemid</code>,
|
|
20
|
+
* <code>dbgstat</code>, <code>eat_profile</code>,
|
|
21
|
+
* <code>measurements</code>, <code>submods</code>, … plus the
|
|
22
|
+
* standard CWT claims) and signs it as a CWT.
|
|
23
|
+
*
|
|
24
|
+
* <code>b.eat.verify(eat, opts)</code> verifies the CWT (signature +
|
|
25
|
+
* alg allowlist + time claims, via <code>b.cwt</code>) and then
|
|
26
|
+
* enforces the attestation contract:
|
|
27
|
+
*
|
|
28
|
+
* - <strong>Nonce binding</strong> — when the Relying Party supplied
|
|
29
|
+
* a fresh <code>expectedNonce</code>, the token's
|
|
30
|
+
* <code>eat_nonce</code> (claim 10) MUST match it (constant-time
|
|
31
|
+
* compare). This is the freshness / anti-replay defense: without
|
|
32
|
+
* it a captured attestation can be replayed indefinitely.
|
|
33
|
+
* - <strong>Debug status</strong> — <code>requireDebugDisabled</code>
|
|
34
|
+
* refuses a token whose <code>dbgstat</code> is
|
|
35
|
+
* <code>enabled</code> (0) or absent; only the disabled states
|
|
36
|
+
* (1–4) pass.
|
|
37
|
+
* - <strong>Profile</strong> — <code>expectedProfile</code> pins the
|
|
38
|
+
* <code>eat_profile</code> claim.
|
|
39
|
+
*
|
|
40
|
+
* Signing algorithms follow <code>b.cwt</code> / <code>b.cose</code>:
|
|
41
|
+
* ES256/384/512 + EdDSA (interoperable today) and ML-DSA-87.
|
|
42
|
+
*
|
|
43
|
+
* @card
|
|
44
|
+
* RFC 9711 Entity Attestation Token over b.cwt — sign / verify
|
|
45
|
+
* device + software attestation claims with verifier-nonce binding,
|
|
46
|
+
* debug-status policy, and profile pinning.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
var cwt = require("./cwt");
|
|
50
|
+
var bCrypto = require("./crypto");
|
|
51
|
+
var validateOpts = require("./validate-opts");
|
|
52
|
+
var { defineClass } = require("./framework-error");
|
|
53
|
+
|
|
54
|
+
var EatError = defineClass("EatError", { alwaysPermanent: true });
|
|
55
|
+
|
|
56
|
+
// RFC 9711 / IANA CWT Claims registry claim keys.
|
|
57
|
+
var EAT = { // allow:raw-byte-literal — RFC 9711 / IANA CWT claim-key labels, not byte sizes
|
|
58
|
+
nonce: 10, ueid: 256, sueids: 257, oemid: 258, hwmodel: 259, hwversion: 260, // allow:raw-byte-literal — CWT claim keys
|
|
59
|
+
uptime: 261, oemboot: 262, dbgstat: 263, location: 264, eat_profile: 265, // allow:raw-byte-literal — CWT claim keys
|
|
60
|
+
submods: 266, swname: 270, swversion: 271, manifests: 272, measurements: 273, // allow:raw-byte-literal — CWT claim keys
|
|
61
|
+
};
|
|
62
|
+
var EAT_BY_LABEL = {};
|
|
63
|
+
Object.keys(EAT).forEach(function (k) { EAT_BY_LABEL[EAT[k]] = k; });
|
|
64
|
+
|
|
65
|
+
// RFC 9711 §4.3.1 debug-status enumeration.
|
|
66
|
+
var DBGSTAT = {
|
|
67
|
+
"enabled": 0, "disabled": 1, "disabled-since-boot": 2,
|
|
68
|
+
"disabled-permanently": 3, "disabled-fully-and-permanently": 4,
|
|
69
|
+
};
|
|
70
|
+
var DBGSTAT_BY_VALUE = {};
|
|
71
|
+
Object.keys(DBGSTAT).forEach(function (k) { DBGSTAT_BY_VALUE[DBGSTAT[k]] = k; });
|
|
72
|
+
|
|
73
|
+
// Standard CWT claim labels → names (RFC 8392 §3.1.1) so EAT's
|
|
74
|
+
// friendly output names the standard claims alongside the EAT ones.
|
|
75
|
+
var STD_NAME = { 1: "iss", 2: "sub", 3: "aud", 4: "exp", 5: "nbf", 6: "iat", 7: "cti" };
|
|
76
|
+
|
|
77
|
+
function _toBuf(x) {
|
|
78
|
+
if (Buffer.isBuffer(x)) return x;
|
|
79
|
+
if (x instanceof Uint8Array) return Buffer.from(x);
|
|
80
|
+
if (typeof x === "string") return Buffer.from(x, "utf8");
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function _nonceMatches(claimValue, expected) {
|
|
85
|
+
var exp = _toBuf(expected);
|
|
86
|
+
if (!exp) return false;
|
|
87
|
+
// eat_nonce may be a single byte string or an array of them (one per
|
|
88
|
+
// verifier). Constant-time compare against each candidate.
|
|
89
|
+
var candidates = Array.isArray(claimValue) ? claimValue : [claimValue];
|
|
90
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
91
|
+
var c = _toBuf(candidates[i]);
|
|
92
|
+
if (c && c.length === exp.length && bCrypto.timingSafeEqual(c, exp)) return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @primitive b.eat.sign
|
|
99
|
+
* @signature b.eat.sign(claims, opts)
|
|
100
|
+
* @since 0.12.35
|
|
101
|
+
* @status stable
|
|
102
|
+
* @related b.eat.verify, b.cwt.sign
|
|
103
|
+
*
|
|
104
|
+
* Sign EAT attestation claims into a CWT. EAT claim names map to their
|
|
105
|
+
* RFC 9711 integer labels; <code>dbgstat</code> accepts the enum name
|
|
106
|
+
* (<code>"disabled-since-boot"</code>) or its integer. Standard CWT
|
|
107
|
+
* claims (<code>iss</code> / <code>exp</code> / …) pass through to
|
|
108
|
+
* <code>b.cwt.sign</code>.
|
|
109
|
+
*
|
|
110
|
+
* @opts
|
|
111
|
+
* {
|
|
112
|
+
* alg: string, // COSE signing alg (ES256 / EdDSA / ML-DSA-87 / …)
|
|
113
|
+
* privateKey: object, // signing key
|
|
114
|
+
* kid?: string,
|
|
115
|
+
* tagged?: boolean, // CWT tag 61
|
|
116
|
+
* }
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* var eat = await b.eat.sign(
|
|
120
|
+
* { nonce: rpNonce, ueid: deviceUeid, oemid: oem, dbgstat: "disabled-permanently",
|
|
121
|
+
* eat_profile: "https://example.com/eat/profile-1", iat: Math.floor(Date.now()/1000) },
|
|
122
|
+
* { alg: "ES256", privateKey: deviceKey });
|
|
123
|
+
*/
|
|
124
|
+
async function sign(claims, opts) {
|
|
125
|
+
if (!claims || typeof claims !== "object" || Array.isArray(claims)) {
|
|
126
|
+
throw new EatError("eat/bad-claims", "eat.sign: claims must be a plain object");
|
|
127
|
+
}
|
|
128
|
+
validateOpts.requireObject(opts, "eat.sign", EatError);
|
|
129
|
+
// Translate EAT claim names to their integer labels into a Map (so
|
|
130
|
+
// the integer keys survive — a plain object would stringify them).
|
|
131
|
+
// Standard CWT names (iss/sub/aud/exp/nbf/iat/cti) + custom keys are
|
|
132
|
+
// left for b.cwt.sign to handle.
|
|
133
|
+
var mapped = new Map();
|
|
134
|
+
var keys = Object.keys(claims);
|
|
135
|
+
for (var i = 0; i < keys.length; i++) {
|
|
136
|
+
var name = keys[i];
|
|
137
|
+
var value = claims[name];
|
|
138
|
+
if (name === "dbgstat" && typeof value === "string") {
|
|
139
|
+
if (!Object.prototype.hasOwnProperty.call(DBGSTAT, value)) {
|
|
140
|
+
throw new EatError("eat/bad-dbgstat",
|
|
141
|
+
"eat.sign: dbgstat must be one of " + Object.keys(DBGSTAT).join(" / ") + " (or an integer 0-4)");
|
|
142
|
+
}
|
|
143
|
+
value = DBGSTAT[value];
|
|
144
|
+
}
|
|
145
|
+
mapped.set(Object.prototype.hasOwnProperty.call(EAT, name) ? EAT[name] : name, value);
|
|
146
|
+
}
|
|
147
|
+
return cwt.sign(mapped, opts);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* @primitive b.eat.verify
|
|
152
|
+
* @signature b.eat.verify(eat, opts)
|
|
153
|
+
* @since 0.12.35
|
|
154
|
+
* @status stable
|
|
155
|
+
* @related b.eat.sign, b.cwt.verify
|
|
156
|
+
*
|
|
157
|
+
* Verify an EAT and return its attestation claims. Delegates the CWT
|
|
158
|
+
* signature + algorithm-allowlist + time-claim checks to
|
|
159
|
+
* <code>b.cwt.verify</code>, then enforces the attestation contract:
|
|
160
|
+
* the <code>eat_nonce</code> must match <code>expectedNonce</code>
|
|
161
|
+
* (when supplied — the freshness/anti-replay binding),
|
|
162
|
+
* <code>requireDebugDisabled</code> refuses a non-disabled
|
|
163
|
+
* <code>dbgstat</code>, and <code>expectedProfile</code> pins
|
|
164
|
+
* <code>eat_profile</code>.
|
|
165
|
+
*
|
|
166
|
+
* @opts
|
|
167
|
+
* {
|
|
168
|
+
* algorithms: string[], // required — accepted COSE algs
|
|
169
|
+
* publicKey?: object,
|
|
170
|
+
* keyResolver?: function,
|
|
171
|
+
* expectedNonce?: Buffer, // require eat_nonce to match (freshness)
|
|
172
|
+
* requireDebugDisabled?: boolean, // refuse dbgstat enabled / absent
|
|
173
|
+
* expectedProfile?: string, // pin eat_profile
|
|
174
|
+
* expectedIssuer?: string, // forwarded to b.cwt.verify
|
|
175
|
+
* expectedAudience?: string,
|
|
176
|
+
* clockSkewSec?: number,
|
|
177
|
+
* now?: number,
|
|
178
|
+
* externalAad?: Buffer,
|
|
179
|
+
* }
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* var att = await b.eat.verify(eat, { algorithms: ["ES256"], publicKey: devicePub, expectedNonce: rpNonce, requireDebugDisabled: true });
|
|
183
|
+
* // → { claims: { nonce, ueid, dbgstat: "disabled-permanently", ... }, raw: Map, alg }
|
|
184
|
+
*/
|
|
185
|
+
async function verify(eat, opts) {
|
|
186
|
+
validateOpts.requireObject(opts, "eat.verify", EatError);
|
|
187
|
+
var out = await cwt.verify(eat, {
|
|
188
|
+
algorithms: opts.algorithms, publicKey: opts.publicKey, keyResolver: opts.keyResolver,
|
|
189
|
+
expectedIssuer: opts.expectedIssuer, expectedAudience: opts.expectedAudience,
|
|
190
|
+
clockSkewSec: opts.clockSkewSec, now: opts.now, externalAad: opts.externalAad,
|
|
191
|
+
});
|
|
192
|
+
var raw = out.raw;
|
|
193
|
+
|
|
194
|
+
// Nonce binding — the freshness / anti-replay defense. When the RP
|
|
195
|
+
// supplied a nonce, the token MUST carry a matching eat_nonce.
|
|
196
|
+
if (opts.expectedNonce != null) {
|
|
197
|
+
if (!raw.has(EAT.nonce)) {
|
|
198
|
+
throw new EatError("eat/nonce-missing", "eat.verify: expectedNonce supplied but token has no eat_nonce claim");
|
|
199
|
+
}
|
|
200
|
+
if (!_nonceMatches(raw.get(EAT.nonce), opts.expectedNonce)) {
|
|
201
|
+
throw new EatError("eat/nonce-mismatch", "eat.verify: eat_nonce does not match expectedNonce (stale / replayed attestation)");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Debug status — refuse a token that can't prove debug is disabled.
|
|
206
|
+
if (opts.requireDebugDisabled === true) {
|
|
207
|
+
var ds = raw.get(EAT.dbgstat);
|
|
208
|
+
if (typeof ds !== "number" || ds < DBGSTAT.disabled) {
|
|
209
|
+
throw new EatError("eat/debug-not-disabled",
|
|
210
|
+
"eat.verify: requireDebugDisabled — dbgstat is " +
|
|
211
|
+
(ds === undefined ? "absent" : (DBGSTAT_BY_VALUE[ds] || ds)) + ", not a disabled state");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (opts.expectedProfile != null && raw.get(EAT.eat_profile) !== opts.expectedProfile) {
|
|
216
|
+
throw new EatError("eat/profile-mismatch", "eat.verify: eat_profile does not match expectedProfile");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Build friendly claims: EAT + standard labels → names; decode the
|
|
220
|
+
// dbgstat enum to its name.
|
|
221
|
+
var claims = {};
|
|
222
|
+
raw.forEach(function (v, k) {
|
|
223
|
+
var name = Object.prototype.hasOwnProperty.call(EAT_BY_LABEL, k) ? EAT_BY_LABEL[k]
|
|
224
|
+
: (Object.prototype.hasOwnProperty.call(STD_NAME, k) ? STD_NAME[k] : k);
|
|
225
|
+
if (name === "dbgstat" && typeof v === "number" && Object.prototype.hasOwnProperty.call(DBGSTAT_BY_VALUE, v)) {
|
|
226
|
+
v = DBGSTAT_BY_VALUE[v];
|
|
227
|
+
}
|
|
228
|
+
claims[name] = v;
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return { claims: claims, raw: raw, alg: out.alg, protectedHeaders: out.protectedHeaders };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = {
|
|
235
|
+
sign: sign,
|
|
236
|
+
verify: verify,
|
|
237
|
+
CLAIM_LABELS: EAT,
|
|
238
|
+
DBGSTAT: DBGSTAT,
|
|
239
|
+
EatError: EatError,
|
|
240
|
+
};
|
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:05721aa1-7108-4263-bd58-e67b140f3790",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-24T23:29:47.165Z",
|
|
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.35",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.12.
|
|
25
|
+
"version": "0.12.35",
|
|
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.35",
|
|
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.35",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|