@blamejs/pki 0.5.5 → 0.5.7
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 +42 -0
- package/MIGRATING.md +63 -0
- package/lib/acme.js +47 -19
- package/lib/attrcert-sign.js +5 -1
- package/lib/cmc-build.js +8 -13
- package/lib/cmc-verify.js +2 -2
- package/lib/cmp-build.js +7 -2
- package/lib/cmp-session.js +4 -2
- package/lib/cmp-verify.js +1 -1
- package/lib/cms-compress.js +1 -2
- package/lib/cms-decrypt.js +2 -4
- package/lib/cms-encrypt.js +1 -2
- package/lib/cms-sign.js +42 -4
- package/lib/cms-verify.js +68 -17
- package/lib/crl-sign.js +9 -3
- package/lib/crmf-sign.js +5 -1
- package/lib/csr-sign.js +5 -1
- package/lib/est.js +5 -4
- package/lib/guard-all.js +2 -0
- package/lib/guard-async.js +37 -0
- package/lib/guard-bytes.js +368 -5
- package/lib/guard-parsed.js +117 -9
- package/lib/ocsp.js +24 -6
- package/lib/pbes2.js +11 -1
- package/lib/pkcs12-build.js +90 -27
- package/lib/pki-build.js +2 -3
- package/lib/schema-cms.js +150 -2
- package/lib/sign-scheme.js +7 -4
- package/lib/sigstore.js +10 -0
- package/lib/tsp-sign.js +116 -19
- package/lib/validator-tpm.js +1 -1
- package/lib/webauthn-mds.js +1 -1
- package/lib/webauthn.js +1 -1
- package/lib/x509-sign.js +11 -2
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/pbes2.js
CHANGED
|
@@ -177,7 +177,12 @@ function pbes2Encrypt(pwBytes, plaintext, opts, E, prefix) {
|
|
|
177
177
|
// <prefix>/bad-input a param guard raises is normalized to the structural <prefix>/bad-algorithm-parameters.
|
|
178
178
|
// A wrong key / bad PKCS#7 pad collapses to the UNIFORM <prefix>/decrypt-failed (RFC 8018 sec. 8). The
|
|
179
179
|
// plaintext integrity re-check (re-parse as a PrivateKeyInfo / SafeContents) is the CALLER's step.
|
|
180
|
-
|
|
180
|
+
// `budget` (optional) is a shared { rounds } tally a caller decrypting MANY structures under one
|
|
181
|
+
// call charges against, so the per-structure iteration cap cannot simply reset each time: PBKDF2
|
|
182
|
+
// runs on the event loop, and a store that repeats a costly bag up to the parser's element limit
|
|
183
|
+
// otherwise multiplies the cap by that limit. A caller decrypting exactly one structure passes
|
|
184
|
+
// nothing and is bounded by the cap alone.
|
|
185
|
+
function pbes2Decrypt(pwBytes, params, ciphertext, opts, E, prefix, budget) {
|
|
181
186
|
var keyBits, iv, pb;
|
|
182
187
|
try {
|
|
183
188
|
var p = seqChildren(params, 2, "PBES2 parameters", E, prefix);
|
|
@@ -197,6 +202,11 @@ function pbes2Decrypt(pwBytes, params, ciphertext, opts, E, prefix) {
|
|
|
197
202
|
}
|
|
198
203
|
throw E(prefix + "/bad-algorithm-parameters", "malformed PBES2 parameters", e);
|
|
199
204
|
}
|
|
205
|
+
// Charge the shared budget BEFORE deriving, so the work is refused rather than performed.
|
|
206
|
+
if (budget) {
|
|
207
|
+
budget.rounds -= pb.iterations;
|
|
208
|
+
if (budget.rounds < 0) throw E(prefix + "/iteration-limit", "the aggregate PBKDF2 key-derivation work exceeds the budget (a hostile many-element input)");
|
|
209
|
+
}
|
|
200
210
|
var dk = nodeCrypto.pbkdf2Sync(pwBytes, pb.salt, pb.iterations, keyBits / 8, pb.prfNode);
|
|
201
211
|
try { return cbcDecrypt(dk, iv, ciphertext, keyBits); }
|
|
202
212
|
catch (_e) { throw E(prefix + "/decrypt-failed", "decryption failed"); }
|
package/lib/pkcs12-build.js
CHANGED
|
@@ -69,12 +69,14 @@ var MAX_PBMAC1_KEYLEN = 1024; // an HMAC key beyond a hash block is pointless
|
|
|
69
69
|
// pkcs12-local (its KDF is bespoke here) while PBMAC1 reuses the toolkit-wide PBKDF2 ceiling. 1e6 is ~500x
|
|
70
70
|
// the OpenSSL default (2048) yet still bounds the loop to ~1 second.
|
|
71
71
|
var CLASSIC_MAC_MAX_ITERATIONS = 1000000;
|
|
72
|
-
// The AGGREGATE cap on synchronous
|
|
73
|
-
//
|
|
74
|
-
// hostile store that duplicates a costly
|
|
75
|
-
// the event loop for minutes; this bounds the total to
|
|
76
|
-
//
|
|
77
|
-
|
|
72
|
+
// The AGGREGATE cap on synchronous KDF work across a single open() -- rounds summed over every encrypted
|
|
73
|
+
// bag and safe, whichever scheme it uses: the legacy App. B KDF (its block count x iterations) and PBKDF2
|
|
74
|
+
// alike. The per-bag iteration cap resets per bag, so a hostile store that duplicates a costly bag up to the
|
|
75
|
+
// parser's 1024-element limit would otherwise block the event loop for minutes; this bounds the total to
|
|
76
|
+
// ~the classic MAC ceiling. It covers BOTH schemes because covering only the legacy one left the modern
|
|
77
|
+
// path -- the one every current producer emits -- free to multiply its cap by the element limit.
|
|
78
|
+
// A conforming store runs only a few thousand rounds (its handful of bags at ~2048 iterations).
|
|
79
|
+
var KDF_MAX_ROUNDS = CLASSIC_MAC_MAX_ITERATIONS;
|
|
78
80
|
|
|
79
81
|
// The classic App. B.2 KDF (u = hash output bytes, v = compression block bytes) per RFC 7292 App. B.2.
|
|
80
82
|
var P12_KDF_UV = {
|
|
@@ -102,19 +104,32 @@ var DIGEST_NAME = { sha1: "sha1", sha256: "sha256", sha384: "sha384", sha512: "s
|
|
|
102
104
|
// is taken verbatim as already-formatted bytes (an escape hatch for a caller that pre-encodes).
|
|
103
105
|
function _p12Password(pw) { return _p12PasswordOwned(pw).bytes; }
|
|
104
106
|
|
|
107
|
+
// An ABSENT password is refused rather than encoded as the empty one. The two are not the same
|
|
108
|
+
// credential, and the difference is invisible at the call site: a caller who misspells the option,
|
|
109
|
+
// or threads it through a layer that drops it, otherwise gets a store whose private key is
|
|
110
|
+
// protected by nothing and no error anywhere saying so. The empty password remains available --
|
|
111
|
+
// it just has to be asked for, as "".
|
|
112
|
+
var _MISSING_PASSWORD = "a password must be a string, Buffer, or Uint8Array -- an omitted password " +
|
|
113
|
+
"is not the empty password; pass \"\" to use the empty one deliberately";
|
|
114
|
+
|
|
115
|
+
// Every field opts.integrity reads. Adding one here is the only way to make it accepted, so a
|
|
116
|
+
// capability cannot arrive with its option silently ignored at this boundary.
|
|
117
|
+
var _INTEGRITY_OPTS = { mode: 1, signer: 1, signers: 1, certificates: 1, sid: 1, signingTime: 1 };
|
|
118
|
+
|
|
105
119
|
// The same encoding, reporting OWNERSHIP -- mirroring pbes2.passwordBytesOwned. A caller-supplied
|
|
106
120
|
// Buffer is returned AS-IS and is BORROWED: clearing it would destroy the caller's own credential,
|
|
107
121
|
// which is a worse defect than leaving a copy readable. Every other input is re-encoded into a
|
|
108
122
|
// buffer this module allocated, which it must clear once a derivation has consumed it.
|
|
109
123
|
function _p12PasswordOwned(pw) {
|
|
110
|
-
if (Buffer.isBuffer(pw)) return { bytes: pw, owned: false };
|
|
124
|
+
if (Buffer.isBuffer(pw)) return { bytes: guard.bytes.view(pw, Pkcs12Error, "pkcs12/bad-input", "the password"), owned: false };
|
|
111
125
|
return { bytes: _p12Encode(pw), owned: true };
|
|
112
126
|
}
|
|
113
127
|
function _p12Encode(pw) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (
|
|
128
|
+
// A view first, so a detached backing store is a reject rather than an empty password; then a COPY,
|
|
129
|
+
// because everything this function returns is reported OWNED and gets zeroized after derivation --
|
|
130
|
+
// handing back a view of a caller's Uint8Array would wipe the caller's own credential.
|
|
131
|
+
if (Buffer.isBuffer(pw) || pw instanceof Uint8Array) return guard.bytes.snapshot(pw, Pkcs12Error, "pkcs12/bad-input", "the password");
|
|
132
|
+
if (typeof pw !== "string") throw _err("pkcs12/bad-input", _MISSING_PASSWORD);
|
|
118
133
|
var out = Buffer.alloc(pw.length * 2 + 2); // + the 2-byte NULL terminator
|
|
119
134
|
for (var i = 0; i < pw.length; i++) {
|
|
120
135
|
var u = pw.charCodeAt(i);
|
|
@@ -130,12 +145,19 @@ function _p12Encode(pw) {
|
|
|
130
145
|
// PBKDF2 the raw UTF-8 password for these modern schemes (confirmed byte-for-byte against `openssl pkcs12`),
|
|
131
146
|
// reserving the BMPString+NULL form for the bespoke Appendix B KDF only. A file we emit must open in OpenSSL,
|
|
132
147
|
// so the modern schemes use UTF-8 here; only the classic Appendix B MAC uses `_p12Password`.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
148
|
+
// The UTF-8 encoding, reporting OWNERSHIP -- the sibling of _p12PasswordOwned, and it exists
|
|
149
|
+
// for the same reason. A caller-supplied Buffer is BORROWED and left alone; every other input is
|
|
150
|
+
// re-encoded into a buffer this module allocated, and a plaintext password copy this module made
|
|
151
|
+
// is cleared once the derivation has consumed it. Without it the App. B.1 copy taken from the same
|
|
152
|
+
// argument in the same call was wiped while this one was not.
|
|
153
|
+
function _pbePasswordOwned(pw) {
|
|
154
|
+
if (Buffer.isBuffer(pw)) return { bytes: guard.bytes.view(pw, Pkcs12Error, "pkcs12/bad-input", "the password"), owned: false };
|
|
155
|
+
if (pw instanceof Uint8Array) return { bytes: guard.bytes.snapshot(pw, Pkcs12Error, "pkcs12/bad-input", "the password"), owned: true };
|
|
156
|
+
if (typeof pw !== "string") throw _err("pkcs12/bad-input", _MISSING_PASSWORD);
|
|
157
|
+
return { bytes: Buffer.from(pw, "utf8"), owned: true };
|
|
158
|
+
}
|
|
159
|
+
function _wipePw(owned) {
|
|
160
|
+
if (owned.owned) guard.secret.zeroize(owned.bytes, Pkcs12Error, "pkcs12/bad-input", "the password encoding");
|
|
139
161
|
}
|
|
140
162
|
|
|
141
163
|
// Concatenate copies of `src` to the smallest positive multiple of `blockSize` (>= src length), truncating
|
|
@@ -267,8 +289,10 @@ function _buildBag(bag, opts, depth) {
|
|
|
267
289
|
var kDer = _coerceDer(bag.key, "shroudedKey key");
|
|
268
290
|
try { pkcs8.parse(kDer); } catch (e2) { throw _err("pkcs12/bad-input", "shroudedKey key is not a well-formed PKCS#8 PrivateKeyInfo", e2); }
|
|
269
291
|
var enc = bag.encrypt || {};
|
|
270
|
-
var pw =
|
|
271
|
-
var r
|
|
292
|
+
var pw = _pbePasswordOwned(enc.password != null ? enc.password : opts.password);
|
|
293
|
+
var r;
|
|
294
|
+
try { r = pbes2.pbes2Encrypt(pw.bytes, kDer, _pbeOpts(enc), _err, "pkcs12"); }
|
|
295
|
+
finally { _wipePw(pw); }
|
|
272
296
|
return _safeBag("pkcs8ShroudedKeyBag", b.sequence([r.algId, b.octetString(r.ct)]), bag); // EncryptedPrivateKeyInfo
|
|
273
297
|
}
|
|
274
298
|
case "cert": {
|
|
@@ -376,8 +400,10 @@ async function _buildAuthSafeElement(sc, opts) {
|
|
|
376
400
|
return b.sequence([b.oid(O("data")), b.explicit(0, b.octetString(safeContentsDer))]);
|
|
377
401
|
}
|
|
378
402
|
if (!sc.encrypt || typeof sc.encrypt !== "object") throw _err("pkcs12/bad-input", "safeContents.encrypt must be an object { password? } (RFC 7292 sec. 5.1) -- omit it entirely for a plaintext safe");
|
|
379
|
-
var pw =
|
|
380
|
-
var r
|
|
403
|
+
var pw = _pbePasswordOwned(sc.encrypt.password != null ? sc.encrypt.password : opts.password);
|
|
404
|
+
var r;
|
|
405
|
+
try { r = pbes2.pbes2Encrypt(pw.bytes, safeContentsDer, _pbeOpts(sc.encrypt), _err, "pkcs12"); }
|
|
406
|
+
finally { _wipePw(pw); }
|
|
381
407
|
var eci = b.sequence([b.oid(O("data")), r.algId, b.contextPrimitive(0, r.ct)]); // EncryptedContentInfo, [0] IMPLICIT ct
|
|
382
408
|
var encData = b.sequence([b.integer(0n), eci]); // EncryptedData { version 0, eci }
|
|
383
409
|
return b.sequence([b.oid(O("encryptedData")), b.explicit(0, encData)]);
|
|
@@ -420,7 +446,10 @@ async function _buildMacData(macOpts, sharedPassword, authSafeDer) {
|
|
|
420
446
|
var iter2 = _assertMacIter(macOpts.iterations == null ? DEFAULT_PBMAC1_ITER : macOpts.iterations, C.LIMITS.PBKDF2_MAX_ITERATIONS);
|
|
421
447
|
var keyLen = macOpts.keyLength != null ? macOpts.keyLength : prf.keyLen;
|
|
422
448
|
if (typeof keyLen !== "number" || !Number.isInteger(keyLen) || keyLen < 20 || keyLen > MAX_PBMAC1_KEYLEN) throw _err("pkcs12/bad-input", "PBMAC1 keyLength must be an integer in [20, " + MAX_PBMAC1_KEYLEN + "] (RFC 9579 sec. 9)");
|
|
423
|
-
var
|
|
449
|
+
var macPw2 = _pbePasswordOwned(password);
|
|
450
|
+
var mac;
|
|
451
|
+
try { mac = await pbes2.pbmac1(macPw2.bytes, salt, iter2, keyLen, prf.wc, prf.wc, authSafeDer); } // PBKDF2 -> UTF-8; prf == messageAuthScheme on build
|
|
452
|
+
finally { _wipePw(macPw2); }
|
|
424
453
|
var desc = { salt: salt, iterationCount: iter2, keyLength: keyLen, prfName: prf.prfName, macName: prf.prfName };
|
|
425
454
|
var digestInfo2 = b.sequence([pbes2.pbmac1AlgId(desc), b.octetString(mac)]);
|
|
426
455
|
// MacData.macSalt + iterations are ignored on a PBMAC1 verify but MUST be present + non-1 (RFC 9579 4c/4d).
|
|
@@ -496,9 +525,33 @@ function _normalizeSpec(spec, opts) {
|
|
|
496
525
|
* { type: 'shroudedKey', key: signerKeyPkcs8, encrypt: { password: 'changeit' } } ] }] },
|
|
497
526
|
* { password: 'changeit', mac: { algorithm: 'hmac', hash: 'sha256' } });
|
|
498
527
|
*/
|
|
499
|
-
|
|
528
|
+
function build(spec, opts) {
|
|
529
|
+
// Both arguments copied at entry and released when the call settles -- see the note on the same
|
|
530
|
+
// call in x509-sign. A PKCS#12 file is assembled over many promise turns (a key derivation per
|
|
531
|
+
// bag, then the outer MAC) while the caller still owns the object holding the passwords and the
|
|
532
|
+
// bag contents. Rewriting a password buffer partway through produced a file whose MAC and whose
|
|
533
|
+
// bag encryption were keyed to two different values, so it opened with neither the password
|
|
534
|
+
// passed in nor the one written over it. The release is what keeps the copy of that password
|
|
535
|
+
// from outliving the call.
|
|
536
|
+
return guard.bytes.fixedCall(Pkcs12Error, "pkcs12/bad-input", [
|
|
537
|
+
[spec, "the PKCS#12 spec"], [opts, "pki.pkcs12.build options"],
|
|
538
|
+
], _build);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async function _build(spec, opts) {
|
|
500
542
|
opts = opts || {};
|
|
501
|
-
|
|
543
|
+
// The integrity mode is checked against the permitted set rather than compared to one literal.
|
|
544
|
+
// Compared, any other spelling reads as "not public-key" and silently selects password integrity,
|
|
545
|
+
// dropping the signer with it: a caller who wrote "publicKey" gets a MAC where they asked for a
|
|
546
|
+
// signature, and nothing in the store or the call says which they got.
|
|
547
|
+
if (opts.integrity != null) {
|
|
548
|
+
if (typeof opts.integrity !== "object" || Array.isArray(opts.integrity)) throw _err("pkcs12/bad-input", "opts.integrity must be an object { mode, signer|signers, ... }");
|
|
549
|
+
guard.identifier.assertKnownKeys(opts.integrity, _INTEGRITY_OPTS, _err, "pkcs12/bad-input", "opts.integrity has an unknown option ");
|
|
550
|
+
if (opts.integrity.mode !== "public-key") {
|
|
551
|
+
throw _err("pkcs12/bad-integrity-mode", "opts.integrity.mode must be \"public-key\" (the only mode it selects); omit opts.integrity for password integrity, got " + JSON.stringify(opts.integrity.mode));
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
var pubKey = opts.integrity != null;
|
|
502
555
|
// RFC 7292 sec. 4: public-key integrity OMITS MacData entirely -- a caller combining opts.mac with it is a
|
|
503
556
|
// config-time reject (the self-check re-parse would otherwise fail the coherence rule anyway).
|
|
504
557
|
if (pubKey && opts.mac != null && opts.mac !== false) throw _err("pkcs12/bad-integrity-mode", "public-key integrity has no MacData -- do not combine opts.mac with opts.integrity.mode 'public-key' (RFC 7292 sec. 4)");
|
|
@@ -590,7 +643,9 @@ async function _verifyMacOfStore(m, password, opts) {
|
|
|
590
643
|
// downgraded store cannot pass under a weak MAC even though the algorithm identifiers parse.
|
|
591
644
|
if (prfWc === "SHA-1" || macWc === "SHA-1") throw _err("pkcs12/unsupported-algorithm", "PBMAC1 with a <= 160-bit digest (SHA-1) is refused (RFC 9579 sec. 5/7)");
|
|
592
645
|
_capWork(kdf.iterationCount, kdf.salt, opts, kdf.keyLength, C.LIMITS.PBKDF2_MAX_ITERATIONS);
|
|
593
|
-
|
|
646
|
+
var vPw = _pbePasswordOwned(password);
|
|
647
|
+
try { computed = await pbes2.pbmac1(vPw.bytes, kdf.salt, kdf.iterationCount, kdf.keyLength, prfWc, macWc, m.macedBytes); } // PBKDF2 -> UTF-8
|
|
648
|
+
finally { _wipePw(vPw); }
|
|
594
649
|
}
|
|
595
650
|
return computed.length === expected.length && guard.crypto.constantTimeEqual(computed, expected);
|
|
596
651
|
}
|
|
@@ -687,7 +742,7 @@ async function open(pfx, password, opts) {
|
|
|
687
742
|
// wrong bag password fails at the first encrypted bag as the uniform pkcs12/decrypt-failed.
|
|
688
743
|
var out = { integrityMode: m.integrityMode, macVerified: macVerified, signers: signers, keys: [], certs: [], crls: [], secrets: [] };
|
|
689
744
|
var i;
|
|
690
|
-
var kdfBudget = { rounds:
|
|
745
|
+
var kdfBudget = { rounds: KDF_MAX_ROUNDS }; // aggregate KDF work budget for this whole open(), both schemes
|
|
691
746
|
for (i = 0; i < m.safeBags.length; i++) _openBag(m.safeBags[i], password, opts, out, 0, kdfBudget);
|
|
692
747
|
for (i = 0; i < m.encryptedSafes.length; i++) await _openEncryptedSafe(m.encryptedSafes[i], password, opts, out, 0, kdfBudget);
|
|
693
748
|
if (opts.keys === "crypto") {
|
|
@@ -801,7 +856,15 @@ function _decryptLegacyPbe(ea, ct, password, opts, budget) {
|
|
|
801
856
|
// Decrypt a PBES2 (RFC 8018) or legacy-PBE (RFC 7292 App. C) bag/safe -- dispatch on the encryptionAlgorithm
|
|
802
857
|
// OID. PBES2 uses the UTF-8 password (the pinned interop convention); legacy PBE uses the App. B.1 BMPString.
|
|
803
858
|
function _decryptBag(ea, ct, password, opts, budget) {
|
|
804
|
-
|
|
859
|
+
// BOTH arms charge the one shared budget. Charging only the legacy arm left the modern one --
|
|
860
|
+
// the arm every current producer emits -- able to reset its per-bag cap on every bag, so a store
|
|
861
|
+
// that repeats a costly PBES2 bag up to the parser's element limit multiplied the cap by that
|
|
862
|
+
// limit in blocking pbkdf2Sync work.
|
|
863
|
+
if (ea.oid === O("pbes2")) {
|
|
864
|
+
var pw = _pbePasswordOwned(password);
|
|
865
|
+
try { return pbes2.pbes2Decrypt(pw.bytes, ea.parameters, ct, opts, _err, "pkcs12", budget); }
|
|
866
|
+
finally { _wipePw(pw); }
|
|
867
|
+
}
|
|
805
868
|
return _decryptLegacyPbe(ea, ct, password, opts, budget);
|
|
806
869
|
}
|
|
807
870
|
|
package/lib/pki-build.js
CHANGED
|
@@ -223,15 +223,14 @@ function makeBuilder(ctx) {
|
|
|
223
223
|
return nodeCrypto.createHash("sha1").update(keyBytes).digest();
|
|
224
224
|
}
|
|
225
225
|
function skiKeyId(val, spkiDer) {
|
|
226
|
-
if (Buffer.isBuffer(val)) return val;
|
|
226
|
+
if (Buffer.isBuffer(val)) return guard.bytes.snapshot(val, ErrorClass, ctx.prefix + "/bad-input", "subjectKeyIdentifier");
|
|
227
227
|
if (val === true) return spkiKeyId(spkiDer);
|
|
228
228
|
throw E("bad-input", "subjectKeyIdentifier must be true (auto-derive) or a Buffer key id");
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
// ---- embedded-input validators ----
|
|
232
232
|
function reqDer(v, what) {
|
|
233
|
-
if (Buffer.isBuffer(v)) return v;
|
|
234
|
-
if (v instanceof Uint8Array) return Buffer.from(v);
|
|
233
|
+
if (Buffer.isBuffer(v) || v instanceof Uint8Array) return guard.bytes.snapshot(v, ErrorClass, ctx.prefix + "/bad-input", what);
|
|
235
234
|
throw E("bad-input", what + " must be a DER Buffer");
|
|
236
235
|
}
|
|
237
236
|
// Full validation of an embedded SubjectPublicKeyInfo via the SAME parser the decoder uses.
|
package/lib/schema-cms.js
CHANGED
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
var asn1 = require("./asn1-der");
|
|
48
48
|
var schema = require("./schema-engine");
|
|
49
49
|
var pkix = require("./schema-pkix");
|
|
50
|
+
var guard = require("./guard-all");
|
|
50
51
|
var oid = require("./oid");
|
|
51
52
|
var frameworkError = require("./framework-error");
|
|
52
53
|
var schemaX509 = require("./schema-x509");
|
|
@@ -211,6 +212,138 @@ function _checkContentBindingAttrs(attrs, mode) {
|
|
|
211
212
|
if (md === 0) throw NS.E("cms/missing-message-digest", "the attribute set must contain a message-digest attribute (RFC 5652 sec. 11.2)");
|
|
212
213
|
}
|
|
213
214
|
|
|
215
|
+
// looksLikeSignedAttributes(bytes) -> boolean.
|
|
216
|
+
//
|
|
217
|
+
// Does `bytes` parse as a DER SignedAttributes block -- a SET OF Attribute carrying BOTH the
|
|
218
|
+
// content-type and message-digest attributes RFC 5652 sec. 5.3 makes mandatory whenever signed
|
|
219
|
+
// attributes are present?
|
|
220
|
+
//
|
|
221
|
+
// This is the detector for the signed-attribute stripping forgery
|
|
222
|
+
// (draft-vangeest-lamps-cms-euf-cma-signeddata, Attack Type 1). A CMS signature does not commit to
|
|
223
|
+
// WHETHER signed attributes were present, so a signature made over a SignedAttributes block can be
|
|
224
|
+
// re-presented as one made over content: drop the signedAttrs field and set the encapsulated
|
|
225
|
+
// content to the DER of those same attributes. Sec. 5.4 then says the signature is over the content
|
|
226
|
+
// itself, which is exactly what it covers, and with no attributes there is no message-digest or
|
|
227
|
+
// content-type attribute left to disagree. The proposed standards fixes are protocol changes -- a
|
|
228
|
+
// context string naming which mode was signed -- that no verifier can apply on its own.
|
|
229
|
+
//
|
|
230
|
+
// What a verifier CAN do is refuse the shape. Every message produced by the attack has, as its
|
|
231
|
+
// content, the encoded SignedAttributes of a real message, and sec. 5.3 requires those to carry
|
|
232
|
+
// both attributes named above. That makes their presence a NECESSARY condition of the attack rather
|
|
233
|
+
// than a guess, and the shape is one ordinary content does not have: a certificate, a JSON payload,
|
|
234
|
+
// arbitrary bytes, and even a SET OF other attributes all fail it. The cost is a message whose
|
|
235
|
+
// legitimate content really is an encoded SignedAttributes block signed WITHOUT attributes, which
|
|
236
|
+
// is refused as genuinely ambiguous -- sign it with attributes and it is unambiguous again.
|
|
237
|
+
//
|
|
238
|
+
// One mandatory attribute value, held to all three conditions a real SignedAttributes meets:
|
|
239
|
+
// exactly one value (RFC 5652 sec. 11.1 / sec. 11.2 make both single-valued), the right tag, and a
|
|
240
|
+
// body that actually READS as that type. All three together, for each attribute -- checking the
|
|
241
|
+
// cardinality and the tag while letting an undecodable body through would refuse content the real
|
|
242
|
+
// SignedAttributes parser could never have produced, which is the false positive this whole
|
|
243
|
+
// detector is shaped to avoid.
|
|
244
|
+
function _readsAs(vals, tagNumber, reader) {
|
|
245
|
+
if (vals.length !== 1 || !schema.isUniversal(vals[0], tagNumber)) return false;
|
|
246
|
+
var reads = true;
|
|
247
|
+
try { reader(vals[0]); }
|
|
248
|
+
catch (_e) {
|
|
249
|
+
reads = false; // right tag, unreadable body -- not a preimage anything signed
|
|
250
|
+
}
|
|
251
|
+
return reads;
|
|
252
|
+
}
|
|
253
|
+
// SigningTime ::= Time is a CHOICE of UTCTime and GeneralizedTime, so the tag is one of two and the
|
|
254
|
+
// reader settles which -- _readsAs pins a single tag and cannot express it.
|
|
255
|
+
function _readsAsTime(vals) {
|
|
256
|
+
if (vals.length !== 1) return false;
|
|
257
|
+
var reads = true;
|
|
258
|
+
try { asn1.read.time(vals[0]); }
|
|
259
|
+
catch (_e) {
|
|
260
|
+
reads = false;
|
|
261
|
+
}
|
|
262
|
+
return reads;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Deliberately a total function: it answers about arbitrary attacker bytes and must never throw.
|
|
266
|
+
// The X.690 sec. 11.6 SET OF ordering rule, as the schema engine applies it: each component's
|
|
267
|
+
// encoding is greater than or equal to the one before it.
|
|
268
|
+
function _ascendingDer(nodes) {
|
|
269
|
+
for (var i = 1; i < nodes.length; i++) {
|
|
270
|
+
if (Buffer.compare(nodes[i - 1].bytes, nodes[i].bytes) > 0) return false;
|
|
271
|
+
}
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function looksLikeSignedAttributes(bytes) {
|
|
276
|
+
if (!bytes || !bytes.length) return false;
|
|
277
|
+
var node;
|
|
278
|
+
try { node = asn1.decode(bytes); }
|
|
279
|
+
catch (_e) { return false; } // not DER at all -- not this shape
|
|
280
|
+
if (!schema.isUniversal(node, asn1.TAGS.SET) || !node.constructed) return false;
|
|
281
|
+
var kids = node.children || [];
|
|
282
|
+
if (!kids.length) return false;
|
|
283
|
+
// Every rule _checkContentBindingAttrs applies to a REAL SignedAttributes is applied here, and
|
|
284
|
+
// for one reason: the detector must match only what a conforming block can be. A set the real
|
|
285
|
+
// parser would have rejected cannot be the preimage of any signature, so matching it would refuse
|
|
286
|
+
// content no attack could have produced. Enumerated against that function rather than discovered
|
|
287
|
+
// one rule at a time -- no duplicate attribute types (sec. 5.3), content-type single-valued and a
|
|
288
|
+
// readable OID (sec. 11.1), message-digest single-valued and a readable OCTET STRING (sec. 11.2),
|
|
289
|
+
// signing-time when present single-valued and a readable Time (sec. 11.3).
|
|
290
|
+
// X.690 sec. 11.6: the components of a SET OF appear in ascending DER order, and the schema
|
|
291
|
+
// engine enforces exactly that on the real SignedAttributes. An out-of-order set is one the
|
|
292
|
+
// walker refuses, so it is not the preimage of any signature either.
|
|
293
|
+
if (!_ascendingDer(kids)) return false;
|
|
294
|
+
var sawContentType = false, sawMessageDigest = false, seenTypes = Object.create(null);
|
|
295
|
+
for (var i = 0; i < kids.length; i++) {
|
|
296
|
+
var a = kids[i];
|
|
297
|
+
// Attribute ::= SEQUENCE { attrType OBJECT IDENTIFIER, attrValues SET OF ANY }
|
|
298
|
+
if (!schema.isUniversal(a, asn1.TAGS.SEQUENCE)) return false;
|
|
299
|
+
if (!a.children || a.children.length !== 2) return false;
|
|
300
|
+
var t = a.children[0], vs = a.children[1];
|
|
301
|
+
if (!schema.isUniversal(t, asn1.TAGS.OBJECT_IDENTIFIER)) return false;
|
|
302
|
+
if (!schema.isUniversal(vs, asn1.TAGS.SET)) return false;
|
|
303
|
+
var attrOid;
|
|
304
|
+
try { attrOid = asn1.read.oid(t); }
|
|
305
|
+
catch (_e2) { return false; }
|
|
306
|
+
if (seenTypes[attrOid]) return false; // a repeated attribute type -- sec. 5.3 forbids it
|
|
307
|
+
seenTypes[attrOid] = true;
|
|
308
|
+
// The sec. 11 PLACEMENT rows, from the same table the real parser reads. An attribute the
|
|
309
|
+
// parser refuses to see in signedAttrs -- id-countersignature is the one sec. 11.4 names --
|
|
310
|
+
// cannot appear in a conforming SignedAttributes, so a set containing it is not a preimage any
|
|
311
|
+
// signature covers. Missing this row was the difference between a necessary condition and a
|
|
312
|
+
// guess: it would have refused ordinary content that merely carried that attribute encoding.
|
|
313
|
+
var placement = ATTR_FORBIDDEN_IN[attrOid];
|
|
314
|
+
if (placement && placement.signed) return false;
|
|
315
|
+
// RFC 5652 gives every AttributeValue set SIZE (1..MAX), so an attribute with an EMPTY value
|
|
316
|
+
// set is one no conforming signer produced and no signature covers. The upper bound is
|
|
317
|
+
// deliberately not applied: this decoder caps values per attribute as a resource limit of its
|
|
318
|
+
// own, and a limit this implementation chose is not a fact about what a signature can cover.
|
|
319
|
+
// An external signer may sign a conforming set larger than that cap, and the stripped message
|
|
320
|
+
// presents those bytes as opaque content where the cap never applies -- refusing to recognize
|
|
321
|
+
// it because of a local limit would miss exactly the preimage the attack reuses.
|
|
322
|
+
var n = (vs.children || []).length;
|
|
323
|
+
if (n < 1) return false;
|
|
324
|
+
if (!_ascendingDer(vs.children || [])) return false; // the inner SET OF is ordered too
|
|
325
|
+
// The two mandatory attributes are checked down to their VALUES, not just their type OIDs.
|
|
326
|
+
// RFC 5652 sec. 11.1 makes content-type a single OBJECT IDENTIFIER and sec. 11.2 makes
|
|
327
|
+
// message-digest a single OCTET STRING, so a set carrying those OIDs over an empty or
|
|
328
|
+
// wrongly-typed value CANNOT be the preimage of a real signature -- and refusing it would be a
|
|
329
|
+
// false positive on content that merely resembles the shape. The detector has to stay a
|
|
330
|
+
// necessary condition of the attack; anything broader costs a legitimate caller.
|
|
331
|
+
var vals = vs.children || [];
|
|
332
|
+
if (attrOid === OID_CONTENT_TYPE) {
|
|
333
|
+
if (!_readsAs(vals, asn1.TAGS.OBJECT_IDENTIFIER, asn1.read.oid)) return false;
|
|
334
|
+
sawContentType = true;
|
|
335
|
+
} else if (attrOid === OID_MESSAGE_DIGEST) {
|
|
336
|
+
if (!_readsAs(vals, asn1.TAGS.OCTET_STRING, asn1.read.octetString)) return false;
|
|
337
|
+
sawMessageDigest = true;
|
|
338
|
+
} else if (attrOid === OID_SIGNING_TIME) {
|
|
339
|
+
// Not mandatory, but when present sec. 11.3 constrains it the same way, so a set carrying an
|
|
340
|
+
// unreadable signing-time is one the real parser would have refused.
|
|
341
|
+
if (!_readsAsTime(vals)) return false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return sawContentType && sawMessageDigest;
|
|
345
|
+
}
|
|
346
|
+
|
|
214
347
|
// RFC 5652 sec. 5.3 / sec. 9.3 -- when a content-type attribute is present, it MUST
|
|
215
348
|
// be single-valued (sec. 11.1) and its value MUST equal the eContentType (a
|
|
216
349
|
// cross-field consistency both parsed here). Shared by SignedData signedAttrs,
|
|
@@ -1150,7 +1283,15 @@ var CONTENT_INFO = schema.seq([
|
|
|
1150
1283
|
* cms.signerInfos[0].sid.serialNumberHex; // -> "0a1b"
|
|
1151
1284
|
* cms.encapContentInfo.eContent; // -> Buffer | null (detached)
|
|
1152
1285
|
*/
|
|
1153
|
-
|
|
1286
|
+
// Recording, for the reason the certificate and CRL parsers are: a SignedData is one or more
|
|
1287
|
+
// signatures over byte ranges, and a parsed one presents those ranges (`signedAttrsBytes`, the
|
|
1288
|
+
// encapsulated `eContent`), the signatures, and the certificates that verify them as separate
|
|
1289
|
+
// properties of one object. Keep a genuine signer's `signature` and `signedAttrsBytes` and replace
|
|
1290
|
+
// the `eContent` beside them, and every part of the check still passes for content that signer never
|
|
1291
|
+
// signed -- which is exactly the forgery pki.cms.verify's own block claims to defend. The verdict
|
|
1292
|
+
// verbs re-derive from what is recorded here, so the object a caller passes names bytes rather than
|
|
1293
|
+
// asserting facts.
|
|
1294
|
+
var parse = pkix.makeRecordingParser({ pemLabel: "CMS", PemError: PemError, ErrorClass: CmsError, prefix: "cms", what: "CMS ContentInfo", topSchema: CONTENT_INFO, ns: NS }, "cms");
|
|
1154
1295
|
|
|
1155
1296
|
/**
|
|
1156
1297
|
* @primitive pki.schema.cms.pemDecode
|
|
@@ -1220,7 +1361,13 @@ function walkEnvelopedData(node) { return schema.walk(ENVELOPED_DATA, node, NS).
|
|
|
1220
1361
|
// (an RFC 7292 PFX authSafe or encrypted safe, whose wire encoding may be BER
|
|
1221
1362
|
// that the strict `parse` entry would refuse). Same contract as
|
|
1222
1363
|
// walkEnvelopedData: the node is the bare structure, typed cms/* on rejection.
|
|
1223
|
-
|
|
1364
|
+
// Records provenance, like parse: the structure a consumer hands to pki.cms.verify must be
|
|
1365
|
+
// re-derivable from the bytes it was walked from, and a PFX authSafe reaches verify this way. The
|
|
1366
|
+
// walker's own BER-tolerant decode is what replays it -- the strict `parse` entry would refuse the
|
|
1367
|
+
// indefinite-length encoding real stores carry.
|
|
1368
|
+
var walkSignedData = guard.parsed.recordingWalker("cms", function (node) {
|
|
1369
|
+
return schema.walk(SIGNED_DATA, node, NS).result;
|
|
1370
|
+
}, function (der) { return asn1.decode(der, { ber: true }); });
|
|
1224
1371
|
function walkEncryptedData(node) { return schema.walk(ENCRYPTED_DATA, node, NS).result; }
|
|
1225
1372
|
// Validate + surface one countersignature value (RFC 5652 sec. 11.4, Countersignature ::=
|
|
1226
1373
|
// SignerInfo) into the same parsed shape parse() gives a top-level SignerInfo -- so pki.cms.verify
|
|
@@ -1295,6 +1442,7 @@ module.exports = {
|
|
|
1295
1442
|
walkSignedData: walkSignedData,
|
|
1296
1443
|
walkEncryptedData: walkEncryptedData,
|
|
1297
1444
|
walkCountersignature: walkCountersignature,
|
|
1445
|
+
looksLikeSignedAttributes: looksLikeSignedAttributes,
|
|
1298
1446
|
assertAttachedCiphertext: assertAttachedCiphertext,
|
|
1299
1447
|
// The structure's own algorithm tables, exported (like the walk* helpers) as the single source
|
|
1300
1448
|
// of truth the crypto layer (cms-encrypt / cms-decrypt) shares -- so the wrap<->KEK-length, the
|
package/lib/sign-scheme.js
CHANGED
|
@@ -195,8 +195,11 @@ function _assertKeyMatchesScheme(key, imp, E) {
|
|
|
195
195
|
if (imp.namedCurve && ka.namedCurve !== imp.namedCurve) throw E("bad-input", "the signer CryptoKey curve (" + ka.namedCurve + ") does not match the certificate curve (" + imp.namedCurve + ")");
|
|
196
196
|
}
|
|
197
197
|
function _normPkcs8(k, label, E) {
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
// A caller's own Buffer is BORROWED, not copied -- the same rule _importKey states below. Every
|
|
199
|
+
// other form is copied, and neither copy is duplicated further, so a private key never gains a
|
|
200
|
+
// plaintext duplicate this module cannot account for.
|
|
201
|
+
if (Buffer.isBuffer(k)) return guard.bytes.view(k, E, "bad-input", label);
|
|
202
|
+
if (k instanceof Uint8Array) return guard.bytes.snapshot(k, E, "bad-input", label);
|
|
200
203
|
if (typeof k === "string") { try { return pkcs8.pemDecode(k); } catch (e) { throw E("bad-input", label + " PEM could not be decoded", e); } }
|
|
201
204
|
throw E("bad-input", label + " must be a PKCS#8 DER Buffer, Uint8Array, or PEM string");
|
|
202
205
|
}
|
|
@@ -219,8 +222,8 @@ function _importKey(key, imp, E) {
|
|
|
219
222
|
// only thing that reads it. A caller's own Buffer is passed through untouched: they hold a live
|
|
220
223
|
// reference and will use it again, so clearing it would destroy their key.
|
|
221
224
|
var der, owned = false;
|
|
222
|
-
if (Buffer.isBuffer(key)) der = key;
|
|
223
|
-
else if (key instanceof Uint8Array) { der =
|
|
225
|
+
if (Buffer.isBuffer(key)) der = guard.bytes.view(key, E, "bad-input", "the signer private key");
|
|
226
|
+
else if (key instanceof Uint8Array) { der = guard.bytes.snapshot(key, E, "bad-input", "the signer private key"); owned = true; }
|
|
224
227
|
else if (typeof key === "string") {
|
|
225
228
|
try { der = pkcs8.pemDecode(key); }
|
|
226
229
|
catch (e) { throw E("bad-input", "the signer PEM private key could not be decoded", e); }
|
package/lib/sigstore.js
CHANGED
|
@@ -522,6 +522,9 @@ var IDENTITY_FIELDS = ["san", "issuer", "sourceRepositoryURI"];
|
|
|
522
522
|
// The guard tests membership with hasOwnProperty, so the permitted set is a lookup object --
|
|
523
523
|
// an array would treat "0"/"1" as the known keys and reject every real field name.
|
|
524
524
|
var IDENTITY_KEYS = { san: 1, issuer: 1, sourceRepositoryURI: 1 };
|
|
525
|
+
// Every option pki.sigstore.verifyBundle reads. Adding one here is the only way to make it
|
|
526
|
+
// accepted, so a capability cannot arrive with its option silently ignored at this boundary.
|
|
527
|
+
var _VERIFY_BUNDLE_OPTS = { fulcioRoots: 1, rekorKeys: 1, identity: 1, predicateType: 1, time: 1 };
|
|
525
528
|
|
|
526
529
|
// Returns WHICH fields were actually compared. A bundle verifies its own signature and log
|
|
527
530
|
// inclusion whoever signed it -- Fulcio issues to anyone who completes an OIDC flow -- so
|
|
@@ -633,6 +636,13 @@ async function verifyBundle(bundle, opts) {
|
|
|
633
636
|
throw new TypeError("verifyBundle: bundle must be an object, JSON string, or Buffer");
|
|
634
637
|
}
|
|
635
638
|
opts = opts || {};
|
|
639
|
+
// An unrecognized option is refused, not swallowed. The identity policy one level down already
|
|
640
|
+
// closes this door for exactly the reason it needs closing here too: cosign spells the signer pin
|
|
641
|
+
// `certificateIdentity`, and a swallowed spelling checks nothing under a name the operator
|
|
642
|
+
// believes pins the signer. At the top level the same slip loses the SLSA predicate pin as well,
|
|
643
|
+
// and `predicateType` has no `identityChecked`-style field, so nothing in the verdict reveals
|
|
644
|
+
// that the pin never ran.
|
|
645
|
+
guard.identifier.assertKnownKeys(opts, _VERIFY_BUNDLE_OPTS, _err, "sigstore/bad-input", "pki.sigstore.verifyBundle has an unknown option ");
|
|
636
646
|
var b = parseBundle(bundle);
|
|
637
647
|
var vm = b.verificationMaterial;
|
|
638
648
|
var env = b.dsseEnvelope;
|