@blamejs/pki 0.5.16 → 0.5.18

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +57 -1
  2. package/lib/acme.js +7 -4
  3. package/lib/asn1-der.js +1 -0
  4. package/lib/attrcert-sign.js +1 -0
  5. package/lib/cbor-det.js +1 -0
  6. package/lib/cmc-verify.js +1 -0
  7. package/lib/cmp-build.js +16 -1
  8. package/lib/cms-decrypt.js +58 -29
  9. package/lib/cms-encrypt.js +200 -38
  10. package/lib/cms-sign.js +18 -4
  11. package/lib/cms-verify.js +5 -1
  12. package/lib/constants.js +1 -0
  13. package/lib/crl-sign.js +62 -2
  14. package/lib/crmf-sign.js +1 -0
  15. package/lib/csr-sign.js +1 -0
  16. package/lib/ct.js +1 -0
  17. package/lib/est.js +1 -0
  18. package/lib/framework-error.js +1 -0
  19. package/lib/guard-secret.js +45 -1
  20. package/lib/hpke.js +6 -2
  21. package/lib/http-transport.js +1 -0
  22. package/lib/inspect.js +1 -0
  23. package/lib/jose.js +31 -3
  24. package/lib/key.js +2 -1
  25. package/lib/lint.js +1 -0
  26. package/lib/merkle.js +1 -0
  27. package/lib/ocsp.js +1 -0
  28. package/lib/oid.js +1 -0
  29. package/lib/path-validate.js +1 -0
  30. package/lib/pbes2.js +16 -8
  31. package/lib/pkcs12-build.js +203 -12
  32. package/lib/schema-all.js +1 -0
  33. package/lib/schema-c509.js +1 -0
  34. package/lib/schema-csrattrs.js +1 -0
  35. package/lib/schema-engine.js +1 -0
  36. package/lib/schema-pkcs8.js +1 -0
  37. package/lib/schema-smime.js +1 -0
  38. package/lib/schema-tsp.js +1 -0
  39. package/lib/schema-x509.js +1 -0
  40. package/lib/shbs.js +1 -0
  41. package/lib/sigstore.js +1 -0
  42. package/lib/smime.js +1 -0
  43. package/lib/tls-cert-compress.js +1 -0
  44. package/lib/trust.js +1 -0
  45. package/lib/tsp-sign.js +65 -0
  46. package/lib/webauthn.js +1 -0
  47. package/lib/webcrypto.js +11 -8
  48. package/lib/x509-sign.js +38 -0
  49. package/package.json +1 -1
  50. package/sbom.cdx.json +6 -6
@@ -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
- module.exports = { zeroize: zeroize, zeroizeAll: zeroizeAll };
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 = concat([c.update(_buf(pt)), c.final()]);
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
- try { pt = concat([d.update(ct.subarray(0, ct.length - aead.Nt)), d.final()]); }
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;
@@ -5,6 +5,7 @@
5
5
  * @module pki.transport
6
6
  * @nav Protocols
7
7
  * @title Transport
8
+ * @fullname HTTP transport for OCSP, CRL, EST and ACME requests
8
9
  * @order 195
9
10
  * @slug transport
10
11
  *
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
@@ -5,6 +5,7 @@
5
5
  * @module pki.jose
6
6
  * @nav Protocols
7
7
  * @title JOSE (JWS / JWK)
8
+ * @fullname JOSE: JWS signatures and JWK keys
8
9
  * @order 10
9
10
  * @slug jose
10
11
  *
