@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.
- package/CHANGELOG.md +57 -1
- package/lib/acme.js +7 -4
- 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 +16 -1
- package/lib/cms-decrypt.js +58 -29
- package/lib/cms-encrypt.js +200 -38
- package/lib/cms-sign.js +18 -4
- package/lib/cms-verify.js +5 -1
- package/lib/constants.js +1 -0
- package/lib/crl-sign.js +62 -2
- 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 +31 -3
- 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 +203 -12
- 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 +65 -0
- package/lib/webauthn.js +1 -0
- package/lib/webcrypto.js +11 -8
- package/lib/x509-sign.js +38 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
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
|
@@ -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,
|
|
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
|
-
|
|
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(
|
|
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
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
|
}
|