@blamejs/blamejs-shop 0.0.127 → 0.0.128
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 +2 -0
- package/README.md +2 -0
- package/lib/storefront.js +128 -0
- package/lib/vendor/MANIFEST.json +3 -3
- package/lib/vendor/blamejs/CHANGELOG.md +12 -0
- package/lib/vendor/blamejs/README.md +6 -1
- package/lib/vendor/blamejs/SECURITY.md +3 -0
- package/lib/vendor/blamejs/api-snapshot.json +378 -2
- package/lib/vendor/blamejs/index.js +5 -0
- package/lib/vendor/blamejs/lib/cose.js +212 -4
- package/lib/vendor/blamejs/lib/cwt.js +244 -0
- package/lib/vendor/blamejs/lib/eat.js +240 -0
- package/lib/vendor/blamejs/lib/scitt.js +243 -0
- package/lib/vendor/blamejs/lib/tsa.js +688 -0
- package/lib/vendor/blamejs/lib/vc.js +328 -0
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.12.34.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.35.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.12.36.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.37.json +27 -0
- package/lib/vendor/blamejs/release-notes/v0.12.38.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.39.json +18 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +9 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cose.test.js +84 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/cwt.test.js +137 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/eat.test.js +111 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/scitt.test.js +158 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/tsa.test.js +373 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vc.test.js +188 -0
- package/package.json +1 -1
|
@@ -63,6 +63,7 @@ var HDR_ALG = 1;
|
|
|
63
63
|
var HDR_CRIT = 2; // header label: crit
|
|
64
64
|
var HDR_CONTENT_TYPE = 3; // header label: content type
|
|
65
65
|
var HDR_KID = 4; // header label: kid
|
|
66
|
+
var HDR_CWT_CLAIMS = 15; // allow:raw-byte-literal — RFC 9597 CWT Claims header label (carries SCITT iss/sub)
|
|
66
67
|
|
|
67
68
|
// COSE algorithm identifiers. ML-DSA-87 is a NON-FINAL requested
|
|
68
69
|
// assignment (draft-ietf-cose-dilithium) — pinned deliberately, re-open
|
|
@@ -83,7 +84,7 @@ var SIGNABLE = ["ML-DSA-87", "ES256", "ES384", "ES512", "EdDSA"];
|
|
|
83
84
|
|
|
84
85
|
// Header labels this verifier understands — a `crit` entry naming any
|
|
85
86
|
// 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
|
+
var UNDERSTOOD_LABELS = [HDR_ALG, HDR_CRIT, HDR_CONTENT_TYPE, HDR_KID, HDR_CWT_CLAIMS];
|
|
87
88
|
|
|
88
89
|
function _toKeyObject(key, kind) {
|
|
89
90
|
if (key && typeof key === "object" && typeof key.asymmetricKeyType === "string") return key;
|
|
@@ -138,9 +139,10 @@ function _toBeSigned(protectedBstr, externalAad, payload) {
|
|
|
138
139
|
* alg: string, // "ES256" | "ES384" | "ES512" | "EdDSA" | "ML-DSA-87"
|
|
139
140
|
* privateKey: object, // matching KeyObject or PEM
|
|
140
141
|
* kid?: string, // → unprotected header label 4
|
|
141
|
-
* contentType?: number,
|
|
142
|
+
* contentType?: number|string, // → protected header label 3 (CoAP Content-Format uint or media-type string)
|
|
142
143
|
* externalAad?: Buffer, // default empty — bound into the signature
|
|
143
144
|
* unprotectedHeaders?: object, // extra unprotected map entries (numeric keys)
|
|
145
|
+
* protectedHeaders?: object, // extra INTEGRITY-PROTECTED map entries (numeric keys); label 1 (alg) is reserved
|
|
144
146
|
* }
|
|
145
147
|
*
|
|
146
148
|
* @example
|
|
@@ -150,7 +152,7 @@ function _toBeSigned(protectedBstr, externalAad, payload) {
|
|
|
150
152
|
*/
|
|
151
153
|
async function sign(payload, opts) {
|
|
152
154
|
validateOpts.requireObject(opts, "cose.sign", CoseError);
|
|
153
|
-
validateOpts(opts, ["alg", "privateKey", "kid", "contentType", "externalAad", "unprotectedHeaders"], "cose.sign");
|
|
155
|
+
validateOpts(opts, ["alg", "privateKey", "kid", "contentType", "externalAad", "unprotectedHeaders", "protectedHeaders"], "cose.sign");
|
|
154
156
|
if (SIGNABLE.indexOf(opts.alg) === -1) {
|
|
155
157
|
throw new CoseError("cose/unsignable-alg",
|
|
156
158
|
"cose.sign: alg must be one of " + SIGNABLE.join(" / ") +
|
|
@@ -165,7 +167,31 @@ async function sign(payload, opts) {
|
|
|
165
167
|
|
|
166
168
|
var protMap = new Map();
|
|
167
169
|
protMap.set(HDR_ALG, algId);
|
|
168
|
-
|
|
170
|
+
// Content type (RFC 9052 §3.1): a uint (CoAP Content-Format) or a
|
|
171
|
+
// media-type string (tstr) — a SCITT signed statement declares its
|
|
172
|
+
// payload media type as a string here.
|
|
173
|
+
if (typeof opts.contentType === "number" || typeof opts.contentType === "string") {
|
|
174
|
+
protMap.set(HDR_CONTENT_TYPE, opts.contentType);
|
|
175
|
+
}
|
|
176
|
+
// Extra integrity-protected headers (e.g. CWT_Claims label 15 for a
|
|
177
|
+
// SCITT signed statement). alg (label 1) is managed via opts.alg and
|
|
178
|
+
// cannot be overridden here — a caller that needs a different alg
|
|
179
|
+
// names it in opts.alg.
|
|
180
|
+
if (opts.protectedHeaders && typeof opts.protectedHeaders === "object") {
|
|
181
|
+
var pk = opts.protectedHeaders instanceof Map
|
|
182
|
+
? Array.from(opts.protectedHeaders.keys())
|
|
183
|
+
: Object.keys(opts.protectedHeaders);
|
|
184
|
+
for (var pi = 0; pi < pk.length; pi++) {
|
|
185
|
+
var plabel = Number(pk[pi]);
|
|
186
|
+
if (plabel === HDR_ALG) {
|
|
187
|
+
throw new CoseError("cose/reserved-header",
|
|
188
|
+
"cose.sign: protectedHeaders must not set label 1 (alg) — pass opts.alg instead");
|
|
189
|
+
}
|
|
190
|
+
var pval = opts.protectedHeaders instanceof Map
|
|
191
|
+
? opts.protectedHeaders.get(pk[pi]) : opts.protectedHeaders[pk[pi]];
|
|
192
|
+
protMap.set(plabel, pval);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
169
195
|
var protectedBstr = cbor.encode(protMap);
|
|
170
196
|
|
|
171
197
|
var unprot = new Map();
|
|
@@ -330,10 +356,192 @@ async function verify(coseSign1, opts) {
|
|
|
330
356
|
};
|
|
331
357
|
}
|
|
332
358
|
|
|
359
|
+
// ---- COSE_Encrypt0 (RFC 9052 §5.2) — single-recipient AEAD ----
|
|
360
|
+
|
|
361
|
+
var COSE_ENCRYPT0_TAG = 16; // allow:raw-byte-literal — RFC 9052 COSE_Encrypt0 CBOR tag
|
|
362
|
+
var HDR_IV = 5; // RFC 9052 §3.1 unprotected header label: IV
|
|
363
|
+
var AEAD_TAG_LEN = 16; // allow:raw-byte-literal — AEAD authentication tag length (bytes)
|
|
364
|
+
|
|
365
|
+
// AEAD algorithm: COSE id → node cipher + key / IV sizes. ChaCha20/
|
|
366
|
+
// Poly1305 (24) is the default; AES-GCM is opt-in (project hard-rule
|
|
367
|
+
// #2 forbids AES-GCM as a default).
|
|
368
|
+
var AEAD_NAME_TO_ID = { "ChaCha20-Poly1305": 24, "A256GCM": 3, "A128GCM": 1 }; // allow:raw-byte-literal — COSE AEAD algorithm identifiers (RFC 9053), not sizes
|
|
369
|
+
var AEAD_ID_TO_NAME = {};
|
|
370
|
+
Object.keys(AEAD_NAME_TO_ID).forEach(function (k) { AEAD_ID_TO_NAME[AEAD_NAME_TO_ID[k]] = k; });
|
|
371
|
+
|
|
372
|
+
function _aeadParams(algId) {
|
|
373
|
+
switch (algId) {
|
|
374
|
+
case 24: return { cipher: "chacha20-poly1305", keyLen: 32, ivLen: 12 }; // allow:raw-byte-literal — ChaCha20/Poly1305 key+IV sizes
|
|
375
|
+
case 3: return { cipher: "aes-256-gcm", keyLen: 32, ivLen: 12 }; // allow:raw-byte-literal — AES-256-GCM key+IV sizes
|
|
376
|
+
case 1: return { cipher: "aes-128-gcm", keyLen: 16, ivLen: 12 }; // allow:raw-byte-literal — AES-128-GCM key+IV sizes
|
|
377
|
+
default:
|
|
378
|
+
throw new CoseError("cose/unknown-alg", "cose: unrecognized AEAD COSE alg id " + algId);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Enc_structure (§5.3) = [ "Encrypt0", body_protected (bstr), external_aad (bstr) ]
|
|
383
|
+
// — deterministically CBOR-encoded, used as the AEAD associated data.
|
|
384
|
+
function _encStructure(protectedBstr, externalAad) {
|
|
385
|
+
return cbor.encode(["Encrypt0", protectedBstr, externalAad]);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* @primitive b.cose.encrypt0
|
|
390
|
+
* @signature b.cose.encrypt0(plaintext, opts)
|
|
391
|
+
* @since 0.12.36
|
|
392
|
+
* @status stable
|
|
393
|
+
* @related b.cose.decrypt0, b.cose.sign
|
|
394
|
+
*
|
|
395
|
+
* Encrypt bytes into a tagged COSE_Encrypt0 (RFC 9052 §5.2), a
|
|
396
|
+
* single-recipient AEAD container where the recipient already holds
|
|
397
|
+
* the symmetric key (direct mode). Default algorithm is
|
|
398
|
+
* <code>ChaCha20-Poly1305</code>; <code>A256GCM</code> / <code>A128GCM</code>
|
|
399
|
+
* are opt-in. The Enc_structure is bound as the AEAD associated data,
|
|
400
|
+
* and the authentication tag is appended to the ciphertext per COSE.
|
|
401
|
+
*
|
|
402
|
+
* @opts
|
|
403
|
+
* {
|
|
404
|
+
* alg: string, // "ChaCha20-Poly1305" (default) | "A256GCM" | "A128GCM"
|
|
405
|
+
* key: Buffer, // symmetric key (32 bytes for ChaCha/A256GCM, 16 for A128GCM)
|
|
406
|
+
* iv?: Buffer, // 12-byte IV (random if omitted)
|
|
407
|
+
* externalAad?: Buffer, // bound into the AEAD tag
|
|
408
|
+
* unprotectedHeaders?: object,
|
|
409
|
+
* }
|
|
410
|
+
*
|
|
411
|
+
* @example
|
|
412
|
+
* var enc = b.cose.encrypt0(Buffer.from("secret"), { alg: "ChaCha20-Poly1305", key: k });
|
|
413
|
+
*/
|
|
414
|
+
function encrypt0(plaintext, opts) {
|
|
415
|
+
validateOpts.requireObject(opts, "cose.encrypt0", CoseError);
|
|
416
|
+
validateOpts(opts, ["alg", "key", "iv", "externalAad", "unprotectedHeaders"], "cose.encrypt0");
|
|
417
|
+
var alg = opts.alg || "ChaCha20-Poly1305";
|
|
418
|
+
if (!(alg in AEAD_NAME_TO_ID)) {
|
|
419
|
+
throw new CoseError("cose/unknown-alg", "cose.encrypt0: alg must be one of " + Object.keys(AEAD_NAME_TO_ID).join(" / "));
|
|
420
|
+
}
|
|
421
|
+
var algId = AEAD_NAME_TO_ID[alg];
|
|
422
|
+
var p = _aeadParams(algId);
|
|
423
|
+
var key = _bstr(opts.key);
|
|
424
|
+
if (key.length !== p.keyLen) throw new CoseError("cose/bad-key", "cose.encrypt0: " + alg + " requires a " + p.keyLen + "-byte key");
|
|
425
|
+
var iv = opts.iv != null ? _bstr(opts.iv) : nodeCrypto.randomBytes(p.ivLen);
|
|
426
|
+
if (iv.length !== p.ivLen) throw new CoseError("cose/bad-iv", "cose.encrypt0: " + alg + " requires a " + p.ivLen + "-byte IV");
|
|
427
|
+
|
|
428
|
+
var protMap = new Map(); protMap.set(HDR_ALG, algId);
|
|
429
|
+
var protectedBstr = cbor.encode(protMap);
|
|
430
|
+
var aad = _encStructure(protectedBstr, opts.externalAad == null ? Buffer.alloc(0) : _bstr(opts.externalAad));
|
|
431
|
+
|
|
432
|
+
var cipher = nodeCrypto.createCipheriv(p.cipher, key, iv, { authTagLength: AEAD_TAG_LEN });
|
|
433
|
+
cipher.setAAD(aad);
|
|
434
|
+
var ct = Buffer.concat([cipher.update(_bstr(plaintext)), cipher.final()]);
|
|
435
|
+
var ciphertext = Buffer.concat([ct, cipher.getAuthTag()]); // COSE appends the auth tag to the ciphertext
|
|
436
|
+
|
|
437
|
+
var unprot = new Map(); unprot.set(HDR_IV, iv);
|
|
438
|
+
if (opts.unprotectedHeaders && typeof opts.unprotectedHeaders === "object") {
|
|
439
|
+
var uk = Object.keys(opts.unprotectedHeaders);
|
|
440
|
+
for (var i = 0; i < uk.length; i++) {
|
|
441
|
+
var label = Number(uk[i]);
|
|
442
|
+
// The IV (label 5) is managed via opts.iv and must match the IV
|
|
443
|
+
// the AEAD used — refuse an override that would emit a token whose
|
|
444
|
+
// stored IV disagrees with the one it was encrypted under.
|
|
445
|
+
if (label === HDR_IV) {
|
|
446
|
+
throw new CoseError("cose/reserved-header",
|
|
447
|
+
"cose.encrypt0: unprotectedHeaders must not set label 5 (IV) — pass opts.iv instead");
|
|
448
|
+
}
|
|
449
|
+
unprot.set(label, opts.unprotectedHeaders[uk[i]]);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return cbor.encode(new cbor.Tag(COSE_ENCRYPT0_TAG, [protectedBstr, unprot, ciphertext]));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* @primitive b.cose.decrypt0
|
|
457
|
+
* @signature b.cose.decrypt0(coseEncrypt0, opts)
|
|
458
|
+
* @since 0.12.36
|
|
459
|
+
* @status stable
|
|
460
|
+
* @related b.cose.encrypt0
|
|
461
|
+
*
|
|
462
|
+
* Decrypt a COSE_Encrypt0 and return the plaintext. The algorithm is
|
|
463
|
+
* read from the protected header and must be in
|
|
464
|
+
* <code>opts.algorithms</code>; the Enc_structure is reconstructed as
|
|
465
|
+
* the AEAD associated data and authentication failure (wrong key /
|
|
466
|
+
* tampered ciphertext or AAD) is refused.
|
|
467
|
+
*
|
|
468
|
+
* @opts
|
|
469
|
+
* {
|
|
470
|
+
* key: Buffer, // symmetric key
|
|
471
|
+
* algorithms: string[], // required — accepted AEAD algs (allowlist)
|
|
472
|
+
* externalAad?: Buffer, // must match what was encrypted
|
|
473
|
+
* maxBytes?: number,
|
|
474
|
+
* maxDepth?: number,
|
|
475
|
+
* }
|
|
476
|
+
*
|
|
477
|
+
* @example
|
|
478
|
+
* var pt = b.cose.decrypt0(enc, { key: k, algorithms: ["ChaCha20-Poly1305"] }).plaintext;
|
|
479
|
+
*/
|
|
480
|
+
function decrypt0(coseEncrypt0, opts) {
|
|
481
|
+
validateOpts.requireObject(opts, "cose.decrypt0", CoseError);
|
|
482
|
+
validateOpts(opts, ["key", "algorithms", "externalAad", "maxBytes", "maxDepth"], "cose.decrypt0");
|
|
483
|
+
if (!Array.isArray(opts.algorithms) || opts.algorithms.length === 0) {
|
|
484
|
+
throw new CoseError("cose/algorithms-required", "cose.decrypt0: opts.algorithms is required (no defaults — name the accepted algorithms)");
|
|
485
|
+
}
|
|
486
|
+
var decoded = cbor.decode(_bstr(coseEncrypt0), { allowedTags: [COSE_ENCRYPT0_TAG], maxBytes: opts.maxBytes, maxDepth: opts.maxDepth });
|
|
487
|
+
var arr = (decoded instanceof cbor.Tag && decoded.tag === COSE_ENCRYPT0_TAG) ? decoded.value : decoded;
|
|
488
|
+
if (!Array.isArray(arr) || arr.length !== 3) {
|
|
489
|
+
throw new CoseError("cose/malformed", "cose.decrypt0: not a COSE_Encrypt0 (expected a 3-element array)");
|
|
490
|
+
}
|
|
491
|
+
var protectedBstr = arr[0], unprotected = arr[1], ciphertext = arr[2];
|
|
492
|
+
if (!Buffer.isBuffer(protectedBstr) || !Buffer.isBuffer(ciphertext)) {
|
|
493
|
+
throw new CoseError("cose/malformed", "cose.decrypt0: protected header and ciphertext must be byte strings");
|
|
494
|
+
}
|
|
495
|
+
if (!(unprotected instanceof Map)) {
|
|
496
|
+
throw new CoseError("cose/malformed", "cose.decrypt0: unprotected header must be a CBOR map");
|
|
497
|
+
}
|
|
498
|
+
var protMap = protectedBstr.length === 0 ? new Map()
|
|
499
|
+
: cbor.decode(protectedBstr, { maxBytes: opts.maxBytes, maxDepth: opts.maxDepth });
|
|
500
|
+
if (!(protMap instanceof Map)) {
|
|
501
|
+
throw new CoseError("cose/malformed", "cose.decrypt0: protected header is not a CBOR map");
|
|
502
|
+
}
|
|
503
|
+
var algId = protMap.get(HDR_ALG);
|
|
504
|
+
var algName = AEAD_ID_TO_NAME[algId];
|
|
505
|
+
if (algName === undefined) {
|
|
506
|
+
throw new CoseError("cose/unknown-alg", "cose.decrypt0: unrecognized AEAD alg id " + algId);
|
|
507
|
+
}
|
|
508
|
+
if (opts.algorithms.indexOf(algName) === -1) {
|
|
509
|
+
throw new CoseError("cose/alg-not-allowed", "cose.decrypt0: alg '" + algName + "' is not in the allowlist");
|
|
510
|
+
}
|
|
511
|
+
var p = _aeadParams(algId);
|
|
512
|
+
var key = _bstr(opts.key);
|
|
513
|
+
if (key.length !== p.keyLen) throw new CoseError("cose/bad-key", "cose.decrypt0: " + algName + " requires a " + p.keyLen + "-byte key");
|
|
514
|
+
var iv = unprotected.get(HDR_IV);
|
|
515
|
+
if (!Buffer.isBuffer(iv) || iv.length !== p.ivLen) {
|
|
516
|
+
throw new CoseError("cose/bad-iv", "cose.decrypt0: missing or wrong-length IV (unprotected label 5)");
|
|
517
|
+
}
|
|
518
|
+
if (ciphertext.length < AEAD_TAG_LEN) {
|
|
519
|
+
throw new CoseError("cose/malformed", "cose.decrypt0: ciphertext shorter than the AEAD tag");
|
|
520
|
+
}
|
|
521
|
+
var tag = ciphertext.subarray(ciphertext.length - AEAD_TAG_LEN);
|
|
522
|
+
var ct = ciphertext.subarray(0, ciphertext.length - AEAD_TAG_LEN);
|
|
523
|
+
var aad = _encStructure(protectedBstr, opts.externalAad == null ? Buffer.alloc(0) : _bstr(opts.externalAad));
|
|
524
|
+
|
|
525
|
+
var decipher = nodeCrypto.createDecipheriv(p.cipher, key, iv, { authTagLength: AEAD_TAG_LEN });
|
|
526
|
+
decipher.setAAD(aad);
|
|
527
|
+
decipher.setAuthTag(tag);
|
|
528
|
+
var pt;
|
|
529
|
+
try {
|
|
530
|
+
pt = Buffer.concat([decipher.update(ct), decipher.final()]);
|
|
531
|
+
} catch (_e) {
|
|
532
|
+
throw new CoseError("cose/decrypt-failed", "cose.decrypt0: AEAD authentication failed (wrong key, tampered ciphertext, or AAD mismatch)");
|
|
533
|
+
}
|
|
534
|
+
return { plaintext: pt, alg: algName, protectedHeaders: protMap, unprotectedHeaders: unprotected };
|
|
535
|
+
}
|
|
536
|
+
|
|
333
537
|
module.exports = {
|
|
334
538
|
sign: sign,
|
|
335
539
|
verify: verify,
|
|
540
|
+
encrypt0: encrypt0,
|
|
541
|
+
decrypt0: decrypt0,
|
|
336
542
|
ALGORITHMS: ALG_NAME_TO_ID,
|
|
543
|
+
AEAD_ALGORITHMS: AEAD_NAME_TO_ID,
|
|
337
544
|
COSE_SIGN1_TAG: COSE_SIGN1_TAG,
|
|
545
|
+
COSE_ENCRYPT0_TAG: COSE_ENCRYPT0_TAG,
|
|
338
546
|
CoseError: CoseError,
|
|
339
547
|
};
|
|
@@ -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
|
+
};
|