@@ -456,7 +457,7 @@ async function verify(jws, opts) {
456
457
  * payload: Buffer // the raw payload octets ("" for POST-as-GET)
457
458
  * key: CryptoKey // the private signing key
458
459
  * profile: string // default "acme-outer"
459
- * jwk: object // the public JWK, when the header embeds jwk
460
+ * jwk: object // the public JWK, in kid mode (a header that embeds jwk supplies its own)
460
461
  *
461
462
  * @example
462
463
  * var ec = await pki.key.generate({ name: "ECDSA", namedCurve: "P-256" });
@@ -465,11 +466,38 @@ async function verify(jws, opts) {
465
466
  * nonce: "oFvnlFP1wIhRlYS2jTaXbA", url: "https://ca.example/acme/new-acct" };
466
467
  * var jws = await pki.jose.sign({ protected: hdr, payload: Buffer.from("{}"), key: priv });
467
468
  */
469
+ // The public JWK has two sources and the embedded one wins, so opts.jwk is read only in kid mode.
470
+ // Supplied alongside a header that embeds its own, it selected nothing: the signature parameters
471
+ // came from the header's key while the caller had named a different one.
468
472
  var _SIGN_KEYS = { protected: 1, payload: 1, key: 1, jwk: 1, profile: 1 };
473
+ var _SIGN_KEYS_EMBEDDED_JWK = { protected: 1, payload: 1, key: 1, profile: 1 };
469
474
  async function sign(opts) {
470
475
  opts = opts || {};
476
+ // The options are checked BEFORE the header is touched. Serializing it runs the caller's own code
477
+ // -- a toJSON, an accessor -- and that code can delete an option from the very bag being checked,
478
+ // so a misspelling reached the door already removed and the message went out signed.
471
479
  guard.identifier.assertKnownKeys(opts, _SIGN_KEYS, E, "jose/bad-input", "unknown pki.jose.sign option ");
472
- var header = opts.protected;
480
+ // The header is then serialized ONCE, and every read below comes from a snapshot of those exact
481
+ // bytes: the header validated, the header embedded in the JWS and the key confirmed against the
482
+ // produced signature are then all the same values. Read live, a caller getter answers each of
483
+ // those questions separately -- assertPublicJwk sees a public JWK while the bytes that ship carry
484
+ // the private one, and the door sees a jwk-mode header while the signer sees a kid-mode one.
485
+ var headerJson;
486
+ try { headerJson = JSON.stringify(opts.protected); }
487
+ catch (e) { throw E("jose/bad-header", "the protected header must be JSON-serializable", e); }
488
+ // Read back through the same bounded reader a received JWS goes through, so a header this side
489
+ // produces is held to the caps and the duplicate-member rule the other side enforces.
490
+ var headerBytes = headerJson === undefined ? null : Buffer.from(headerJson, "utf8");
491
+ var header = headerBytes === null ? opts.protected : parseJson(headerBytes);
492
+ // Which of the two key sources is in force is known only once the header has been read, so the
493
+ // narrower table runs here. It re-walks the bag, which also catches a name that code added.
494
+ var embedsJwk = !!(header && typeof header === "object" && header.jwk);
495
+ if (embedsJwk) {
496
+ guard.identifier.assertKnownKeys(opts, _SIGN_KEYS_EMBEDDED_JWK, E, "jose/bad-input", function (k) {
497
+ return "unknown pki.jose.sign option " + JSON.stringify(k) +
498
+ (k === "jwk" ? "; the protected header already embeds the public JWK" : "");
499
+ });
500
+ }
473
501
  var payload = opts.payload;
474
502
  if (!Buffer.isBuffer(payload)) throw E("jose/bad-input", "payload must be a Buffer (empty Buffer for POST-as-GET)");
475
503
  // Re-view before the length===0 fast-path: a DETACHED payload reads as zero-length
@@ -493,7 +521,7 @@ async function sign(opts) {
493
521
  // also PUBLISHED to the server, so it must be public-only: an accidentally exported
494
522
  // private JWK must never be serialized into the protected header.
495
523
  if (header.jwk) { _assertKeyType(checked.algRow, header.jwk); assertPublicJwk(header.jwk); }
496
- var protectedB64 = b64uEncode(Buffer.from(JSON.stringify(header), "utf8"));
524
+ var protectedB64 = b64uEncode(headerBytes); // the exact bytes every check above read
497
525
  var payloadB64 = payload.length === 0 ? "" : b64uEncode(payload);
498
526
  var signingInput = Buffer.from(protectedB64 + "." + payloadB64, "ascii");
499
527
  var jwk = header.jwk || opts.jwk || {};
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
@@ -5,6 +5,7 @@
5
5
  * @module pki.merkle
6
6
  * @nav Transparency
7
7
  * @title Merkle
8
+ * @fullname Merkle trees: inclusion and consistency proofs (RFC 6962)
8
9
  * @order 220
9
10
  * @slug merkle
10
11
  *
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
@@ -5,6 +5,7 @@
5
5
  * @module pki.oid
6
6
  * @nav Core
7
7
  * @title Object Identifiers
8
+ * @fullname OID registry: resolve object identifiers by name or dotted form
8
9
  * @order 40
9
10
  * @slug oid
10
11
  *
@@ -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
- function cbcEncrypt(key, iv, plaintext, keyBits) {
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 Buffer.concat([c.update(plaintext), c.final()]);
141
+ return guard.secret.cipherFinish(c, plaintext, E, code, "a content-encryption intermediate");
139
142
  }
140
- function cbcDecrypt(key, iv, ct, keyBits) {
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
- return Buffer.concat([d.update(ct), d.final()]);
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. yields the error the factory returns, so the
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
  }