@blamejs/pki 0.5.16 → 0.5.17
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 +16 -1
- package/lib/acme.js +1 -0
- package/lib/asn1-der.js +1 -0
- package/lib/attrcert-sign.js +1 -0
- package/lib/cbor-det.js +1 -0
- package/lib/cmc-verify.js +1 -0
- package/lib/cmp-build.js +1 -0
- package/lib/cms-decrypt.js +58 -29
- package/lib/cms-encrypt.js +45 -24
- package/lib/cms-sign.js +3 -3
- package/lib/cms-verify.js +5 -1
- package/lib/constants.js +1 -0
- package/lib/crl-sign.js +1 -0
- package/lib/crmf-sign.js +1 -0
- package/lib/csr-sign.js +1 -0
- package/lib/ct.js +1 -0
- package/lib/est.js +1 -0
- package/lib/framework-error.js +1 -0
- package/lib/guard-secret.js +45 -1
- package/lib/hpke.js +6 -2
- package/lib/http-transport.js +1 -0
- package/lib/inspect.js +1 -0
- package/lib/jose.js +1 -0
- package/lib/key.js +2 -1
- package/lib/lint.js +1 -0
- package/lib/merkle.js +1 -0
- package/lib/ocsp.js +1 -0
- package/lib/oid.js +1 -0
- package/lib/path-validate.js +1 -0
- package/lib/pbes2.js +16 -8
- package/lib/pkcs12-build.js +2 -1
- package/lib/schema-all.js +1 -0
- package/lib/schema-c509.js +1 -0
- package/lib/schema-csrattrs.js +1 -0
- package/lib/schema-engine.js +1 -0
- package/lib/schema-pkcs8.js +1 -0
- package/lib/schema-smime.js +1 -0
- package/lib/schema-tsp.js +1 -0
- package/lib/schema-x509.js +1 -0
- package/lib/shbs.js +1 -0
- package/lib/sigstore.js +1 -0
- package/lib/smime.js +1 -0
- package/lib/tls-cert-compress.js +1 -0
- package/lib/trust.js +1 -0
- package/lib/tsp-sign.js +1 -0
- package/lib/webauthn.js +1 -0
- package/lib/webcrypto.js +11 -8
- package/lib/x509-sign.js +1 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,22 @@ All notable changes to `@blamejs/pki` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
-
## v0.5.
|
|
7
|
+
## v0.5.17 — 2026-08-20
|
|
8
|
+
|
|
9
|
+
A failed integrity check destroys the plaintext it recovered, AuthEnvelopedData validates the attributes it is asked to authenticate, and every refusal names the rule it is actually applying.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- The refusals for a content cipher in the wrong container now cite the rule they apply. RFC 5083 sec. 2 defines `AuthEnvelopedData` in terms of a content-authenticated-encryption algorithm; it never names GCM, and RFC 5084 sec. 3.1 defines AES-CCM for that content type. Saying the standard required GCM sent an operator to argue with a specification that says the opposite. The check now draws the line the RFC draws -- authenticated versus not -- and an AES-CCM message is declined separately, by name, as an algorithm this toolkit does not implement rather than as an unrecognized OID. Error codes are unchanged.
|
|
14
|
+
- Documentation pages at pkijs.com carry the expanded name of the format they describe in the title and heading, so a page about CMS says Cryptographic Message Syntax rather than three letters that mean a content management system elsewhere. The sitemap now reports each page's real last-changed date, taken from the commit that last touched the source it is generated from, and omits the date where it is unknown instead of reporting the build time. The home page gained runnable examples for the six things people most often arrive wanting to do.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- A failed AEAD integrity check now destroys the plaintext it recovered. `update` returns the complete recovered plaintext and `final` is what decides whether the message was authentic at all, so on a forged `AuthEnvelopedData` the plaintext existed in full before anything had judged it, and the buffer was then dropped unreferenced and unzeroed. RFC 5083 sec. 1 requires it to be destroyed. The success path leaked too, less visibly: joining the two halves copies them, leaving the first buffer as a second complete copy of the plaintext that the caller never receives and nothing else would ever clear. Both exits are now cleared, at every cipher the toolkit runs -- CMS content decryption and the RFC 3211 password-recipient unwrap, PBES2, HPKE `open`, PKCS#12 safe decryption, and the AES-GCM, AES-CBC, AES-CTR and AES-KW paths of the WebCrypto engine.
|
|
19
|
+
- `pki.cms.encrypt` validates the authenticated attributes a caller supplies for an `AuthEnvelopedData` before it MACs them. Each must be a well-formed `Attribute SEQUENCE { type, non-empty SET OF value }` with no repeated type (RFC 5652 sec. 5.3). `AuthenticatedData` had enforced this all along; `AuthEnvelopedData` accepted whatever it was handed and emitted it, so a malformed attribute reached the wire with the MAC already computed over it and the operator learned of it from a peer's parser rather than from the builder.
|
|
20
|
+
- `pki.cms.encrypt` refuses a `message-digest` attribute in an `AuthEnvelopedData`'s `authAttrs`. Its value is the unencrypted one-way hash of the plaintext, so disclosing it alongside the ciphertext enables content tracking and confirms a guessed plaintext against a message that was encrypted to prevent exactly that (RFC 5083 sec. 2.1 and sec. 5). Decryption still accepts one, since another implementation may legitimately emit it; the toolkit will not produce one. `AuthenticatedData` is unaffected -- it builds and MACs its own `message-digest` by design (RFC 5652 sec. 9.2).
|
|
21
|
+
|
|
22
|
+
## v0.5.16 — 2026-08-19
|
|
8
23
|
|
|
9
24
|
`pki.cmp.verify` reads every option at the call, and every guard takes what it needs from the runtime when it loads, so code a caller runs afterwards cannot change what a verification decides.
|
|
10
25
|
|
package/lib/acme.js
CHANGED
package/lib/asn1-der.js
CHANGED
package/lib/attrcert-sign.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* @module pki.attrcert
|
|
7
7
|
* @nav Signing
|
|
8
8
|
* @title Attribute certificates
|
|
9
|
+
* @fullname X.509 attribute certificates (RFC 5755)
|
|
9
10
|
* @intro The RFC 5755 attribute-certificate producing side. `pki.attrcert.sign` builds an
|
|
10
11
|
* `AttributeCertificateInfo` binding a Holder to a set of privilege attributes (role, clearance,
|
|
11
12
|
* group, ...) over a validity window, signs it with an Attribute Authority's private key, and emits an
|
package/lib/cbor-det.js
CHANGED
package/lib/cmc-verify.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @module pki.cmc
|
|
6
6
|
* @nav Enrollment
|
|
7
7
|
* @title CMC
|
|
8
|
+
* @fullname CMC (Certificate Management over CMS, RFC 5272)
|
|
8
9
|
* @intro Interpret an RFC 5272 Full PKI Response. `verify(response, sent)` takes the response
|
|
9
10
|
* a CA returned and the state the client retained from its request, binds the two together
|
|
10
11
|
* (transaction identifier, the Sender/Recipient Nonce echo, the Data Return echo), reads the
|
package/lib/cmp-build.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* @module pki.cmp
|
|
7
7
|
* @nav Signing
|
|
8
8
|
* @title Certificate management protocol messages
|
|
9
|
+
* @fullname CMP (Certificate Management Protocol, RFC 4210)
|
|
9
10
|
* @intro The RFC 9810 Certificate Management Protocol producing side. `pki.cmp.build` assembles a
|
|
10
11
|
* `PKIMessage` -- a `PKIHeader` (sender / recipient / transaction metadata), a `PKIBody` carrying one
|
|
11
12
|
* request or transaction (an `ir` / `cr` / `kur` certificate request via `pki.crmf.build`, a `p10cr`
|
package/lib/cms-decrypt.js
CHANGED
|
@@ -29,6 +29,10 @@ var subtle = webcrypto.webcrypto.subtle;
|
|
|
29
29
|
var CmsError = frameworkError.CmsError;
|
|
30
30
|
var WRAP_KEK_LENGTHS = schemaCms.WRAP_KEK_LENGTHS;
|
|
31
31
|
var KEM_CT_LENGTHS = schemaCms.KEM_CT_LENGTHS;
|
|
32
|
+
// The parser's own AEAD table: the single place that decides whether a content-encryption OID names
|
|
33
|
+
// an authenticated encryption algorithm, so the container-vs-mode rule below cannot drift from the
|
|
34
|
+
// parameter validation that accepted the message.
|
|
35
|
+
var AEAD_ALGS = schemaCms.AEAD_ALGS;
|
|
32
36
|
|
|
33
37
|
function O(n) { return oid.byName(n); }
|
|
34
38
|
function _err(code, message, cause) { return new CmsError(code, message, cause); }
|
|
@@ -397,26 +401,45 @@ async function _kemriCek(ri, km) {
|
|
|
397
401
|
}
|
|
398
402
|
|
|
399
403
|
// The declared cipher MODE must match the container's authentication model: an AEAD cipher belongs in
|
|
400
|
-
// AuthEnvelopedData and a plain CBC cipher in EnvelopedData (RFC 5083 sec. 2.
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
//
|
|
404
|
+
// AuthEnvelopedData and a plain CBC cipher in EnvelopedData (RFC 5083 sec. 2). Key length alone does
|
|
405
|
+
// not separate them, so without this an EnvelopedData naming AES-GCM was opened as unauthenticated CBC
|
|
406
|
+
// while the result still reported the AEAD algorithm, telling the caller the content was authenticated
|
|
407
|
+
// when nothing had authenticated it. The mirror case dereferenced AEAD parameters the parser leaves
|
|
408
|
+
// null for a non-AEAD algorithm, faulting instead of failing closed.
|
|
409
|
+
//
|
|
410
|
+
// The line drawn here is AEAD-vs-not, which is the line RFC 5083 draws. GCM-vs-CBC is a narrower
|
|
411
|
+
// line this toolkit draws, because AES-GCM is the only AEAD it implements, and stating the narrower
|
|
412
|
+
// one as though the RFC required it sends an operator to argue with a specification that says the
|
|
413
|
+
// opposite: RFC 5084 sec. 3.1 defines AES-CCM for this content type. So an AEAD reaching the right
|
|
414
|
+
// container passes here even when decryption will later decline it, and it is _openContent that says
|
|
415
|
+
// why, by name. An unresolvable OID is left to _openContent too, as the unsupported algorithm it is.
|
|
406
416
|
function _assertContentCipherMode(eci, ct) {
|
|
407
417
|
var oidStr = eci.contentEncryptionAlgorithm.oid;
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
if (
|
|
411
|
-
|
|
418
|
+
var isAead = guard.intrinsic.hasOwn(AEAD_ALGS, oidStr);
|
|
419
|
+
if (!isAead && !guard.intrinsic.hasOwn(CONTENT_MODE, oidStr)) return;
|
|
420
|
+
if (ct === "authEnvelopedData") {
|
|
421
|
+
if (isAead) return;
|
|
422
|
+
throw _err("cms/unsupported-algorithm", "contentEncryptionAlgorithm " + oidStr + " is not an authenticated encryption algorithm, which authEnvelopedData requires (RFC 5083 sec. 2)");
|
|
412
423
|
}
|
|
424
|
+
if (!isAead) return;
|
|
425
|
+
throw _err("cms/unsupported-algorithm", "contentEncryptionAlgorithm " + oidStr + " is an authenticated encryption algorithm, which belongs in an authEnvelopedData rather than a " + ct + " (RFC 5083 sec. 2)");
|
|
413
426
|
}
|
|
414
427
|
|
|
415
428
|
// ---- stage 3: open the content (uniform failure) ---------------------------
|
|
416
429
|
async function _openContent(parsed, eci, cek, ct) {
|
|
417
430
|
var alg = eci.contentEncryptionAlgorithm;
|
|
418
|
-
var keyBits = CONTENT_KEYBITS[alg.oid];
|
|
419
|
-
if (!keyBits)
|
|
431
|
+
var keyBits = guard.intrinsic.hasOwn(CONTENT_KEYBITS, alg.oid) ? CONTENT_KEYBITS[alg.oid] : 0;
|
|
432
|
+
if (!keyBits) {
|
|
433
|
+
// AES-CCM parses completely here -- registered OIDs, the sec. 3.1 nonce bound, the seven legal
|
|
434
|
+
// ICV lengths -- and is then declined, because AES-GCM is the only AEAD this toolkit implements
|
|
435
|
+
// for content encryption. Saying only "unsupported <oid>" is what an operator also sees for an
|
|
436
|
+
// OID nothing in the toolkit recognizes, and the two call for different things: one is a
|
|
437
|
+
// capability gap to route around, the other is a malformed or exotic message.
|
|
438
|
+
if (guard.intrinsic.hasOwn(AEAD_ALGS, alg.oid) && AEAD_ALGS[alg.oid] === "ccm") {
|
|
439
|
+
throw _err("cms/unsupported-algorithm", "contentEncryptionAlgorithm " + alg.oid + " is AES-CCM. RFC 5084 sec. 3.1 permits it for this content type; this toolkit implements AES-GCM only for content encryption");
|
|
440
|
+
}
|
|
441
|
+
throw _err("cms/unsupported-algorithm", "unsupported contentEncryptionAlgorithm " + alg.oid);
|
|
442
|
+
}
|
|
420
443
|
if (eci.encryptedContent == null) throw _err("cms/no-encrypted-content", "the message has no encryptedContent (detached; supply it out of band)");
|
|
421
444
|
// A null CEK (v1.5 implicit rejection) or a wrong-length CEK -> a fresh random key of the right
|
|
422
445
|
// length, so the failure surfaces here as the uniform verdict, never earlier.
|
|
@@ -435,7 +458,7 @@ async function _openContent(parsed, eci, cek, ct) {
|
|
|
435
458
|
}
|
|
436
459
|
var iv = asn1.read.octetString(asn1.decode(alg.parameters));
|
|
437
460
|
if (iv.length !== 16) throw _fail();
|
|
438
|
-
return pbes2.cbcDecrypt(cek, iv, eci.encryptedContent, keyBits);
|
|
461
|
+
return pbes2.cbcDecrypt(cek, iv, eci.encryptedContent, keyBits, CmsError, "cms/decrypt-failed");
|
|
439
462
|
} catch (e) {
|
|
440
463
|
if (e instanceof CmsError && e.code !== "cms/decrypt-failed") throw e;
|
|
441
464
|
throw _fail();
|
|
@@ -450,7 +473,10 @@ function _gcmOpen(cek, nonce, ct, tag, aad, keyBits, icvLen) {
|
|
|
450
473
|
var d = nodeCrypto.createDecipheriv("aes-" + keyBits + "-gcm", cek, nonce, { authTagLength: icvLen });
|
|
451
474
|
d.setAuthTag(tag);
|
|
452
475
|
if (aad && aad.length) d.setAAD(aad);
|
|
453
|
-
|
|
476
|
+
// final() throws on a tag mismatch, and update() has already produced the whole plaintext by
|
|
477
|
+
// then. RFC 5083 sec. 1 requires a receiver whose integrity check fails to DESTROY the plaintext,
|
|
478
|
+
// not merely withhold it, so the intermediate is cleared on both exits.
|
|
479
|
+
return guard.secret.cipherFinish(d, ct, CmsError, "cms/decrypt-failed", "the recovered content");
|
|
454
480
|
}
|
|
455
481
|
|
|
456
482
|
// ---- EncryptedData (sec. 8) ------------------------------------------------
|
|
@@ -465,7 +491,7 @@ async function _decryptEncryptedData(parsed, km, opts) {
|
|
|
465
491
|
var cek = guard.bytes.view(km.cek, CmsError, "cms/bad-input", "cek");
|
|
466
492
|
if (cek.length !== keyBits / 8) throw _err("cms/bad-input", "the supplied cek length does not match the content algorithm");
|
|
467
493
|
var iv = asn1.read.octetString(asn1.decode(alg.parameters));
|
|
468
|
-
try { return _originFields({ content: pbes2.cbcDecrypt(cek, iv, eci.encryptedContent, keyBits), contentType: eci.contentType, contentTypeName: oid.name(eci.contentType) || eci.contentType, recipientType: "cek", recipientIndex: -1, contentEncryptionAlgorithm: alg.name || alg.oid, authenticated: false }, null, null); }
|
|
494
|
+
try { return _originFields({ content: pbes2.cbcDecrypt(cek, iv, eci.encryptedContent, keyBits, CmsError, "cms/decrypt-failed"), contentType: eci.contentType, contentTypeName: oid.name(eci.contentType) || eci.contentType, recipientType: "cek", recipientIndex: -1, contentEncryptionAlgorithm: alg.name || alg.oid, authenticated: false }, null, null); }
|
|
469
495
|
catch (_e) { throw _fail(); }
|
|
470
496
|
}
|
|
471
497
|
async function _decryptPbes2(parsed, eci, km, opts) {
|
|
@@ -497,7 +523,7 @@ async function _decryptPbes2(parsed, eci, km, opts) {
|
|
|
497
523
|
var key;
|
|
498
524
|
try { key = nodeCrypto.pbkdf2Sync(pwE.bytes, pb.salt, pb.iterations, keyBits / 8, pb.prfNode); }
|
|
499
525
|
finally { if (pwE.owned) guard.secret.zeroize(pwE.bytes, CmsError, "cms/bad-input", "the password encoding"); }
|
|
500
|
-
try { return _originFields({ content: pbes2.cbcDecrypt(key, iv, eci.encryptedContent, keyBits), contentType: eci.contentType, contentTypeName: oid.name(eci.contentType) || eci.contentType, recipientType: "password", recipientIndex: -1, contentEncryptionAlgorithm: oid.name(encOid) || encOid, authenticated: false }, null, null); }
|
|
526
|
+
try { return _originFields({ content: pbes2.cbcDecrypt(key, iv, eci.encryptedContent, keyBits, CmsError, "cms/decrypt-failed"), contentType: eci.contentType, contentTypeName: oid.name(eci.contentType) || eci.contentType, recipientType: "password", recipientIndex: -1, contentEncryptionAlgorithm: oid.name(encOid) || encOid, authenticated: false }, null, null); }
|
|
501
527
|
catch (_e) { throw _fail(); }
|
|
502
528
|
finally { guard.secret.zeroize(key, CmsError, "cms/bad-input", "the password-derived content-encryption key"); }
|
|
503
529
|
}
|
|
@@ -548,27 +574,30 @@ function _pwriUnwrap(kek, wrapped, iv, keyBits) {
|
|
|
548
574
|
// pass2 = CBC(kek, iv2 = pass1[last block], pass1). Recover pass1's last block by decrypting the
|
|
549
575
|
// last ciphertext block (ECB) and XORing the previous ciphertext block, a standard CBC last-
|
|
550
576
|
// block decrypt that needs no IV, then CBC-decrypt pass2 under that recovered iv2 to get pass1.
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
var pass1 =
|
|
557
|
-
var d2 = nodeCrypto.createDecipheriv(alg, kek, iv); d2.setAutoPadding(false);
|
|
558
|
-
// body is the recovered plaintext block: it holds the CEK. The caller receives an independent
|
|
559
|
-
// copy, so the block is cleared on every exit, including the two validation rejects, which are
|
|
560
|
-
// the paths an attacker induces by tampering with the wrapped key.
|
|
561
|
-
var body = Buffer.concat([d2.update(pass1), d2.final()]);
|
|
577
|
+
// Every intermediate below is derived from the KEK and reveals key material, so all three are
|
|
578
|
+
// named before the try and cleared in one finally. pass1 and body hold the wrapped key in the
|
|
579
|
+
// clear; lastDec is pass1's final block, recovered ahead of it. The two validation rejects are
|
|
580
|
+
// inside the same scope because they are the paths an attacker induces by tampering with the
|
|
581
|
+
// wrapped key, and they are where an unswept intermediate would otherwise be abandoned.
|
|
582
|
+
var lastDec = null, pass1 = null, body = null;
|
|
562
583
|
try {
|
|
584
|
+
var ecb = nodeCrypto.createDecipheriv("aes-" + keyBits + "-ecb", kek, Buffer.alloc(0)); ecb.setAutoPadding(false);
|
|
585
|
+
lastDec = guard.secret.cipherFinish(ecb, wrapped.subarray(n - blk), CmsError, "cms/decrypt-failed", "the recovered CBC last block");
|
|
586
|
+
var iv2 = Buffer.alloc(blk);
|
|
587
|
+
for (var i = 0; i < blk; i++) iv2[i] = lastDec[i] ^ wrapped[n - 2 * blk + i];
|
|
588
|
+
var d1 = nodeCrypto.createDecipheriv(alg, kek, iv2); d1.setAutoPadding(false);
|
|
589
|
+
pass1 = guard.secret.cipherFinish(d1, wrapped, CmsError, "cms/decrypt-failed", "the PWRI first-pass plaintext");
|
|
590
|
+
var d2 = nodeCrypto.createDecipheriv(alg, kek, iv); d2.setAutoPadding(false);
|
|
591
|
+
body = guard.secret.cipherFinish(d2, pass1, CmsError, "cms/decrypt-failed", "the PWRI plaintext block");
|
|
563
592
|
var count = body[0];
|
|
564
593
|
if (count < 1 || count + 4 > body.length) throw _fail();
|
|
565
594
|
var cek = body.subarray(4, 4 + count);
|
|
566
595
|
var bad = 0;
|
|
567
596
|
for (var j = 0; j < 3; j++) bad |= (body[1 + j] ^ 0xff) ^ cek[j]; // complement check bytes
|
|
568
597
|
if (bad !== 0) throw _fail();
|
|
569
|
-
return Buffer.from(cek);
|
|
598
|
+
return Buffer.from(cek); // the caller's copy, independent of the block cleared below
|
|
570
599
|
} finally {
|
|
571
|
-
guard.secret.zeroizeAll([body, pass1], CmsError, "cms/bad-input", "the PWRI plaintext block");
|
|
600
|
+
guard.secret.zeroizeAll([body, pass1, lastDec], CmsError, "cms/bad-input", "the PWRI plaintext block");
|
|
572
601
|
}
|
|
573
602
|
}
|
|
574
603
|
// Coverage residual (the unsupported-algorithm throw arm of each lookup below: _hashW3c,
|
package/lib/cms-encrypt.js
CHANGED
|
@@ -351,10 +351,10 @@ function _pwriWrapIv(kek, cek, iv) {
|
|
|
351
351
|
var wk = _pwriFormat(cek);
|
|
352
352
|
try {
|
|
353
353
|
var c1 = nodeCrypto.createCipheriv("aes-256-cbc", kek, iv); c1.setAutoPadding(false);
|
|
354
|
-
var pass1 =
|
|
354
|
+
var pass1 = guard.secret.cipherFinish(c1, wk, CmsError, "cms/bad-input", "the PWRI first-pass ciphertext");
|
|
355
355
|
var iv2 = pass1.subarray(pass1.length - 16);
|
|
356
356
|
var c2 = nodeCrypto.createCipheriv("aes-256-cbc", kek, iv2); c2.setAutoPadding(false);
|
|
357
|
-
return
|
|
357
|
+
return guard.secret.cipherFinish(c2, pass1, CmsError, "cms/bad-input", "the PWRI wrapped key");
|
|
358
358
|
} finally {
|
|
359
359
|
guard.secret.zeroize(wk, CmsError, "cms/bad-input", "the PWRI plaintext block");
|
|
360
360
|
}
|
|
@@ -432,11 +432,48 @@ function _emit(inner, ctName, opts) {
|
|
|
432
432
|
|
|
433
433
|
function _envelopedData(contentBytes, cek, ca, contentType, riNodes, recips) {
|
|
434
434
|
var iv = nodeCrypto.randomBytes(16);
|
|
435
|
-
var enc = pbes2.cbcEncrypt(cek, iv, contentBytes, ca.keyBits);
|
|
435
|
+
var enc = pbes2.cbcEncrypt(cek, iv, contentBytes, ca.keyBits, CmsError, "cms/bad-input");
|
|
436
436
|
var eci = b.sequence([b.oid(O(contentType)), b.sequence([b.oid(O(ca.oid)), b.octetString(iv)]), b.contextPrimitive(0, enc)]);
|
|
437
437
|
return b.sequence([b.integer(BigInt(_envelopedVersion(recips, false))), b.setOf(riNodes), eci]);
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
+
// Every authenticated attribute this module MACs -- auto-built or caller-supplied, in either content
|
|
441
|
+
// type that carries them -- MUST be a well-formed Attribute SEQUENCE { type OBJECT IDENTIFIER,
|
|
442
|
+
// non-empty SET OF value } with no repeated type (RFC 5652 sec. 5.3). Rejecting here means a
|
|
443
|
+
// malformed attribute never reaches the wire; leaving it to the wire means the operator learns from
|
|
444
|
+
// a peer's parser that the message they built is unreadable, with the MAC already computed over it.
|
|
445
|
+
//
|
|
446
|
+
// `forbidden` names an attribute type this particular content type must not emit, with the clause
|
|
447
|
+
// that says so. It is a parameter rather than a constant because the same attribute is required in
|
|
448
|
+
// one content type and discouraged in the other: AuthenticatedData MACs its own message-digest by
|
|
449
|
+
// design (RFC 5652 sec. 9.2), while for AuthEnvelopedData that value is the unencrypted hash of a
|
|
450
|
+
// plaintext the message exists to conceal (RFC 5083 sec. 2.1 / sec. 5).
|
|
451
|
+
function _assertAuthAttrs(pairs, forbidden) {
|
|
452
|
+
var seenTypes = {};
|
|
453
|
+
pairs.forEach(function (p) {
|
|
454
|
+
var node;
|
|
455
|
+
try { node = asn1.decode(p); } catch (e) { throw _err("cms/bad-input", "an authenticated attribute is not well-formed DER", e); }
|
|
456
|
+
if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children || node.children.length !== 2 ||
|
|
457
|
+
node.children[1].tagClass !== "universal" || node.children[1].tagNumber !== asn1.TAGS.SET ||
|
|
458
|
+
!node.children[1].children || node.children[1].children.length < 1) {
|
|
459
|
+
throw _err("cms/bad-input", "an authenticated attribute must be an Attribute SEQUENCE { type, non-empty SET OF value } (RFC 5652)");
|
|
460
|
+
}
|
|
461
|
+
var t;
|
|
462
|
+
try { t = asn1.read.oid(node.children[0]); } catch (e) { throw _err("cms/bad-input", "an authenticated attribute type is not an OBJECT IDENTIFIER", e); }
|
|
463
|
+
if (guard.intrinsic.hasOwn(seenTypes, t)) throw _err("cms/bad-input", "authenticated attributes must not repeat an attribute type (RFC 5652): " + t);
|
|
464
|
+
seenTypes[t] = 1;
|
|
465
|
+
if (forbidden && t === forbidden.oid) throw _err("cms/bad-input", forbidden.why);
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// The one attribute AuthEnvelopedData declines to emit. Its value is the unencrypted one-way hash of
|
|
470
|
+
// the plaintext, so disclosing it enables content tracking and confirms a guessed plaintext against
|
|
471
|
+
// a message that was encrypted to prevent exactly that.
|
|
472
|
+
var AUTH_ENVELOPED_FORBIDDEN = {
|
|
473
|
+
oid: O("messageDigest"),
|
|
474
|
+
why: "authAttrs must not carry the message-digest attribute in an AuthEnvelopedData: its value is the unencrypted hash of the plaintext, which enables content tracking and confirmation of a guessed plaintext (RFC 5083 sec. 2.1, sec. 5)",
|
|
475
|
+
};
|
|
476
|
+
|
|
440
477
|
function _authEnvelopedData(contentBytes, cek, ca, contentType, opts, riNodes, recips) {
|
|
441
478
|
var nonce = nodeCrypto.randomBytes(12);
|
|
442
479
|
var authAttrsDer = null, aad = Buffer.alloc(0);
|
|
@@ -446,6 +483,7 @@ function _authEnvelopedData(contentBytes, cek, ca, contentType, opts, riNodes, r
|
|
|
446
483
|
throw _err("cms/bad-input", "AuthEnvelopedData with a non-data contentType requires authAttrs (RFC 5083 sec. 2.1)");
|
|
447
484
|
}
|
|
448
485
|
if (opts.authAttrs && opts.authAttrs.length) {
|
|
486
|
+
_assertAuthAttrs(opts.authAttrs, AUTH_ENVELOPED_FORBIDDEN);
|
|
449
487
|
// authAttrs are transmitted [1] IMPLICIT but MACed under the EXPLICIT SET OF tag (RFC 5083 sec. 2.2).
|
|
450
488
|
var setOf = b.setOf(opts.authAttrs);
|
|
451
489
|
aad = setOf; authAttrsDer = b.contextConstructed(1, setOf.subarray(_tlvHeaderLen(setOf)));
|
|
@@ -475,7 +513,7 @@ function _encryptedData(contentBytes, desc, ca, contentType, opts, cek) {
|
|
|
475
513
|
} else {
|
|
476
514
|
throw _err("cms/bad-input", "EncryptedData needs a single { cek } or { password } descriptor");
|
|
477
515
|
}
|
|
478
|
-
var enc = pbes2.cbcEncrypt(encKey, iv, contentBytes, ca.keyBits);
|
|
516
|
+
var enc = pbes2.cbcEncrypt(encKey, iv, contentBytes, ca.keyBits, CmsError, "cms/bad-input");
|
|
479
517
|
var eci = b.sequence([b.oid(O(contentType)), contentAlgNode, b.contextPrimitive(0, enc)]);
|
|
480
518
|
var inner = b.sequence([b.integer(0n), eci]);
|
|
481
519
|
return _emit(inner, "encryptedData", opts);
|
|
@@ -495,7 +533,7 @@ function _encryptedDataPbes2(contentBytes, desc, ca, contentType, iv, opts) {
|
|
|
495
533
|
var key = nodeCrypto.pbkdf2Sync(password, salt, iterations, ca.keyBits / 8, pbes2.prfNodeByName(prf, _err, "cms"));
|
|
496
534
|
if (pwOwn2.owned) guard.secret.zeroize(password, CmsError, "cms/bad-input", "the password encoding");
|
|
497
535
|
try {
|
|
498
|
-
var enc = pbes2.cbcEncrypt(key, iv, contentBytes, ca.keyBits);
|
|
536
|
+
var enc = pbes2.cbcEncrypt(key, iv, contentBytes, ca.keyBits, CmsError, "cms/bad-input");
|
|
499
537
|
var contentAlg = pbes2.pbes2AlgId(salt, iterations, prf, ca.oid, iv);
|
|
500
538
|
var eci = b.sequence([b.oid(O(contentType)), contentAlg, b.contextPrimitive(0, enc)]);
|
|
501
539
|
var inner = b.sequence([b.integer(0n), eci]);
|
|
@@ -509,7 +547,7 @@ function _encryptedDataPbes2(contentBytes, desc, ca, contentType, iv, opts) {
|
|
|
509
547
|
function _gcmEncrypt(key, nonce, plaintext, aad, keyBits, tagLen) {
|
|
510
548
|
var c = nodeCrypto.createCipheriv("aes-" + keyBits + "-gcm", key, nonce, { authTagLength: tagLen });
|
|
511
549
|
if (aad && aad.length) c.setAAD(aad);
|
|
512
|
-
var ct =
|
|
550
|
+
var ct = guard.secret.cipherFinish(c, plaintext, CmsError, "cms/bad-input", "a content-encryption intermediate");
|
|
513
551
|
return { ct: ct, tag: c.getAuthTag() };
|
|
514
552
|
}
|
|
515
553
|
|
|
@@ -573,24 +611,7 @@ async function authenticate(content, recipients, opts) {
|
|
|
573
611
|
b.sequence([b.oid(O("messageDigest")), b.setOf([b.octetString(mdDigest)])]),
|
|
574
612
|
];
|
|
575
613
|
if (opts.authAttrs && opts.authAttrs.length) pairs = pairs.concat(opts.authAttrs);
|
|
576
|
-
|
|
577
|
-
// { type OBJECT IDENTIFIER, values SET OF } and each type appears at most once (RFC 5652), so a
|
|
578
|
-
// malformed or duplicate caller attribute is rejected before it is MACed and emitted, never left to
|
|
579
|
-
// fail an operator's parser downstream.
|
|
580
|
-
var seenTypes = {};
|
|
581
|
-
pairs.forEach(function (p) {
|
|
582
|
-
var node;
|
|
583
|
-
try { node = asn1.decode(p); } catch (e) { throw _err("cms/bad-input", "an authenticated attribute is not well-formed DER", e); }
|
|
584
|
-
if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children || node.children.length !== 2 ||
|
|
585
|
-
node.children[1].tagClass !== "universal" || node.children[1].tagNumber !== asn1.TAGS.SET ||
|
|
586
|
-
!node.children[1].children || node.children[1].children.length < 1) {
|
|
587
|
-
throw _err("cms/bad-input", "an authenticated attribute must be an Attribute SEQUENCE { type, non-empty SET OF value } (RFC 5652)");
|
|
588
|
-
}
|
|
589
|
-
var t;
|
|
590
|
-
try { t = asn1.read.oid(node.children[0]); } catch (e) { throw _err("cms/bad-input", "an authenticated attribute type is not an OBJECT IDENTIFIER", e); }
|
|
591
|
-
if (seenTypes[t]) throw _err("cms/bad-input", "authenticated attributes must not repeat an attribute type (RFC 5652): " + t);
|
|
592
|
-
seenTypes[t] = 1;
|
|
593
|
-
});
|
|
614
|
+
_assertAuthAttrs(pairs, null);
|
|
594
615
|
var setOf = b.setOf(pairs);
|
|
595
616
|
preimage = setOf; // MAC over the 0x31 SET OF
|
|
596
617
|
authAttrsDer = b.contextConstructed(2, setOf.subarray(_tlvHeaderLen(setOf))); // [2] IMPLICIT on the wire
|
package/lib/cms-sign.js
CHANGED
|
@@ -109,7 +109,7 @@ function _buildSid(cert, useSki) {
|
|
|
109
109
|
function _buildSignedAttrs(pairs) {
|
|
110
110
|
var seenTypes = {};
|
|
111
111
|
var attrs = pairs.map(function (p) {
|
|
112
|
-
if (seenTypes
|
|
112
|
+
if (guard.intrinsic.hasOwn(seenTypes, p.type)) throw _err("cms/bad-input", "signedAttrs must not repeat an attribute type (RFC 5652 sec. 5.3): " + p.type);
|
|
113
113
|
seenTypes[p.type] = 1;
|
|
114
114
|
return b.sequence([b.oid(p.type), b.set(p.values)]);
|
|
115
115
|
});
|
|
@@ -147,8 +147,8 @@ function _buildUnsignedAttrs(list) {
|
|
|
147
147
|
var pairs = _resolveAttrPairs(list, "an unsigned attribute value");
|
|
148
148
|
var seen = {};
|
|
149
149
|
pairs.forEach(function (p) {
|
|
150
|
-
if (UNSIGNED_FORBIDDEN
|
|
151
|
-
if (seen
|
|
150
|
+
if (guard.intrinsic.hasOwn(UNSIGNED_FORBIDDEN, p.type)) throw _err("cms/bad-input", "the " + UNSIGNED_FORBIDDEN[p.type] + " attribute must not appear as an unsigned attribute (RFC 5652 sec. 11)");
|
|
151
|
+
if (guard.intrinsic.hasOwn(seen, p.type)) throw _err("cms/bad-input", "unsignedAttrs must not repeat an attribute type (RFC 5652 sec. 5.3): " + p.type);
|
|
152
152
|
seen[p.type] = 1;
|
|
153
153
|
});
|
|
154
154
|
var setOf = b.set(pairs.map(function (p) { return b.sequence([b.oid(p.type), b.set(p.values)]); }));
|
package/lib/cms-verify.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @module pki.cms
|
|
6
6
|
* @nav Verification
|
|
7
7
|
* @title CMS
|
|
8
|
+
* @fullname CMS (Cryptographic Message Syntax) signing and verification
|
|
8
9
|
* @intro Verify a CMS SignedData signature (RFC 5652 sec. 5) -- the structure S/MIME signed
|
|
9
10
|
* mail, RFC 3161 timestamp tokens, and code-signing all rest on. `verify(cms, opts)` parses
|
|
10
11
|
* the SignedData (over the strict `pki.schema.cms` codec), locates each SignerInfo's signer
|
|
@@ -1118,7 +1119,10 @@ var countersign = cmsSign.countersign;
|
|
|
1118
1119
|
* @opts oaepHash The RSAES-OAEP hash for ktri recipients: `"sha256"` (default) / `"sha384"` / `"sha512"`.
|
|
1119
1120
|
* @opts keyIdentifier The recipient identifier form: `"issuerAndSerial"` (default) or `"subjectKeyIdentifier"`.
|
|
1120
1121
|
* @opts ukm User keying material (a Buffer) for kari / kemri recipients.
|
|
1121
|
-
* @opts authAttrs Authenticated attributes (SET OF Attribute) for AuthEnvelopedData.
|
|
1122
|
+
* @opts authAttrs Authenticated attributes (SET OF Attribute) for AuthEnvelopedData. Each must be a
|
|
1123
|
+
* well-formed `Attribute SEQUENCE { type, non-empty SET OF value }` and no type may repeat (RFC 5652
|
|
1124
|
+
* sec. 5.3). `message-digest` is refused: its value is the unencrypted hash of the plaintext, which
|
|
1125
|
+
* enables content tracking and confirms a guessed plaintext (RFC 5083 sec. 2.1, sec. 5).
|
|
1122
1126
|
* @opts pem Return a PEM string instead of a DER Buffer.
|
|
1123
1127
|
* @example
|
|
1124
1128
|
* var rsa = { name: "RSA-OAEP", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" };
|
package/lib/constants.js
CHANGED
package/lib/crl-sign.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* @module pki.crl
|
|
7
7
|
* @nav Signing
|
|
8
8
|
* @title CRLs
|
|
9
|
+
* @fullname CRLs (certificate revocation lists): read, sign and shard
|
|
9
10
|
* @intro The X.509 CRL producing side. `pki.crl.sign` builds a `TBSCertList`, signs it, and emits a
|
|
10
11
|
* `CertificateList` (RFC 5280 sec. 5) that `pki.schema.crl.parse`, `pki.path.crlChecker`, and OpenSSL
|
|
11
12
|
* all accept -- over any signature algorithm the toolkit registry resolves: RSA (PKCS#1 v1.5 / PSS),
|
package/lib/crmf-sign.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* @module pki.crmf
|
|
7
7
|
* @nav Signing
|
|
8
8
|
* @title Certificate request messages
|
|
9
|
+
* @fullname CRMF (Certificate Request Message Format, RFC 4211)
|
|
9
10
|
* @intro The RFC 4211 certificate-request-message producing side. `pki.crmf.build` assembles a
|
|
10
11
|
* `CertReqMessages` -- one or more `CertReqMsg`, each a `CertRequest` (a `CertTemplate` of the requested
|
|
11
12
|
* certificate fields plus optional controls) paired with a proof of possession. The common proof is a
|
package/lib/csr-sign.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* @module pki.csr
|
|
7
7
|
* @nav Signing
|
|
8
8
|
* @title Certification requests
|
|
9
|
+
* @fullname CSRs (PKCS#10 certification requests): build, sign and verify
|
|
9
10
|
* @intro The PKCS#10 certification-request producing side. `pki.csr.sign` builds a
|
|
10
11
|
* `CertificationRequestInfo`, signs it with the subject's own private key (proof of possession, since
|
|
11
12
|
* a CSR has no issuer), and emits a `CertificationRequest` (RFC 2986) that `pki.schema.csr.parse`,
|
package/lib/ct.js
CHANGED
package/lib/est.js
CHANGED
package/lib/framework-error.js
CHANGED
package/lib/guard-secret.js
CHANGED
|
@@ -38,6 +38,10 @@ var intrinsic = require("./guard-intrinsic");
|
|
|
38
38
|
// wiping means; reading `call` off the captured function would let them decide it again, since
|
|
39
39
|
// that function is still the one sitting on the prototype (see guard-intrinsic).
|
|
40
40
|
var _fill = intrinsic.uncurry(Uint8Array.prototype.fill);
|
|
41
|
+
// The join that produces the buffer cipherFinish hands back. Captured for the same reason as
|
|
42
|
+
// the fill: a replaced `Buffer.concat` decides what the caller receives, and it would receive it
|
|
43
|
+
// while the two intermediates this module is responsible for clearing were cleared as promised.
|
|
44
|
+
var _concat = intrinsic.bufferConcat;
|
|
41
45
|
|
|
42
46
|
// zeroize(value, ErrorClass, code, label) -> the same object, cleared.
|
|
43
47
|
// value : a Buffer / TypedArray the TOOLKIT allocated, or null / undefined
|
|
@@ -94,4 +98,44 @@ function zeroizeAll(list, ErrorClass, code, label) {
|
|
|
94
98
|
return list;
|
|
95
99
|
}
|
|
96
100
|
|
|
97
|
-
|
|
101
|
+
// cipherFinish(transform, input, ErrorClass, code, label) -> the output, as one fresh buffer
|
|
102
|
+
// transform : a node:crypto Cipher or Decipher the CALLER built and configured (key, iv, AAD,
|
|
103
|
+
// auth tag, padding mode). This guard performs the two calls and nothing else.
|
|
104
|
+
// input : the bytes to feed it.
|
|
105
|
+
//
|
|
106
|
+
// The single place this toolkit runs a configured cipher to completion, because
|
|
107
|
+
// `Buffer.concat([c.update(x), c.final()])` abandons a copy on BOTH of its exits and the leak is
|
|
108
|
+
// invisible at the call site.
|
|
109
|
+
//
|
|
110
|
+
// On a DECIPHER the abandoned copy is plaintext. `update` returns the recovered plaintext while
|
|
111
|
+
// `final` is what decides whether the message was authentic at all -- a GCM tag mismatch, a CBC
|
|
112
|
+
// padding failure -- so on a forged message the whole plaintext exists before anything has judged
|
|
113
|
+
// it, and the `concat` never runs: that buffer becomes unreferenced garbage holding readable
|
|
114
|
+
// plaintext. RFC 5083 sec. 1 requires a receiver whose integrity check fails to destroy the
|
|
115
|
+
// plaintext, and withholding it from the caller is not destroying it. On the success path `concat`
|
|
116
|
+
// COPIES, so the update buffer is a second full copy the caller never receives and nothing else
|
|
117
|
+
// will ever clear.
|
|
118
|
+
//
|
|
119
|
+
// It covers the ENCRYPT direction too, where the intermediates are ciphertext and clearing them
|
|
120
|
+
// buys little. That is deliberate: any rule for which of the two directions must clear is a
|
|
121
|
+
// judgment made at each call site, and the sites where it is wrong are the ones nobody re-reads.
|
|
122
|
+
// One shape, cleared everywhere, needs no such judgment -- and the encrypt intermediates of a
|
|
123
|
+
// key-wrap are not obviously worthless to an attacker either.
|
|
124
|
+
//
|
|
125
|
+
// @enforced-by guard-shape-reinlined
|
|
126
|
+
// @guard-shape \.update\s*\([\s\S]{0,300}?\)\s*,\s*[A-Za-z_$][\w$]*\s*\.\s*final\s*\(\s*\)
|
|
127
|
+
function cipherFinish(transform, input, ErrorClass, code, label) {
|
|
128
|
+
var head = null, tail = null;
|
|
129
|
+
try {
|
|
130
|
+
head = transform.update(input);
|
|
131
|
+
tail = transform.final();
|
|
132
|
+
// Always a fresh copy, never `head` itself: the returned buffer belongs to the caller and the
|
|
133
|
+
// two intermediates have to be clearable independently of it.
|
|
134
|
+
return _concat([head, tail]);
|
|
135
|
+
} finally {
|
|
136
|
+
zeroize(head, ErrorClass, code, label);
|
|
137
|
+
zeroize(tail, ErrorClass, code, label);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = { zeroize: zeroize, zeroizeAll: zeroizeAll, cipherFinish: cipherFinish };
|
package/lib/hpke.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @module pki.hpke
|
|
6
6
|
* @nav Protocols
|
|
7
7
|
* @title HPKE
|
|
8
|
+
* @fullname HPKE (Hybrid Public Key Encryption, RFC 9180)
|
|
8
9
|
* @intro Hybrid Public Key Encryption (RFC 9180) -- the standard construction
|
|
9
10
|
* behind TLS Encrypted Client Hello, MLS, and OHTTP that turns a recipient KEM
|
|
10
11
|
* public key into an encapsulated key plus an AEAD-encrypting context. This is
|
|
@@ -319,7 +320,7 @@ Context.prototype.seal = function (aad, pt) {
|
|
|
319
320
|
var nonce = this._nonce();
|
|
320
321
|
var c = nodeCrypto.createCipheriv(aead.cipher, this._key, nonce, { authTagLength: aead.Nt });
|
|
321
322
|
if (aad && aad.length) c.setAAD(aad);
|
|
322
|
-
var body =
|
|
323
|
+
var body = guard.secret.cipherFinish(c, _buf(pt), HpkeError, "hpke/bad-input", "a seal intermediate");
|
|
323
324
|
var ct = concat([body, c.getAuthTag()]);
|
|
324
325
|
this._inc();
|
|
325
326
|
return ct;
|
|
@@ -335,7 +336,10 @@ Context.prototype.open = function (aad, ct) {
|
|
|
335
336
|
if (aad && aad.length) d.setAAD(aad);
|
|
336
337
|
d.setAuthTag(ct.subarray(ct.length - aead.Nt));
|
|
337
338
|
var pt;
|
|
338
|
-
|
|
339
|
+
// Through the shared guard so the recovered plaintext is destroyed when the AEAD tag does not
|
|
340
|
+
// check out (RFC 9180 sec. 5.2 gives open one outcome on failure, and it is not a copy left in
|
|
341
|
+
// the heap), and so the success path's abandoned intermediate is cleared too.
|
|
342
|
+
try { pt = guard.secret.cipherFinish(d, ct.subarray(0, ct.length - aead.Nt), HpkeError, "hpke/bad-input", "the recovered plaintext"); }
|
|
339
343
|
catch (e) { throw _err("hpke/open-failed", "AEAD authentication failed (RFC 9180 sec. 5.2)", e); }
|
|
340
344
|
this._inc();
|
|
341
345
|
return pt;
|
package/lib/http-transport.js
CHANGED
package/lib/inspect.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @module pki.inspect
|
|
6
6
|
* @nav Tooling
|
|
7
7
|
* @title Inspect
|
|
8
|
+
* @fullname Inspect: render any PKI structure as readable text
|
|
8
9
|
* @intro Human-readable inspection of a parsed certificate: the pure-JS
|
|
9
10
|
* equivalent of `openssl x509 -text`. `certificate(input)` ingests a PEM string,
|
|
10
11
|
* a DER Buffer, or an already-parsed certificate and returns a familiar
|
package/lib/jose.js
CHANGED
package/lib/key.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @module pki.key
|
|
6
6
|
* @nav Signing
|
|
7
7
|
* @title Keys
|
|
8
|
+
* @fullname Key generation, import and export (RSA, EC, EdDSA, ML-DSA)
|
|
8
9
|
* @intro The key-material lifecycle: export / import a private key as PKCS#8 (`OneAsymmetricKey`,
|
|
9
10
|
* RFC 5958) or a public key as SPKI (RFC 5280 sec. 4.1.2.7), encrypt / decrypt a private key under
|
|
10
11
|
* RFC 8018 PBES2 (`EncryptedPrivateKeyInfo`, PBKDF2 + AES-CBC-Pad), and `generate` /
|
|
@@ -138,7 +139,7 @@ async function encrypt(privateKey, password, opts) {
|
|
|
138
139
|
try { dk = nodeCrypto.pbkdf2Sync(pwK.bytes, salt, iterations, keyBits / 8, prfNode); }
|
|
139
140
|
finally { if (pwK.owned) guard.secret.zeroize(pwK.bytes, KeyError, "key/bad-input", "the password encoding"); }
|
|
140
141
|
try {
|
|
141
|
-
var ciphertext = pbes2.cbcEncrypt(dk, iv, der, keyBits);
|
|
142
|
+
var ciphertext = pbes2.cbcEncrypt(dk, iv, der, keyBits, KeyError, "key/bad-input");
|
|
142
143
|
var epki = b.sequence([pbes2.pbes2AlgId(salt, iterations, prf, cipherName, iv), b.octetString(ciphertext)]);
|
|
143
144
|
pkcs8.parseEncrypted(epki); // self-check: the produced EncryptedPrivateKeyInfo re-parses
|
|
144
145
|
return opts.pem ? pkcs8.pemEncode(epki, "ENCRYPTED PRIVATE KEY") : epki;
|
package/lib/lint.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @module pki.lint
|
|
6
6
|
* @nav Tooling
|
|
7
7
|
* @title Lint
|
|
8
|
+
* @fullname Certificate linting against CA/Browser Forum requirements
|
|
8
9
|
* @intro The certificate LINTING engine -- the zlint / pkilint of JavaScript. It walks an
|
|
9
10
|
* ALREADY-PARSED certificate (from `pki.schema.x509.parse`, whose extension values it
|
|
10
11
|
* decodes with the shared RFC 5280 decoders) and emits graded, advisory FINDINGS: each
|
package/lib/merkle.js
CHANGED
package/lib/ocsp.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @module pki.ocsp
|
|
6
6
|
* @nav Revocation
|
|
7
7
|
* @title OCSP
|
|
8
|
+
* @fullname OCSP (Online Certificate Status Protocol) revocation checking
|
|
8
9
|
* @intro The producing + client-facing half of RFC 6960 Online Certificate Status Protocol:
|
|
9
10
|
* build a status request over a certificate (`pki.ocsp.buildRequest`), build and sign a status
|
|
10
11
|
* response as an authorized responder (`pki.ocsp.sign`), emit an unsigned error response
|
package/lib/oid.js
CHANGED
package/lib/path-validate.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* @module pki.path
|
|
7
7
|
* @nav Path validation
|
|
8
8
|
* @title Certification path validation (RFC 5280 6)
|
|
9
|
+
* @fullname Certificate chain validation (RFC 5280 sec. 6 path validation)
|
|
9
10
|
* @intro
|
|
10
11
|
* RFC 5280 6 certification-path validation as a pure, re-entrant algorithm
|
|
11
12
|
* over already-parsed certificates. `pki.path.validate(path, opts)` runs the
|
package/lib/pbes2.js
CHANGED
|
@@ -133,13 +133,21 @@ function parsePbkdf2Params(paramsDer, opts, E, prefix, strictPrf) {
|
|
|
133
133
|
|
|
134
134
|
// AES-CBC content encryption / decryption (PKCS#7 pad). decrypt's final() throws on bad pad, and the caller
|
|
135
135
|
// collapses that into its uniform decrypt-failed verdict.
|
|
136
|
-
|
|
136
|
+
// cbcEncrypt(key, iv, plaintext, keyBits, E, code) -> the ciphertext.
|
|
137
|
+
// Takes the caller's typed error for the same reason cbcDecrypt does: the cipher runs through the
|
|
138
|
+
// shared guard, whose one throw has to keep the calling domain's own code.
|
|
139
|
+
function cbcEncrypt(key, iv, plaintext, keyBits, E, code) {
|
|
137
140
|
var c = nodeCrypto.createCipheriv("aes-" + keyBits + "-cbc", key, iv);
|
|
138
|
-
return
|
|
141
|
+
return guard.secret.cipherFinish(c, plaintext, E, code, "a content-encryption intermediate");
|
|
139
142
|
}
|
|
140
|
-
|
|
143
|
+
// cbcDecrypt(key, iv, ct, keyBits, E, code) -> the recovered plaintext.
|
|
144
|
+
// Takes the caller's typed error the way rc2.cbcDecrypt does, so a refusal keeps its own domain.
|
|
145
|
+
function cbcDecrypt(key, iv, ct, keyBits, E, code) {
|
|
141
146
|
var d = nodeCrypto.createDecipheriv("aes-" + keyBits + "-cbc", key, iv);
|
|
142
|
-
|
|
147
|
+
// A wrong key fails inside final(), on the padding check, and update() has by then produced every
|
|
148
|
+
// block but the last. Through the guard so that partial plaintext is cleared on both exits rather
|
|
149
|
+
// than abandoned readable on exactly the path an attacker induces.
|
|
150
|
+
return guard.secret.cipherFinish(d, ct, E, code, "the partially recovered plaintext");
|
|
143
151
|
}
|
|
144
152
|
|
|
145
153
|
// Higher-level PBES2 AES-CBC encrypt: derive an AES key from PRE-FORMATTED password bytes (the caller owns
|
|
@@ -160,11 +168,11 @@ function pbes2Encrypt(pwBytes, plaintext, opts, E, prefix) {
|
|
|
160
168
|
// is not cleared: the caller formatted it and may still own the buffer.
|
|
161
169
|
var key = nodeCrypto.pbkdf2Sync(pwBytes, salt, iterations, keyBits / 8, prfNode);
|
|
162
170
|
try {
|
|
163
|
-
return { algId: pbes2AlgId(salt, iterations, prf, cipherName, iv), ct: cbcEncrypt(key, iv, plaintext, keyBits) };
|
|
171
|
+
return { algId: pbes2AlgId(salt, iterations, prf, cipherName, iv), ct: cbcEncrypt(key, iv, plaintext, keyBits, E, prefix + "/bad-input") };
|
|
164
172
|
} finally {
|
|
165
173
|
// E is the caller's error FACTORY, not a class; guard-secret delegates its only throw to
|
|
166
|
-
// guard-bytes, which constructs.
|
|
167
|
-
// currency is compatible -- and the throw is unreachable here regardless, because a freshly
|
|
174
|
+
// guard-bytes, which constructs. `new` on a factory yields the object the factory returns, so
|
|
175
|
+
// the currency is compatible -- and the throw is unreachable here regardless, because a freshly
|
|
168
176
|
// allocated pbkdf2 buffer cannot be detached.
|
|
169
177
|
guard.secret.zeroize(key, E, prefix + "/bad-input", "the password-derived encryption key");
|
|
170
178
|
}
|
|
@@ -208,7 +216,7 @@ function pbes2Decrypt(pwBytes, params, ciphertext, opts, E, prefix, budget) {
|
|
|
208
216
|
if (budget.rounds < 0) throw E(prefix + "/iteration-limit", "the aggregate PBKDF2 key-derivation work exceeds the budget (a hostile many-element input)");
|
|
209
217
|
}
|
|
210
218
|
var dk = nodeCrypto.pbkdf2Sync(pwBytes, pb.salt, pb.iterations, keyBits / 8, pb.prfNode);
|
|
211
|
-
try { return cbcDecrypt(dk, iv, ciphertext, keyBits); }
|
|
219
|
+
try { return cbcDecrypt(dk, iv, ciphertext, keyBits, E, prefix + "/decrypt-failed"); }
|
|
212
220
|
catch (_e) { throw E(prefix + "/decrypt-failed", "decryption failed"); }
|
|
213
221
|
finally { guard.secret.zeroize(dk, E, prefix + "/bad-input", "the password-derived decryption key"); }
|
|
214
222
|
}
|
package/lib/pkcs12-build.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @module pki.pkcs12
|
|
6
6
|
* @nav Signing
|
|
7
7
|
* @title PKCS#12
|
|
8
|
+
* @fullname PKCS#12 keystores (.p12 and .pfx): build and open
|
|
8
9
|
* @intro The PKCS#12 (.p12 / .pfx) producing side (RFC 7292, RFC 9579). `pki.pkcs12.build` assembles a
|
|
9
10
|
* password-integrity PFX -- key, certificate, CRL, and secret bags (shrouded keys and cert safes encrypted
|
|
10
11
|
* under RFC 8018 PBES2) wrapped in an AuthenticatedSafe, protected by either a classic Appendix B HMAC or
|
|
@@ -848,7 +849,7 @@ function _decryptLegacyPbe(ea, ct, password, opts, budget) {
|
|
|
848
849
|
try {
|
|
849
850
|
if (scheme.rc2) return rc2.cbcDecrypt(keyM, scheme.rc2, iv, ct, _err, "pkcs12/decrypt-failed");
|
|
850
851
|
var d = nodeCrypto.createDecipheriv(scheme.cipher, keyM, iv);
|
|
851
|
-
return
|
|
852
|
+
return guard.secret.cipherFinish(d, ct, Pkcs12Error, "pkcs12/bad-input", "the recovered safe contents");
|
|
852
853
|
} catch (_e) { throw _err("pkcs12/decrypt-failed", "decryption failed"); }
|
|
853
854
|
finally { guard.secret.zeroize(keyM, Pkcs12Error, "pkcs12/bad-input", "the password-derived decryption key"); }
|
|
854
855
|
}
|
package/lib/schema-all.js
CHANGED
package/lib/schema-c509.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @module pki.schema.c509
|
|
6
6
|
* @nav Schema
|
|
7
7
|
* @title C509
|
|
8
|
+
* @fullname C509 compact certificates (CBOR-encoded X.509)
|
|
8
9
|
* @intro C509 CBOR-encoded certificates (draft-ietf-cose-cbor-encoded-cert). A compact CBOR
|
|
9
10
|
* re-encoding of an X.509 v3 certificate: a deterministic-CBOR array of exactly 11 elements
|
|
10
11
|
* (10 TBS fields + the issuer signature). Two modes: c509CertificateType 2 is a natively-signed
|
package/lib/schema-csrattrs.js
CHANGED
package/lib/schema-engine.js
CHANGED
package/lib/schema-pkcs8.js
CHANGED
package/lib/schema-smime.js
CHANGED
package/lib/schema-tsp.js
CHANGED
package/lib/schema-x509.js
CHANGED
package/lib/shbs.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @module pki.shbs
|
|
6
6
|
* @nav Signatures
|
|
7
7
|
* @title Stateful hash-based
|
|
8
|
+
* @fullname Stateful hash-based signatures: LMS and XMSS
|
|
8
9
|
* @intro Stateful hash-based signature VERIFICATION -- HSS/LMS (RFC 8554),
|
|
9
10
|
* carried in X.509 by RFC 9802 and in CMS by RFC 9708, profiled by NIST
|
|
10
11
|
* SP 800-208. Verify only, by deliberate design: stateful hash-based signing
|
package/lib/sigstore.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @module pki.sigstore
|
|
6
6
|
* @nav Supply chain
|
|
7
7
|
* @title Sigstore
|
|
8
|
+
* @fullname Sigstore bundles: keyless signing and provenance
|
|
8
9
|
* @intro Zero-dependency verifier for a Sigstore bundle, the exact artifact
|
|
9
10
|
* `npm publish --provenance` produces and the npm registry serves at its
|
|
10
11
|
* attestations API. A bundle is a keyless (Fulcio) signature over a DSSE-wrapped
|
package/lib/smime.js
CHANGED
package/lib/tls-cert-compress.js
CHANGED
package/lib/trust.js
CHANGED
package/lib/tsp-sign.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @module pki.tsp
|
|
6
6
|
* @nav Signing
|
|
7
7
|
* @title Timestamps
|
|
8
|
+
* @fullname RFC 3161 trusted timestamps: request, sign and verify
|
|
8
9
|
* @intro Create an RFC 3161 timestamp token. A TimeStampToken IS a CMS SignedData whose
|
|
9
10
|
* encapsulated content is a `TSTInfo` (the timestamped message imprint + trusted time), so
|
|
10
11
|
* `sign(messageImprint, tsa, opts)` builds the `TSTInfo`, attaches the RFC 3161 sec. 2.4.2
|
package/lib/webauthn.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @module pki.webauthn
|
|
6
6
|
* @nav Validation
|
|
7
7
|
* @title WebAuthn
|
|
8
|
+
* @fullname WebAuthn: verify registration attestation and assertions
|
|
8
9
|
* @intro Trust evaluation of a W3C WebAuthn (Level 3) / passkey attestation: parse
|
|
9
10
|
* the attestation object + authenticatorData, decode the COSE credential public
|
|
10
11
|
* key, and verify each defined attestation-statement format (packed, tpm,
|
package/lib/webcrypto.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @module pki.webcrypto
|
|
6
6
|
* @nav Core
|
|
7
7
|
* @title WebCrypto
|
|
8
|
+
* @fullname WebCrypto: a SubtleCrypto surface over node:crypto
|
|
8
9
|
* @order 50
|
|
9
10
|
* @featured true
|
|
10
11
|
* @slug webcrypto
|
|
@@ -470,7 +471,7 @@ SubtleCrypto.prototype.encrypt = async function encrypt(algorithm, key, data) {
|
|
|
470
471
|
return _withSecretBytes(key, function (kb) {
|
|
471
472
|
var cipher = nodeCrypto.createCipheriv("aes-" + key.algorithm.length + "-gcm", kb, iv, { authTagLength: (alg.tagLength || 128) / 8 });
|
|
472
473
|
if (aad) cipher.setAAD(aad);
|
|
473
|
-
var ct =
|
|
474
|
+
var ct = guard.secret.cipherFinish(cipher, buf, WebCryptoError, "webcrypto/operation", "an AES-GCM encrypt intermediate");
|
|
474
475
|
return Buffer.concat([ct, cipher.getAuthTag()]);
|
|
475
476
|
});
|
|
476
477
|
}, "encrypt"));
|
|
@@ -480,7 +481,7 @@ SubtleCrypto.prototype.encrypt = async function encrypt(algorithm, key, data) {
|
|
|
480
481
|
return _toArrayBuffer(_runCipher(function () {
|
|
481
482
|
return _withSecretBytes(key, function (kb) {
|
|
482
483
|
var c2 = nodeCrypto.createCipheriv("aes-" + key.algorithm.length + "-cbc", kb, cbcIv);
|
|
483
|
-
return
|
|
484
|
+
return guard.secret.cipherFinish(c2, buf, WebCryptoError, "webcrypto/operation", "an AES-CBC encrypt intermediate");
|
|
484
485
|
});
|
|
485
486
|
}, "encrypt"));
|
|
486
487
|
}
|
|
@@ -490,7 +491,7 @@ SubtleCrypto.prototype.encrypt = async function encrypt(algorithm, key, data) {
|
|
|
490
491
|
return _toArrayBuffer(_runCipher(function () {
|
|
491
492
|
return _withSecretBytes(key, function (kb) {
|
|
492
493
|
var c3 = nodeCrypto.createCipheriv("aes-" + key.algorithm.length + "-ctr", kb, ctrCounter);
|
|
493
|
-
return
|
|
494
|
+
return guard.secret.cipherFinish(c3, buf, WebCryptoError, "webcrypto/operation", "an AES-CTR encrypt intermediate");
|
|
494
495
|
});
|
|
495
496
|
}, "encrypt"));
|
|
496
497
|
}
|
|
@@ -528,7 +529,9 @@ SubtleCrypto.prototype.decrypt = async function decrypt(algorithm, key, data) {
|
|
|
528
529
|
var d = nodeCrypto.createDecipheriv("aes-" + key.algorithm.length + "-gcm", kb, iv, { authTagLength: tagLen });
|
|
529
530
|
if (gcmAad) d.setAAD(gcmAad);
|
|
530
531
|
d.setAuthTag(tag);
|
|
531
|
-
|
|
532
|
+
// The recovered plaintext exists in full before final() judges the tag, so a forged message
|
|
533
|
+
// would otherwise abandon a readable copy on the throw path.
|
|
534
|
+
return guard.secret.cipherFinish(d, ct, WebCryptoError, "webcrypto/operation", "the recovered AES-GCM plaintext");
|
|
532
535
|
});
|
|
533
536
|
}, "decrypt"));
|
|
534
537
|
}
|
|
@@ -537,7 +540,7 @@ SubtleCrypto.prototype.decrypt = async function decrypt(algorithm, key, data) {
|
|
|
537
540
|
return _toArrayBuffer(_runCipher(function () {
|
|
538
541
|
return _withSecretBytes(key, function (kb) {
|
|
539
542
|
var d2 = nodeCrypto.createDecipheriv("aes-" + key.algorithm.length + "-cbc", kb, cbcIv2);
|
|
540
|
-
return
|
|
543
|
+
return guard.secret.cipherFinish(d2, buf, WebCryptoError, "webcrypto/operation", "the recovered AES-CBC plaintext");
|
|
541
544
|
});
|
|
542
545
|
}, "decrypt"));
|
|
543
546
|
}
|
|
@@ -547,7 +550,7 @@ SubtleCrypto.prototype.decrypt = async function decrypt(algorithm, key, data) {
|
|
|
547
550
|
return _toArrayBuffer(_runCipher(function () {
|
|
548
551
|
return _withSecretBytes(key, function (kb) {
|
|
549
552
|
var d3 = nodeCrypto.createDecipheriv("aes-" + key.algorithm.length + "-ctr", kb, ctrCounter2);
|
|
550
|
-
return
|
|
553
|
+
return guard.secret.cipherFinish(d3, buf, WebCryptoError, "webcrypto/operation", "the recovered AES-CTR plaintext");
|
|
551
554
|
});
|
|
552
555
|
}, "decrypt"));
|
|
553
556
|
}
|
|
@@ -874,7 +877,7 @@ SubtleCrypto.prototype.wrapKey = async function wrapKey(format, key, wrappingKey
|
|
|
874
877
|
try {
|
|
875
878
|
return _withSecretBytes(wrappingKey, function (wkBytes) {
|
|
876
879
|
var c = nodeCrypto.createCipheriv("aes" + wrappingKey.algorithm.length + "-wrap", wkBytes, Buffer.from("A6A6A6A6A6A6A6A6", "hex"));
|
|
877
|
-
return _toArrayBuffer(
|
|
880
|
+
return _toArrayBuffer(guard.secret.cipherFinish(c, bytes, WebCryptoError, "webcrypto/operation", "an AES-KW wrap intermediate"));
|
|
878
881
|
});
|
|
879
882
|
} catch (e) { throw new WebCryptoError("webcrypto/operation", "wrapKey: AES-KW key wrap failed", e); }
|
|
880
883
|
}
|
|
@@ -919,7 +922,7 @@ SubtleCrypto.prototype.unwrapKey = async function unwrapKey(format, wrappedKey,
|
|
|
919
922
|
try {
|
|
920
923
|
bytes = _withSecretBytes(unwrappingKey, function (kwBytes) {
|
|
921
924
|
var d = nodeCrypto.createDecipheriv("aes" + unwrappingKey.algorithm.length + "-wrap", kwBytes, Buffer.from("A6A6A6A6A6A6A6A6", "hex"));
|
|
922
|
-
return
|
|
925
|
+
return guard.secret.cipherFinish(d, wrapped, WebCryptoError, "webcrypto/operation", "the unwrapped key material");
|
|
923
926
|
});
|
|
924
927
|
} catch (e) { throw new WebCryptoError("webcrypto/operation", "unwrapKey: AES-KW key unwrap failed (integrity or length)", e); }
|
|
925
928
|
} else {
|
package/lib/x509-sign.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* @module pki.x509
|
|
7
7
|
* @nav Signing
|
|
8
8
|
* @title Certificates
|
|
9
|
+
* @fullname X.509 certificates: parse, sign and inspect
|
|
9
10
|
* @intro The X.509 certificate-issuance producing side. `pki.x509.sign` builds a `TBSCertificate`,
|
|
10
11
|
* signs it, and emits a `Certificate` (RFC 5280 sec. 4) that `pki.schema.x509.parse`,
|
|
11
12
|
* `pki.path.validate`, and OpenSSL all accept, self-signed or CA-signed, over any signature
|
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:b8b3d2cf-001e-4a5b-8644-a627ed3ce3c5",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-08-
|
|
8
|
+
"timestamp": "2026-08-20T14:50:41.972Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.5.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.5.17",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.5.
|
|
25
|
+
"version": "0.5.17",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/pki@0.5.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.5.17",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/pki@0.5.
|
|
57
|
+
"ref": "@blamejs/pki@0.5.17",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|