@blamejs/core 0.12.32 → 0.12.34
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/cose.js +339 -0
- package/lib/cwt.js +239 -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.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.
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
11
15
|
- 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.
|
|
12
16
|
|
|
13
17
|
- v0.12.31 (2026-05-24) — **`b.auth.jar.parse` — verify RFC 9101 JWT-Secured Authorization Requests (server side).** A plain OAuth authorization request carries its parameters in the URL query string, where a browser, proxy, or referer log can tamper with or leak them. RFC 9101 JAR packs those parameters into a JWT the client signs — the request object — so the authorization server can confirm they arrived exactly as sent. `b.auth.jar.parse(jar, opts)` is the server-side verifier and the request-side counterpart to the existing JARM response handling (`b.auth.oauth.parseJarmResponse`). It delegates the signature check to `b.auth.jwt.verifyExternal` — which already enforces a mandatory `algorithms` allowlist and refuses the alg-confusion (`alg: "none"`, HMAC-vs-RSA) and JWE-on-a-JWS-verifier shapes against a JWKS public-key trust source — then pins `iss` and the `client_id` claim to the expected client, pins `aud` to this server's issuer identifier, refuses a nested `request` / `request_uri` (RFC 9101 §6.3 recursion / confused-deputy vector), and returns the authorization parameters with the JWT envelope claims stripped. **Added:** *`b.auth.jar.parse(jar, opts)` — request-object verification* — `opts.clientId` (the expected client — pins `iss` + the `client_id` claim), `opts.audience` (this server's issuer identifier — pins `aud`), `opts.algorithms` (required signature allowlist — no defaults, the alg-confusion defense), and one of `opts.jwks` / `opts.jwksUri` / `opts.keyResolver` (the client's verification key). Returns `{ params, claims }` where `params` is the authorization parameters (`response_type`, `redirect_uri`, `scope`, `state`, `nonce`, …) with the JWT envelope claims (`iss`, `aud`, `exp`, `iat`, `nbf`, `jti`) removed. A request object whose `client_id` claim disagrees with `opts.clientId`, or that nests a `request` / `request_uri`, is refused. Emitting a request object (the client side) is deferred-with-condition: it requires signing with the client's key under a classical JWS algorithm, and the framework's own JWT signer is PQC-only for the tokens it issues — a PQC-signed request object would not interoperate with a standard authorization server; client-side emission re-opens when a classical JWS signer lands or operators surface the need. Until then clients sign request objects with their existing JOSE tooling.
|
package/README.md
CHANGED
|
@@ -126,6 +126,8 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
126
126
|
- **JSON / SQL / schema** — `b.safeJson` (with `maxKeys` cap defending CVE-2026-21717 V8 HashDoS), `b.safeBuffer`, `b.safeSql`, `b.safeSchema`
|
|
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
|
+
- **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
|
|
129
131
|
- **Document parsers** — `b.parsers` (XML / TOML / YAML / .env); `b.config` (schema-validated env)
|
|
130
132
|
- **File-type detection** — `b.fileType` magic-byte content classification with deny-on-upload categories (image / document / archive / executable / etc.)
|
|
131
133
|
### Content-safety gates
|
package/index.js
CHANGED
|
@@ -456,6 +456,8 @@ module.exports = {
|
|
|
456
456
|
// the codepoint-stability contract.
|
|
457
457
|
jose: { jwe: { experimental: require("./lib/jose-jwe-experimental") } },
|
|
458
458
|
cbor: require("./lib/cbor"),
|
|
459
|
+
cose: require("./lib/cose"),
|
|
460
|
+
cwt: require("./lib/cwt"),
|
|
459
461
|
queue: queue,
|
|
460
462
|
logStream: logStream,
|
|
461
463
|
redact: redact,
|
package/lib/cose.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.cose
|
|
4
|
+
* @nav Crypto
|
|
5
|
+
* @title COSE signing (RFC 9052)
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* COSE_Sign1 signing and verification (RFC 9052 / 9053), composing
|
|
9
|
+
* the in-tree <code>b.cbor</code> codec for the deterministic
|
|
10
|
+
* Sig_structure encoding. COSE is the signed-statement substrate
|
|
11
|
+
* under SCITT, CWT, and C2PA — a CBOR-native counterpart to JWS.
|
|
12
|
+
*
|
|
13
|
+
* <strong>Signing</strong> supports the classical COSE signature
|
|
14
|
+
* algorithms that are interoperable today — ES256 / ES384 / ES512
|
|
15
|
+
* (ECDSA) and EdDSA (Ed25519), all with final IANA algorithm ids
|
|
16
|
+
* (RFC 9053) — alongside ML-DSA-87 (FIPS 204) for PQC-forward
|
|
17
|
+
* deployments. There is no classical <em>default</em>: the caller
|
|
18
|
+
* names the algorithm and supplies the key. <strong>Verification</strong>
|
|
19
|
+
* accepts the same set, so the framework both produces COSE other
|
|
20
|
+
* implementations can read today and consumes third-party COSE.
|
|
21
|
+
*
|
|
22
|
+
* <strong>Standards-maturity caveat on the PQC algorithm:</strong>
|
|
23
|
+
* the COSE algorithm identifier for ML-DSA-87 is <code>-50</code>, a
|
|
24
|
+
* <em>requested</em> (non-final) IANA assignment from
|
|
25
|
+
* draft-ietf-cose-dilithium; it may change before that draft is
|
|
26
|
+
* published, so an ML-DSA-87 COSE_Sign1 is not yet broadly
|
|
27
|
+
* interoperable — pin the identifier deliberately, re-open on IANA
|
|
28
|
+
* finalization. SLH-DSA-SHAKE-256f (the framework's default PQC
|
|
29
|
+
* signature elsewhere) has <strong>no</strong> COSE algorithm
|
|
30
|
+
* identifier registered at all (the COSE SPHINCS+ draft registers
|
|
31
|
+
* only the Category-1 'small' sets), so it cannot be represented in
|
|
32
|
+
* COSE and is not offered here. The COSE_Sign1 mechanism itself, and
|
|
33
|
+
* the classical algorithms, are stable; ML-DSA-87 is the forward-
|
|
34
|
+
* looking opt-in.
|
|
35
|
+
*
|
|
36
|
+
* <strong>Verify is bounded.</strong> The COSE_Sign1 bytes and the
|
|
37
|
+
* protected-header bstr are decoded through <code>b.cbor.decode</code>
|
|
38
|
+
* (depth + size caps, indefinite-length / tag / duplicate-key
|
|
39
|
+
* refusal). The protected header is the integrity-protected one;
|
|
40
|
+
* <code>alg</code> (label 1) lives there. A <code>crit</code> (label
|
|
41
|
+
* 2) listing a header label the verifier does not understand is
|
|
42
|
+
* refused (RFC 9052 §3.1) — a crit-bypass defense.
|
|
43
|
+
*
|
|
44
|
+
* v1 ships COSE_Sign1 (single-signer) with an attached payload.
|
|
45
|
+
* Detached payload, COSE_Sign (multi-signer), COSE_Mac0, and
|
|
46
|
+
* COSE_Encrypt are deferred-with-condition (operator demand).
|
|
47
|
+
*
|
|
48
|
+
* @card
|
|
49
|
+
* COSE_Sign1 sign / verify (RFC 9052) over the in-tree CBOR codec —
|
|
50
|
+
* ML-DSA-87 signing (experimental, draft alg id) + classical verify,
|
|
51
|
+
* bounded + crit-checked. The substrate under SCITT / CWT / C2PA.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
var nodeCrypto = require("node:crypto");
|
|
55
|
+
var cbor = require("./cbor");
|
|
56
|
+
var validateOpts = require("./validate-opts");
|
|
57
|
+
var { defineClass } = require("./framework-error");
|
|
58
|
+
|
|
59
|
+
var CoseError = defineClass("CoseError", { alwaysPermanent: true });
|
|
60
|
+
|
|
61
|
+
var COSE_SIGN1_TAG = 18; // allow:raw-byte-literal — RFC 9052 COSE_Sign1 CBOR tag
|
|
62
|
+
var HDR_ALG = 1; // RFC 9052 §3.1 header label: alg
|
|
63
|
+
var HDR_CRIT = 2; // header label: crit
|
|
64
|
+
var HDR_CONTENT_TYPE = 3; // header label: content type
|
|
65
|
+
var HDR_KID = 4; // header label: kid
|
|
66
|
+
|
|
67
|
+
// COSE algorithm identifiers. ML-DSA-87 is a NON-FINAL requested
|
|
68
|
+
// assignment (draft-ietf-cose-dilithium) — pinned deliberately, re-open
|
|
69
|
+
// on IANA finalization. The classical ECDSA / EdDSA ids are final
|
|
70
|
+
// (RFC 9053). SLH-DSA is intentionally absent (no registered COSE id).
|
|
71
|
+
var ALG_NAME_TO_ID = {
|
|
72
|
+
"ML-DSA-87": -50,
|
|
73
|
+
"ES256": -7, "ES384": -35, "ES512": -36, "EdDSA": -8, // allow:raw-byte-literal — COSE algorithm identifiers (RFC 9053), not byte sizes
|
|
74
|
+
};
|
|
75
|
+
var ALG_ID_TO_NAME = {};
|
|
76
|
+
Object.keys(ALG_NAME_TO_ID).forEach(function (k) { ALG_ID_TO_NAME[ALG_NAME_TO_ID[k]] = k; });
|
|
77
|
+
|
|
78
|
+
// Signable algorithms: the classical ECDSA / EdDSA set (final COSE
|
|
79
|
+
// ids, interoperable today) plus ML-DSA-87 (draft id, PQC-forward).
|
|
80
|
+
// All are accepted for VERIFY as well. There is no classical default —
|
|
81
|
+
// the caller names the algorithm explicitly.
|
|
82
|
+
var SIGNABLE = ["ML-DSA-87", "ES256", "ES384", "ES512", "EdDSA"];
|
|
83
|
+
|
|
84
|
+
// Header labels this verifier understands — a `crit` entry naming any
|
|
85
|
+
// other label is refused (RFC 9052 §3.1 crit-bypass defense).
|
|
86
|
+
var UNDERSTOOD_LABELS = [HDR_ALG, HDR_CRIT, HDR_CONTENT_TYPE, HDR_KID];
|
|
87
|
+
|
|
88
|
+
function _toKeyObject(key, kind) {
|
|
89
|
+
if (key && typeof key === "object" && typeof key.asymmetricKeyType === "string") return key;
|
|
90
|
+
try {
|
|
91
|
+
return kind === "private" ? nodeCrypto.createPrivateKey(key) : nodeCrypto.createPublicKey(key);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
throw new CoseError("cose/bad-key", "cose: could not load " + kind + " key: " + e.message);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function _algParamsFor(algId) {
|
|
98
|
+
switch (algId) {
|
|
99
|
+
case -50: return { nodeAlg: null }; // ML-DSA-87 (KeyObject specifies the hash)
|
|
100
|
+
case -8: return { nodeAlg: null }; // allow:raw-byte-literal — EdDSA COSE alg id (RFC 9053), not a size
|
|
101
|
+
case -7: return { nodeAlg: "sha256", dsaEncoding: "ieee-p1363" }; // ES256
|
|
102
|
+
case -35: return { nodeAlg: "sha384", dsaEncoding: "ieee-p1363" }; // ES384
|
|
103
|
+
case -36: return { nodeAlg: "sha512", dsaEncoding: "ieee-p1363" }; // ES512
|
|
104
|
+
default:
|
|
105
|
+
throw new CoseError("cose/unknown-alg", "cose: unrecognized COSE algorithm id " + algId);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function _bstr(x) {
|
|
110
|
+
if (Buffer.isBuffer(x)) return x;
|
|
111
|
+
if (x instanceof Uint8Array) return Buffer.from(x);
|
|
112
|
+
if (typeof x === "string") return Buffer.from(x, "utf8");
|
|
113
|
+
throw new CoseError("cose/bad-bytes", "cose: expected bytes (Buffer / Uint8Array / string)");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Sig_structure (RFC 9052 §4.4) for COSE_Sign1:
|
|
117
|
+
// [ "Signature1", body_protected (bstr), external_aad (bstr), payload (bstr) ]
|
|
118
|
+
// deterministically CBOR-encoded — the bytes that are signed / verified.
|
|
119
|
+
function _toBeSigned(protectedBstr, externalAad, payload) {
|
|
120
|
+
return cbor.encode(["Signature1", protectedBstr, externalAad, payload]);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @primitive b.cose.sign
|
|
125
|
+
* @signature b.cose.sign(payload, opts)
|
|
126
|
+
* @since 0.12.33
|
|
127
|
+
* @status stable
|
|
128
|
+
* @related b.cose.verify, b.cbor.encode
|
|
129
|
+
*
|
|
130
|
+
* Produce a tagged COSE_Sign1 (RFC 9052) over <code>payload</code>
|
|
131
|
+
* (bytes). <code>alg</code> is one of the classical ECDSA / EdDSA
|
|
132
|
+
* algorithms (final COSE ids, interoperable today) or
|
|
133
|
+
* <code>"ML-DSA-87"</code> (draft id <code>-50</code>, PQC-forward).
|
|
134
|
+
* <code>alg</code> is placed in the integrity-protected header.
|
|
135
|
+
*
|
|
136
|
+
* @opts
|
|
137
|
+
* {
|
|
138
|
+
* alg: string, // "ES256" | "ES384" | "ES512" | "EdDSA" | "ML-DSA-87"
|
|
139
|
+
* privateKey: object, // matching KeyObject or PEM
|
|
140
|
+
* kid?: string, // → unprotected header label 4
|
|
141
|
+
* contentType?: number, // → protected header label 3
|
|
142
|
+
* externalAad?: Buffer, // default empty — bound into the signature
|
|
143
|
+
* unprotectedHeaders?: object, // extra unprotected map entries (numeric keys)
|
|
144
|
+
* }
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* var coseSign1 = await b.cose.sign(Buffer.from("statement"), {
|
|
148
|
+
* alg: "ES256", privateKey: ecKey, kid: "key-1",
|
|
149
|
+
* });
|
|
150
|
+
*/
|
|
151
|
+
async function sign(payload, opts) {
|
|
152
|
+
validateOpts.requireObject(opts, "cose.sign", CoseError);
|
|
153
|
+
validateOpts(opts, ["alg", "privateKey", "kid", "contentType", "externalAad", "unprotectedHeaders"], "cose.sign");
|
|
154
|
+
if (SIGNABLE.indexOf(opts.alg) === -1) {
|
|
155
|
+
throw new CoseError("cose/unsignable-alg",
|
|
156
|
+
"cose.sign: alg must be one of " + SIGNABLE.join(" / ") +
|
|
157
|
+
" (SLH-DSA has no COSE algorithm id and is not offered)");
|
|
158
|
+
}
|
|
159
|
+
if (!opts.privateKey) {
|
|
160
|
+
throw new CoseError("cose/no-key", "cose.sign: opts.privateKey is required");
|
|
161
|
+
}
|
|
162
|
+
var algId = ALG_NAME_TO_ID[opts.alg];
|
|
163
|
+
var params = _algParamsFor(algId);
|
|
164
|
+
var key = _toKeyObject(opts.privateKey, "private");
|
|
165
|
+
|
|
166
|
+
var protMap = new Map();
|
|
167
|
+
protMap.set(HDR_ALG, algId);
|
|
168
|
+
if (typeof opts.contentType === "number") protMap.set(HDR_CONTENT_TYPE, opts.contentType);
|
|
169
|
+
var protectedBstr = cbor.encode(protMap);
|
|
170
|
+
|
|
171
|
+
var unprot = new Map();
|
|
172
|
+
if (typeof opts.kid === "string") unprot.set(HDR_KID, Buffer.from(opts.kid, "utf8"));
|
|
173
|
+
if (opts.unprotectedHeaders && typeof opts.unprotectedHeaders === "object") {
|
|
174
|
+
var uk = Object.keys(opts.unprotectedHeaders);
|
|
175
|
+
for (var i = 0; i < uk.length; i++) unprot.set(Number(uk[i]), opts.unprotectedHeaders[uk[i]]);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
var payloadBytes = _bstr(payload);
|
|
179
|
+
var externalAad = opts.externalAad == null ? Buffer.alloc(0) : _bstr(opts.externalAad);
|
|
180
|
+
var toBeSigned = _toBeSigned(protectedBstr, externalAad, payloadBytes);
|
|
181
|
+
|
|
182
|
+
// ML-DSA-87 + EdDSA: the KeyObject specifies the algorithm, so a
|
|
183
|
+
// null digest name is correct. ECDSA: a digest + the IEEE-P1363
|
|
184
|
+
// fixed-width signature encoding COSE mandates (RFC 9053 §2.1, not
|
|
185
|
+
// ASN.1 DER).
|
|
186
|
+
var signature = (params.nodeAlg === null)
|
|
187
|
+
? nodeCrypto.sign(null, toBeSigned, key)
|
|
188
|
+
: nodeCrypto.sign(params.nodeAlg, toBeSigned, { key: key, dsaEncoding: params.dsaEncoding });
|
|
189
|
+
|
|
190
|
+
var sign1 = [protectedBstr, unprot, payloadBytes, signature];
|
|
191
|
+
return cbor.encode(new cbor.Tag(COSE_SIGN1_TAG, sign1));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* @primitive b.cose.verify
|
|
196
|
+
* @signature b.cose.verify(coseSign1, opts)
|
|
197
|
+
* @since 0.12.33
|
|
198
|
+
* @status experimental
|
|
199
|
+
* @related b.cose.sign, b.cbor.decode
|
|
200
|
+
*
|
|
201
|
+
* Verify a COSE_Sign1 (RFC 9052) and return its payload + headers.
|
|
202
|
+
* The bytes are decoded through the bounded <code>b.cbor</code> codec;
|
|
203
|
+
* <code>alg</code> is read from the integrity-protected header and must
|
|
204
|
+
* be in <code>opts.algorithms</code>; a <code>crit</code> header naming
|
|
205
|
+
* a label the verifier does not understand is refused. Accepts ML-DSA-87
|
|
206
|
+
* plus the classical ECDSA / EdDSA COSE algorithms.
|
|
207
|
+
*
|
|
208
|
+
* @opts
|
|
209
|
+
* {
|
|
210
|
+
* algorithms: string[], // required — accepted alg names (allowlist)
|
|
211
|
+
* publicKey?: object, // the verification key (KeyObject / PEM)
|
|
212
|
+
* keyResolver?: function, // (protectedHeaders, unprotectedHeaders) → key
|
|
213
|
+
* externalAad?: Buffer, // must match what was signed
|
|
214
|
+
* maxBytes?: number, // forwarded to b.cbor.decode
|
|
215
|
+
* maxDepth?: number,
|
|
216
|
+
* }
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* var out = await b.cose.verify(coseSign1, { algorithms: ["ML-DSA-87"], publicKey: pub });
|
|
220
|
+
* // → { payload: <Buffer>, alg: "ML-DSA-87", protectedHeaders: Map, unprotectedHeaders: Map }
|
|
221
|
+
*/
|
|
222
|
+
async function verify(coseSign1, opts) {
|
|
223
|
+
validateOpts.requireObject(opts, "cose.verify", CoseError);
|
|
224
|
+
validateOpts(opts, ["algorithms", "publicKey", "keyResolver", "externalAad", "maxBytes", "maxDepth"], "cose.verify");
|
|
225
|
+
if (!Array.isArray(opts.algorithms) || opts.algorithms.length === 0) {
|
|
226
|
+
throw new CoseError("cose/algorithms-required",
|
|
227
|
+
"cose.verify: opts.algorithms is required (no defaults — name the accepted algorithms)");
|
|
228
|
+
}
|
|
229
|
+
for (var ai = 0; ai < opts.algorithms.length; ai++) {
|
|
230
|
+
if (!(opts.algorithms[ai] in ALG_NAME_TO_ID)) {
|
|
231
|
+
throw new CoseError("cose/unknown-alg", "cose.verify: unknown algorithm '" + opts.algorithms[ai] + "'");
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (!opts.publicKey && typeof opts.keyResolver !== "function") {
|
|
235
|
+
throw new CoseError("cose/no-key", "cose.verify: pass publicKey or keyResolver");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
var decoded = cbor.decode(_bstr(coseSign1), {
|
|
239
|
+
allowedTags: [COSE_SIGN1_TAG],
|
|
240
|
+
maxBytes: opts.maxBytes,
|
|
241
|
+
maxDepth: opts.maxDepth,
|
|
242
|
+
});
|
|
243
|
+
// Accept tagged (18) or bare COSE_Sign1 array.
|
|
244
|
+
var arr = (decoded instanceof cbor.Tag && decoded.tag === COSE_SIGN1_TAG) ? decoded.value : decoded;
|
|
245
|
+
if (!Array.isArray(arr) || arr.length !== 4) {
|
|
246
|
+
throw new CoseError("cose/malformed", "cose.verify: not a COSE_Sign1 (expected a 4-element array)");
|
|
247
|
+
}
|
|
248
|
+
var protectedBstr = arr[0];
|
|
249
|
+
var unprotected = arr[1];
|
|
250
|
+
var payload = arr[2];
|
|
251
|
+
var signature = arr[3];
|
|
252
|
+
if (!Buffer.isBuffer(protectedBstr) || !Buffer.isBuffer(signature)) {
|
|
253
|
+
throw new CoseError("cose/malformed", "cose.verify: protected header and signature must be byte strings");
|
|
254
|
+
}
|
|
255
|
+
if (payload === null || payload === undefined) {
|
|
256
|
+
throw new CoseError("cose/detached-unsupported",
|
|
257
|
+
"cose.verify: detached payload (nil) is not supported in v1 — attached payload only");
|
|
258
|
+
}
|
|
259
|
+
// COSE_Sign1 payload is a bstr (RFC 9052 §4.2) — refuse a non-byte
|
|
260
|
+
// payload rather than return a value that violates the documented
|
|
261
|
+
// { payload: Buffer } shape.
|
|
262
|
+
if (!Buffer.isBuffer(payload)) {
|
|
263
|
+
throw new CoseError("cose/malformed", "cose.verify: payload must be a byte string (bstr)");
|
|
264
|
+
}
|
|
265
|
+
// The unprotected header is a CBOR map — refuse a non-map rather
|
|
266
|
+
// than silently coerce it to empty (callers read kid etc. from it).
|
|
267
|
+
if (!(unprotected instanceof Map)) {
|
|
268
|
+
throw new CoseError("cose/malformed", "cose.verify: unprotected header must be a CBOR map");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Decode the protected header (bounded) — empty bstr means no protected headers.
|
|
272
|
+
var protMap = protectedBstr.length === 0 ? new Map()
|
|
273
|
+
: cbor.decode(protectedBstr, { maxBytes: opts.maxBytes, maxDepth: opts.maxDepth });
|
|
274
|
+
if (!(protMap instanceof Map)) {
|
|
275
|
+
throw new CoseError("cose/malformed", "cose.verify: protected header is not a CBOR map");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// crit-bypass defense: every label in a crit array must be one the
|
|
279
|
+
// verifier understands AND must be present in the protected header.
|
|
280
|
+
if (protMap.has(HDR_CRIT)) {
|
|
281
|
+
var crit = protMap.get(HDR_CRIT);
|
|
282
|
+
if (!Array.isArray(crit)) {
|
|
283
|
+
throw new CoseError("cose/bad-crit", "cose.verify: crit (label 2) must be an array");
|
|
284
|
+
}
|
|
285
|
+
for (var ci = 0; ci < crit.length; ci++) {
|
|
286
|
+
if (UNDERSTOOD_LABELS.indexOf(crit[ci]) === -1) {
|
|
287
|
+
throw new CoseError("cose/crit-unknown",
|
|
288
|
+
"cose.verify: crit lists header label " + crit[ci] + " which is not understood (RFC 9052 §3.1)");
|
|
289
|
+
}
|
|
290
|
+
if (!protMap.has(crit[ci])) {
|
|
291
|
+
throw new CoseError("cose/crit-absent",
|
|
292
|
+
"cose.verify: crit lists label " + crit[ci] + " not present in the protected header");
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
var algId = protMap.get(HDR_ALG);
|
|
298
|
+
var algName = ALG_ID_TO_NAME[algId];
|
|
299
|
+
if (algName === undefined) {
|
|
300
|
+
throw new CoseError("cose/unknown-alg", "cose.verify: unrecognized protected alg id " + algId);
|
|
301
|
+
}
|
|
302
|
+
if (opts.algorithms.indexOf(algName) === -1) {
|
|
303
|
+
throw new CoseError("cose/alg-not-allowed",
|
|
304
|
+
"cose.verify: alg '" + algName + "' is not in the allowlist");
|
|
305
|
+
}
|
|
306
|
+
var params = _algParamsFor(algId); // throws cose/unknown-alg on an unrecognized id
|
|
307
|
+
|
|
308
|
+
var key = opts.publicKey
|
|
309
|
+
? _toKeyObject(opts.publicKey, "public")
|
|
310
|
+
: _toKeyObject(opts.keyResolver(protMap, unprotected), "public");
|
|
311
|
+
|
|
312
|
+
var externalAad = opts.externalAad == null ? Buffer.alloc(0) : _bstr(opts.externalAad);
|
|
313
|
+
var toBeSigned = _toBeSigned(protectedBstr, externalAad, payload);
|
|
314
|
+
|
|
315
|
+
var ok;
|
|
316
|
+
if (params.nodeAlg === null) {
|
|
317
|
+
ok = nodeCrypto.verify(null, toBeSigned, key, signature);
|
|
318
|
+
} else {
|
|
319
|
+
ok = nodeCrypto.verify(params.nodeAlg, toBeSigned,
|
|
320
|
+
{ key: key, dsaEncoding: params.dsaEncoding }, signature);
|
|
321
|
+
}
|
|
322
|
+
if (!ok) {
|
|
323
|
+
throw new CoseError("cose/bad-signature", "cose.verify: signature verification failed");
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
payload: payload,
|
|
327
|
+
alg: algName,
|
|
328
|
+
protectedHeaders: protMap,
|
|
329
|
+
unprotectedHeaders: unprotected,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
module.exports = {
|
|
334
|
+
sign: sign,
|
|
335
|
+
verify: verify,
|
|
336
|
+
ALGORITHMS: ALG_NAME_TO_ID,
|
|
337
|
+
COSE_SIGN1_TAG: COSE_SIGN1_TAG,
|
|
338
|
+
CoseError: CoseError,
|
|
339
|
+
};
|
package/lib/cwt.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
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");
|
|
105
|
+
}
|
|
106
|
+
validateOpts.requireObject(opts, "cwt.sign", CwtError);
|
|
107
|
+
validateOpts(opts, ["alg", "privateKey", "kid", "tagged", "externalAad"], "cwt.sign");
|
|
108
|
+
|
|
109
|
+
var map = new Map();
|
|
110
|
+
var keys = Object.keys(claims);
|
|
111
|
+
for (var i = 0; i < keys.length; i++) {
|
|
112
|
+
var name = keys[i];
|
|
113
|
+
var value = claims[name];
|
|
114
|
+
if (NUMERIC_DATE_CLAIMS[name] &&
|
|
115
|
+
(typeof value !== "number" || !Number.isInteger(value) || value < 0)) {
|
|
116
|
+
throw new CwtError("cwt/bad-numeric-date",
|
|
117
|
+
"cwt.sign: claim '" + name + "' must be a non-negative integer NumericDate (seconds)");
|
|
118
|
+
}
|
|
119
|
+
map.set(Object.prototype.hasOwnProperty.call(STD, name) ? STD[name] : name, value);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
var claimsCbor = cbor.encode(map);
|
|
123
|
+
var coseSign1 = await cose.sign(claimsCbor, {
|
|
124
|
+
alg: opts.alg, privateKey: opts.privateKey, kid: opts.kid, externalAad: opts.externalAad,
|
|
125
|
+
});
|
|
126
|
+
return opts.tagged === true ? Buffer.concat([CWT_TAG_PREFIX, coseSign1]) : coseSign1;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @primitive b.cwt.verify
|
|
131
|
+
* @signature b.cwt.verify(cwt, opts)
|
|
132
|
+
* @since 0.12.34
|
|
133
|
+
* @status stable
|
|
134
|
+
* @related b.cwt.sign, b.cose.verify
|
|
135
|
+
*
|
|
136
|
+
* Verify a CWT and return its claims. The COSE signature is checked
|
|
137
|
+
* via <code>b.cose.verify</code> (mandatory <code>algorithms</code>
|
|
138
|
+
* allowlist), then the standard time / identity claims are enforced:
|
|
139
|
+
* a passed <code>exp</code> (with <code>clockSkewSec</code> tolerance),
|
|
140
|
+
* a not-yet-valid <code>nbf</code>, and — when requested — an
|
|
141
|
+
* <code>iss</code> / <code>aud</code> mismatch are refused. Accepts a
|
|
142
|
+
* CWT-tag-61-wrapped or bare COSE_Sign1.
|
|
143
|
+
*
|
|
144
|
+
* @opts
|
|
145
|
+
* {
|
|
146
|
+
* algorithms: string[], // required — accepted COSE algs (allowlist)
|
|
147
|
+
* publicKey?: object, // verification key (per b.cose.verify)
|
|
148
|
+
* keyResolver?: function,
|
|
149
|
+
* expectedIssuer?: string, // require iss === this
|
|
150
|
+
* expectedAudience?: string, // require aud to include this
|
|
151
|
+
* clockSkewSec?: number, // default 60
|
|
152
|
+
* now?: number, // override clock (ms) for testing
|
|
153
|
+
* externalAad?: Buffer,
|
|
154
|
+
* }
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* var out = await b.cwt.verify(cwt, { algorithms: ["ES256"], publicKey: pub, expectedIssuer: "issuer.example" });
|
|
158
|
+
* // → { claims: { iss, sub, exp, scope }, raw: Map, protectedHeaders: Map }
|
|
159
|
+
*/
|
|
160
|
+
async function verify(cwt, opts) {
|
|
161
|
+
if (!Buffer.isBuffer(cwt) && !(cwt instanceof Uint8Array)) {
|
|
162
|
+
throw new CwtError("cwt/bad-input", "cwt.verify: cwt must be a Buffer / Uint8Array");
|
|
163
|
+
}
|
|
164
|
+
validateOpts.requireObject(opts, "cwt.verify", CwtError);
|
|
165
|
+
validateOpts(opts, [
|
|
166
|
+
"algorithms", "publicKey", "keyResolver", "expectedIssuer",
|
|
167
|
+
"expectedAudience", "clockSkewSec", "now", "externalAad",
|
|
168
|
+
], "cwt.verify");
|
|
169
|
+
|
|
170
|
+
// Strip the optional CWT tag-61 wrapper to recover the COSE_Sign1.
|
|
171
|
+
// Read the tag head generically (1 / 2 / 3 / 5 / 9-byte argument
|
|
172
|
+
// forms) rather than matching only the minimal 0xd8 0x3d encoding —
|
|
173
|
+
// an external CBOR encoder may emit a non-minimal but valid tag 61.
|
|
174
|
+
var coseBytes = Buffer.from(cwt);
|
|
175
|
+
var head = _readTagHead(coseBytes);
|
|
176
|
+
if (head && head.tag === 61) coseBytes = coseBytes.subarray(head.len); // allow:raw-byte-literal — CWT CBOR tag number (RFC 8392 §6)
|
|
177
|
+
|
|
178
|
+
var verified = await cose.verify(coseBytes, {
|
|
179
|
+
algorithms: opts.algorithms, publicKey: opts.publicKey,
|
|
180
|
+
keyResolver: opts.keyResolver, externalAad: opts.externalAad,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
var raw = cbor.decode(verified.payload);
|
|
184
|
+
if (!(raw instanceof Map)) {
|
|
185
|
+
throw new CwtError("cwt/bad-claims", "cwt.verify: claims payload is not a CBOR map");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Time claims (NumericDate, seconds). Skew tolerance both directions.
|
|
189
|
+
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
|
|
190
|
+
var now = _nowSec(opts);
|
|
191
|
+
// A present exp / nbf MUST be a well-formed NumericDate — a non-numeric
|
|
192
|
+
// value would otherwise bypass the time check entirely (a token could
|
|
193
|
+
// carry exp: "whenever" and never expire). Refuse the malformed claim.
|
|
194
|
+
if (raw.has(STD.exp)) {
|
|
195
|
+
var exp = raw.get(STD.exp);
|
|
196
|
+
if (typeof exp !== "number" || !isFinite(exp)) {
|
|
197
|
+
throw new CwtError("cwt/malformed-claim", "cwt.verify: exp claim is present but not a numeric NumericDate");
|
|
198
|
+
}
|
|
199
|
+
if (now > exp + skew) {
|
|
200
|
+
throw new CwtError("cwt/expired", "cwt.verify: token expired (exp " + exp + " < now " + now + ")");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (raw.has(STD.nbf)) {
|
|
204
|
+
var nbf = raw.get(STD.nbf);
|
|
205
|
+
if (typeof nbf !== "number" || !isFinite(nbf)) {
|
|
206
|
+
throw new CwtError("cwt/malformed-claim", "cwt.verify: nbf claim is present but not a numeric NumericDate");
|
|
207
|
+
}
|
|
208
|
+
if (now < nbf - skew) {
|
|
209
|
+
throw new CwtError("cwt/not-yet-valid", "cwt.verify: token not yet valid (nbf " + nbf + " > now " + now + ")");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (opts.expectedIssuer != null) {
|
|
213
|
+
if (raw.get(STD.iss) !== opts.expectedIssuer) {
|
|
214
|
+
throw new CwtError("cwt/issuer-mismatch", "cwt.verify: iss does not match expectedIssuer");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (opts.expectedAudience != null) {
|
|
218
|
+
var aud = raw.get(STD.aud);
|
|
219
|
+
var audOk = Array.isArray(aud) ? aud.indexOf(opts.expectedAudience) !== -1 : aud === opts.expectedAudience;
|
|
220
|
+
if (!audOk) {
|
|
221
|
+
throw new CwtError("cwt/audience-mismatch", "cwt.verify: aud does not include expectedAudience");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Build a friendly claims object (standard labels → names).
|
|
226
|
+
var claims = {};
|
|
227
|
+
raw.forEach(function (v, k) {
|
|
228
|
+
claims[Object.prototype.hasOwnProperty.call(STD_BY_LABEL, k) ? STD_BY_LABEL[k] : k] = v;
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return { claims: claims, raw: raw, alg: verified.alg, protectedHeaders: verified.protectedHeaders };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = {
|
|
235
|
+
sign: sign,
|
|
236
|
+
verify: verify,
|
|
237
|
+
CLAIM_LABELS: STD,
|
|
238
|
+
CwtError: CwtError,
|
|
239
|
+
};
|
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:619f056d-4027-4805-8bea-f39101edd638",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-24T22:52:17.036Z",
|
|
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.34",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.12.
|
|
25
|
+
"version": "0.12.34",
|
|
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.34",
|
|
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.34",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|