@blamejs/pki 0.5.32 → 0.5.33
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 +9 -0
- package/README.md +1 -1
- package/lib/cms-sign.js +207 -86
- package/lib/cms-verify.js +360 -239
- package/lib/guard-bytes.js +32 -1
- package/lib/guard-intrinsic.js +13 -0
- package/lib/webcrypto.js +79 -0
- package/package.json +2 -2
- package/sbom.cdx.json +6 -6
package/lib/cms-verify.js
CHANGED
|
@@ -52,6 +52,13 @@ var pkix = require("./schema-pkix");
|
|
|
52
52
|
|
|
53
53
|
var CmsError = frameworkError.CmsError;
|
|
54
54
|
function _err(code, message, cause) { return new CmsError(code, message, cause); }
|
|
55
|
+
// Promise.resolve captured at load and invoked through the captured Reflect.apply, so a streamed
|
|
56
|
+
// opts.content -- whose iterator runs during the digest await and can replace Promise.resolve -- cannot
|
|
57
|
+
// turn a resolved { ok: false } verdict into acceptance. The verify path resolves every value through
|
|
58
|
+
// this and awaits every step: Promise.all is deliberately NOT used, since its spec-internal element
|
|
59
|
+
// handling dispatches through the caller-replaceable Promise.prototype.then, which await does not.
|
|
60
|
+
var _Promise = Promise, _promiseResolveRaw = Promise.resolve;
|
|
61
|
+
function _pResolve(v) { return intrinsic.apply(_promiseResolveRaw, _Promise, [v]); }
|
|
55
62
|
|
|
56
63
|
var OID_MESSAGE_DIGEST = oid.byName("messageDigest");
|
|
57
64
|
var OID_CONTENT_TYPE = oid.byName("contentType");
|
|
@@ -76,8 +83,8 @@ var SIG_HASH = { sha1: "SHA-1", sha256: "SHA-256", sha384: "SHA-384", sha512: "S
|
|
|
76
83
|
// Is `name` a message-digest algorithm this verifier supports?
|
|
77
84
|
function _supportedDigest(name) { return intrinsic.hasOwn(DIGEST_HASH, name) && !!DIGEST_HASH[name]; }
|
|
78
85
|
// The digest of `content` under the named algorithm, resolved to a Buffer.
|
|
79
|
-
function _computeDigest(name, content) {
|
|
80
|
-
return subtle.digest(DIGEST_HASH[name], content)
|
|
86
|
+
async function _computeDigest(name, content) {
|
|
87
|
+
return intrinsic.bufferFrom(await subtle.digest(DIGEST_HASH[name], content));
|
|
81
88
|
}
|
|
82
89
|
// A signatureAlgorithm name -> its verify scheme. A combined OID (sha256WithRSAEncryption,
|
|
83
90
|
// ecdsaWithSHA256) carries its own hash; a bare key OID (rsaEncryption, ecPublicKey) takes the
|
|
@@ -136,9 +143,11 @@ function _findSignerCerts(sid, parsedCerts) {
|
|
|
136
143
|
for (var i = 0; i < parsedCerts.length; i++) {
|
|
137
144
|
var c = parsedCerts[i];
|
|
138
145
|
if (sid.subjectKeyIdentifier != null) {
|
|
139
|
-
|
|
146
|
+
// Captured equality: over a streamed content the untrusted iterator ran during the digest await
|
|
147
|
+
// and could replace Buffer.prototype.equals to match the wrong certificate to a signer.
|
|
148
|
+
if (c.ski && intrinsic.bufferEquals(c.ski, _toBuf(sid.subjectKeyIdentifier, "sid.subjectKeyIdentifier"))) intrinsic.push(out, c);
|
|
140
149
|
} else if (sid.issuer && sid.serialNumberHex != null) {
|
|
141
|
-
if (c.cert.serialNumberHex === sid.serialNumberHex && guard.name.dnEqual(c.cert.issuer.rdns, sid.issuer.rdns, _err, "cms/bad-name", "the signer certificate issuer"))
|
|
150
|
+
if (c.cert.serialNumberHex === sid.serialNumberHex && guard.name.dnEqual(c.cert.issuer.rdns, sid.issuer.rdns, _err, "cms/bad-name", "the signer certificate issuer")) intrinsic.push(out, c);
|
|
142
151
|
}
|
|
143
152
|
}
|
|
144
153
|
return out;
|
|
@@ -150,27 +159,29 @@ function _findSignerCerts(sid, parsedCerts) {
|
|
|
150
159
|
// through to the next -- so a colliding certificate cannot make a valid signature read invalid.
|
|
151
160
|
// When no candidate verifies, a plain false is a code-less verdict; the last structural fault's
|
|
152
161
|
// code is surfaced if one occurred (so the diagnostic is not lost).
|
|
153
|
-
function _verifyAgainstCandidates(scheme, sigHash, sigBytes, signedBytes, sid, candidates, pssSalt, expectedKeyOid) {
|
|
162
|
+
async function _verifyAgainstCandidates(scheme, sigHash, sigBytes, signedBytes, sid, candidates, pssSalt, expectedKeyOid) {
|
|
154
163
|
var lastErr = null;
|
|
155
|
-
|
|
156
|
-
if (idx >= candidates.length) {
|
|
157
|
-
return lastErr ? { ok: false, code: lastErr.code, sid: sid, cert: candidates[0].der, message: lastErr.message }
|
|
158
|
-
: { ok: false, sid: sid, cert: candidates[0].der };
|
|
159
|
-
}
|
|
164
|
+
for (var idx = 0; idx < candidates.length; idx++) {
|
|
160
165
|
var c = candidates[idx];
|
|
161
166
|
// One-shot family (EdDSA/ML-DSA): the signer cert public-key algorithm OID MUST equal the
|
|
162
167
|
// SignerInfo signatureAlgorithm OID. A candidate whose SPKI disagrees is skipped with a precise
|
|
163
168
|
// verdict; importing it under the wrong name would surface a foreign webcrypto/data throw.
|
|
164
169
|
if (expectedKeyOid && c.cert.subjectPublicKeyInfo.algorithm.oid !== expectedKeyOid) {
|
|
165
170
|
lastErr = _err("cms/unsupported-algorithm", "the signer certificate public-key algorithm does not match the SignerInfo signatureAlgorithm");
|
|
166
|
-
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
// `await`, not `.then`: over a streamed content the untrusted iterator ran during the digest await
|
|
174
|
+
// and could replace Promise.prototype.then to return a fabricated `true`; await is immune (it does
|
|
175
|
+
// not dispatch through the caller-replaceable prototype method).
|
|
176
|
+
try {
|
|
177
|
+
var ok = await _verifySignature(scheme, sigHash, sigBytes, c.cert.subjectPublicKeyInfo.bytes, signedBytes, _certCurveOid(c.cert), pssSalt);
|
|
178
|
+
if (ok === true) return { ok: true, sid: sid, cert: c.der };
|
|
179
|
+
} catch (e) {
|
|
180
|
+
lastErr = (e instanceof CmsError) ? e : _err("cms/verify-error", "the SignerInfo signature could not be evaluated", e);
|
|
167
181
|
}
|
|
168
|
-
return Promise.resolve()
|
|
169
|
-
.then(function () { return _verifySignature(scheme, sigHash, sigBytes, c.cert.subjectPublicKeyInfo.bytes, signedBytes, _certCurveOid(c.cert), pssSalt); })
|
|
170
|
-
.then(function (ok) { return ok === true ? { ok: true, sid: sid, cert: c.der } : attempt(idx + 1); },
|
|
171
|
-
function (e) { lastErr = (e instanceof CmsError) ? e : _err("cms/verify-error", "the SignerInfo signature could not be evaluated", e); return attempt(idx + 1); });
|
|
172
182
|
}
|
|
173
|
-
return
|
|
183
|
+
return lastErr ? { ok: false, code: lastErr.code, sid: sid, cert: candidates[0].der, message: lastErr.message }
|
|
184
|
+
: { ok: false, sid: sid, cert: candidates[0].der };
|
|
174
185
|
}
|
|
175
186
|
|
|
176
187
|
// The cert's subjectKeyIdentifier extension value (the raw key id), or null.
|
|
@@ -255,14 +266,17 @@ function _resolvePss(paramsBytes) {
|
|
|
255
266
|
// Verify the signature over `signedBytes` with the signer certificate's public key under the
|
|
256
267
|
// resolved scheme + hash. Resolves true/false (a false verdict is a verdict); a structural
|
|
257
268
|
// fault throws a typed CmsError.
|
|
258
|
-
function _verifySignature(scheme, hashName, sigBytes, spki, signedBytes, curveOid, pssSalt) {
|
|
269
|
+
async function _verifySignature(scheme, hashName, sigBytes, spki, signedBytes, curveOid, pssSalt) {
|
|
270
|
+
// `await` throughout, never `.then`: a streamed content's untrusted iterator ran during the digest
|
|
271
|
+
// await and could replace Promise.prototype.then to answer a fabricated `true`; await does not
|
|
272
|
+
// dispatch through it.
|
|
259
273
|
if (scheme.kind === "rsa") {
|
|
260
|
-
|
|
261
|
-
|
|
274
|
+
var kRsa = await subtle.importKey("spki", spki, { name: "RSASSA-PKCS1-v1_5", hash: hashName }, false, ["verify"]);
|
|
275
|
+
return subtle.verify({ name: "RSASSA-PKCS1-v1_5" }, kRsa, sigBytes, signedBytes);
|
|
262
276
|
}
|
|
263
277
|
if (scheme.kind === "rsapss") {
|
|
264
|
-
|
|
265
|
-
|
|
278
|
+
var kPss = await subtle.importKey("spki", spki, { name: "RSA-PSS", hash: hashName }, false, ["verify"]);
|
|
279
|
+
return subtle.verify({ name: "RSA-PSS", saltLength: pssSalt }, kPss, sigBytes, signedBytes);
|
|
266
280
|
}
|
|
267
281
|
if (scheme.kind === "ec") {
|
|
268
282
|
var ec = EC_CURVE[curveOid];
|
|
@@ -270,23 +284,23 @@ function _verifySignature(scheme, hashName, sigBytes, spki, signedBytes, curveOi
|
|
|
270
284
|
// The ORDER-AWARE gate: r,s in [1, n-1] per FIPS 186-5 sec. 6.4.2 (rejecting an out-of-range r/s >= the
|
|
271
285
|
// curve order n, not only the r=s=0 forgery). A verifier that knows the curve MUST use it.
|
|
272
286
|
var raw = validator.sig.ecdsaDerToP1363(sigBytes, ec.curve, CmsError, "cms/bad-signature");
|
|
273
|
-
|
|
274
|
-
|
|
287
|
+
var kEc = await subtle.importKey("spki", spki, { name: "ECDSA", namedCurve: ec.curve }, false, ["verify"]);
|
|
288
|
+
return subtle.verify({ name: "ECDSA", hash: hashName }, kEc, raw, signedBytes);
|
|
275
289
|
}
|
|
276
290
|
if (scheme.kind === "mldsa" || scheme.kind === "slhdsa") {
|
|
277
291
|
// ML-DSA (RFC 9882) / SLH-DSA (RFC 9814): a one-shot post-quantum verify, pure mode, empty
|
|
278
292
|
// context -- no external signature hash and no Edwards-point validation (FIPS 204 sec. 5.3 /
|
|
279
293
|
// FIPS 205 sec. 10.3; node structurally validates the SPKI on import). The signature is raw and
|
|
280
294
|
// fixed-length (not ECDSA-DER).
|
|
281
|
-
|
|
282
|
-
|
|
295
|
+
var kPq = await subtle.importKey("spki", spki, { name: scheme.name }, false, ["verify"]);
|
|
296
|
+
return subtle.verify({ name: scheme.name }, kPq, sigBytes, signedBytes);
|
|
283
297
|
}
|
|
284
298
|
// EdDSA -- the WebCrypto name follows the signing key's SPKI OID (Ed25519 / Ed448). node/
|
|
285
299
|
// OpenSSL imports any Ed25519/Ed448 SPKI without validating the point, and a low-order (e.g.
|
|
286
300
|
// all-zeroes) key verifies a forged signature, so reject a non-full-order point first.
|
|
287
301
|
_requireValidEdPoint(spki, scheme.name);
|
|
288
|
-
|
|
289
|
-
|
|
302
|
+
var kEd = await subtle.importKey("spki", spki, { name: scheme.name }, false, ["verify"]);
|
|
303
|
+
return subtle.verify({ name: scheme.name }, kEd, sigBytes, signedBytes);
|
|
290
304
|
}
|
|
291
305
|
|
|
292
306
|
// The EdDSA signer key's point MUST be a valid, canonical, full-order Edwards point: reject an
|
|
@@ -313,7 +327,7 @@ function _certCurveOid(cert) {
|
|
|
313
327
|
function _decodeSignedAttrs(setOfBytes) {
|
|
314
328
|
var set = asn1.decode(setOfBytes);
|
|
315
329
|
if (set.tagClass !== "universal" || set.tagNumber !== asn1.TAGS.SET || !set.children) throw _err("cms/bad-signed-attrs", "signedAttrs is not a SET OF Attribute");
|
|
316
|
-
return set.children
|
|
330
|
+
return intrinsic.map(set.children, function (attr) {
|
|
317
331
|
if (attr.tagClass !== "universal" || attr.tagNumber !== asn1.TAGS.SEQUENCE || !attr.children || attr.children.length !== 2) throw _err("cms/bad-signed-attrs", "a signed Attribute is not a SEQUENCE { type, values }");
|
|
318
332
|
var valuesSet = attr.children[1];
|
|
319
333
|
if (valuesSet.tagClass !== "universal" || valuesSet.tagNumber !== asn1.TAGS.SET || !valuesSet.children) throw _err("cms/bad-signed-attrs", "a signed Attribute values field is not a SET OF");
|
|
@@ -327,23 +341,23 @@ function _decodeSignedAttrs(setOfBytes) {
|
|
|
327
341
|
// preimage is the countersigned signature octets, not the sec. 5.4 content preimage, and no
|
|
328
342
|
// content-type attribute is required or checked (_computeCountersigBytes). content/eContentType are
|
|
329
343
|
// unused in that mode.
|
|
330
|
-
function _verifyOne(si, content, eContentType, parsedCerts, csTarget) {
|
|
344
|
+
async function _verifyOne(si, content, eContentType, parsedCerts, csTarget, digestByName) {
|
|
331
345
|
// A composite ML-DSA SignerInfo (draft-ietf-lamps-cms-composite-sigs) is keyed by its composite
|
|
332
346
|
// OID, not a SIG_SCHEME name; intercept it before the classical dispatch (its verify has no
|
|
333
347
|
// single external signature hash, so it never routes through the SIG_SCHEME/sigHash gate below).
|
|
334
348
|
var composite = compositeSig.COMPOSITE_ALGS[si.signatureAlgorithm.oid];
|
|
335
|
-
if (composite) return _verifyComposite(si, composite, content, eContentType, parsedCerts, csTarget);
|
|
349
|
+
if (composite) return _verifyComposite(si, composite, content, eContentType, parsedCerts, csTarget, digestByName);
|
|
336
350
|
var scheme = SIG_SCHEME[si.signatureAlgorithm.name];
|
|
337
|
-
if (!scheme) return
|
|
351
|
+
if (!scheme) return _pResolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "unsupported signature algorithm " + JSON.stringify(si.signatureAlgorithm.name) });
|
|
338
352
|
var digestHash = SIG_HASH[si.digestAlgorithm.name];
|
|
339
353
|
// RSASSA-PSS takes its signature hash and salt length from the RSASSA-PSS-params, not the
|
|
340
354
|
// digestAlgorithm; a non-conformant parameter set is a fail-closed unsupported verdict.
|
|
341
355
|
var pss = scheme.kind === "rsapss" ? _resolvePss(si.signatureAlgorithm.parameters) : null;
|
|
342
|
-
if (scheme.kind === "rsapss" && !pss) return
|
|
356
|
+
if (scheme.kind === "rsapss" && !pss) return _pResolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "unsupported or non-conformant RSASSA-PSS parameters (RFC 4055)" });
|
|
343
357
|
// signatureAlgorithm is outside the signed preimage, so a forbidden/missing parameters field
|
|
344
358
|
// (a present parameter on ECDSA/EdDSA, a non-NULL or absent one on RSA) must fail closed --
|
|
345
359
|
// otherwise it is a parser differential a caller trusting the verdict inherits (RFC 5754).
|
|
346
|
-
if (scheme.params && !_algParamsOk(scheme.params, si.signatureAlgorithm.parameters)) return
|
|
360
|
+
if (scheme.params && !_algParamsOk(scheme.params, si.signatureAlgorithm.parameters)) return _pResolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "the " + si.signatureAlgorithm.name + " signature algorithm parameters must be " + (scheme.params === "null" ? "DER NULL (RFC 4055)" : "absent (RFC 5758/8410)") });
|
|
347
361
|
// The digestAlgorithm parameters, when present, MUST be a DER NULL (RFC 5754 sec. 2 accepts
|
|
348
362
|
// absent OR NULL; a present non-NULL is malformed and, being outside the signed preimage,
|
|
349
363
|
// must fail closed like the signatureAlgorithm parameters above). EXCEPT for an ML-DSA
|
|
@@ -351,39 +365,40 @@ function _verifyOne(si, content, eContentType, parsedCerts, csTarget) {
|
|
|
351
365
|
// has no meaning there and MUST be ignored, so neither its parameters nor its name may reject.
|
|
352
366
|
var dp = si.digestAlgorithm.parameters;
|
|
353
367
|
var mldsaNoAttrs = scheme.kind === "mldsa" && !si.signedAttrsBytes;
|
|
354
|
-
if (!mldsaNoAttrs && dp !== null && dp !== undefined && !_isDerNull(dp)) return
|
|
368
|
+
if (!mldsaNoAttrs && dp !== null && dp !== undefined && !_isDerNull(dp)) return _pResolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "the " + si.digestAlgorithm.name + " digest algorithm parameters must be absent or DER NULL (RFC 5754 sec. 2)" });
|
|
355
369
|
// A SHAKE digest algorithm identifier (id-shake128 / id-shake256) carries NO parameters at all --
|
|
356
370
|
// not even a DER NULL (RFC 8702 sec. 3.1) -- so for a one-shot PQC SignerInfo (ML-DSA per RFC 9882,
|
|
357
371
|
// SLH-DSA per RFC 9814 sec. 4, both citing RFC 8702) with signed attributes a present SHAKE
|
|
358
372
|
// parameter fails closed. The SHA-2 digests (id-sha256/384/512) follow RFC 5754, which requires a
|
|
359
373
|
// verifier to accept absent OR NULL (the generic rule above); the RFCs' omit-on-encode rule binds
|
|
360
374
|
// the signer, not the verifier, so a SHA-2 NULL is not tightened here.
|
|
361
|
-
if ((scheme.kind === "mldsa" || scheme.kind === "slhdsa") && si.signedAttrsBytes && (si.digestAlgorithm.name === "shake128" || si.digestAlgorithm.name === "shake256") && dp !== null && dp !== undefined) return
|
|
375
|
+
if ((scheme.kind === "mldsa" || scheme.kind === "slhdsa") && si.signedAttrsBytes && (si.digestAlgorithm.name === "shake128" || si.digestAlgorithm.name === "shake256") && dp !== null && dp !== undefined) return _pResolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "a SHAKE digestAlgorithm carries no parameters (RFC 8702 sec. 3.1)" });
|
|
362
376
|
var sigHash = pss ? pss.hash : (scheme.hash || digestHash);
|
|
363
377
|
// The signature hash is required for every non-EdDSA scheme (EdDSA hashes internally);
|
|
364
378
|
// the content digest is required whenever signed attributes are present, for every scheme
|
|
365
379
|
// (the message-digest attribute is computed under digestAlgorithm). Either gap is a
|
|
366
380
|
// fail-closed unsupported-algorithm verdict, never a foreign-domain throw from the digest.
|
|
367
|
-
if ((scheme.kind !== "eddsa" && scheme.kind !== "mldsa" && scheme.kind !== "slhdsa" && !sigHash) || (si.signedAttrsBytes && !_supportedDigest(si.digestAlgorithm.name))) return
|
|
381
|
+
if ((scheme.kind !== "eddsa" && scheme.kind !== "mldsa" && scheme.kind !== "slhdsa" && !sigHash) || (si.signedAttrsBytes && !_supportedDigest(si.digestAlgorithm.name))) return _pResolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "unsupported digest algorithm " + JSON.stringify(si.digestAlgorithm.name) });
|
|
368
382
|
// ML-DSA (RFC 9882 sec. 3.3): with signed attributes present, the message-digest algorithm MUST
|
|
369
383
|
// meet the parameter set's security strength -- a below-strength digest is the weaker link and is
|
|
370
384
|
// rejected fail-closed (the RFC's "verifiers MAY reject", taken by the strict-verifier posture).
|
|
371
385
|
// With signed attributes absent the digestAlgorithm has no meaning (sec. 3.3) and is not checked.
|
|
372
|
-
if (scheme.kind === "mldsa" && si.signedAttrsBytes && !_mldsaDigestSuitable(scheme.name, si.digestAlgorithm.name)) return
|
|
386
|
+
if (scheme.kind === "mldsa" && si.signedAttrsBytes && !_mldsaDigestSuitable(scheme.name, si.digestAlgorithm.name)) return _pResolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "the " + si.digestAlgorithm.name + " message digest is below the security strength of " + scheme.name + " (RFC 9882 sec. 3.3)" });
|
|
373
387
|
// RFC 9814 sec. 4: an SLH-DSA SignerInfo's message-digest algorithm is the one paired with the
|
|
374
388
|
// parameter set (the sec. 4 list, always at least twice the tree-hash size); the signer emits it
|
|
375
389
|
// and this strict verifier requires it, so a digest that does not match the set's paired hash fails
|
|
376
390
|
// closed -- otherwise a signer could compute the message-digest attribute under a weaker hash the
|
|
377
391
|
// set does not pair. Gated on signed attributes (absent -> the digest is not consulted, sec. 4).
|
|
378
|
-
if (scheme.kind === "slhdsa" && si.signedAttrsBytes && si.digestAlgorithm.name !== scheme.digest) return
|
|
392
|
+
if (scheme.kind === "slhdsa" && si.signedAttrsBytes && si.digestAlgorithm.name !== scheme.digest) return _pResolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "SLH-DSA " + scheme.name + " requires the " + scheme.digest + " message digest (RFC 9814 sec. 4)" });
|
|
379
393
|
var signers = _findSignerCerts(si.sid, parsedCerts);
|
|
380
|
-
if (!signers.length) return
|
|
394
|
+
if (!signers.length) return _pResolve({ ok: false, code: "cms/signer-cert-not-found", sid: si.sid, message: "no certificate matches this SignerInfo's signer identifier" });
|
|
381
395
|
var sigBytes = _toBuf(si.signature, "the SignerInfo signature");
|
|
382
396
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
397
|
+
// `await`, not `.then`: see _verifyAgainstCandidates -- a streamed content's iterator could replace
|
|
398
|
+
// Promise.prototype.then to skip the signed-bytes computation and fabricate a passing verdict.
|
|
399
|
+
var signedBytes = await (csTarget != null ? _computeCountersigBytes(si, csTarget) : _computeSignedBytes(si, content, eContentType, digestByName));
|
|
400
|
+
if (signedBytes && signedBytes.mismatch) return { ok: false, code: signedBytes.mismatch.code, sid: si.sid, cert: signers[0].der, message: signedBytes.mismatch.message };
|
|
401
|
+
return _verifyAgainstCandidates(scheme, sigHash, sigBytes, signedBytes, si.sid, signers, pss ? pss.saltLength : 0, scheme.sameKeyOid ? oid.byName(si.signatureAlgorithm.name) : null);
|
|
387
402
|
}
|
|
388
403
|
|
|
389
404
|
// The exact bytes a countersignature covers (RFC 5652 sec. 11.4): the countersigned SignerInfo's
|
|
@@ -392,88 +407,102 @@ function _verifyOne(si, content, eContentType, parsedCerts, csTarget) {
|
|
|
392
407
|
// covers the re-tagged SignedAttributes ([0] -> universal SET OF) -- the identical transform the
|
|
393
408
|
// top-level signer uses, minus the content-type attribute (which sec. 11.4 FORBIDS). Resolves the
|
|
394
409
|
// signed bytes, or { mismatch } when the message-digest disagrees.
|
|
395
|
-
function _computeCountersigBytes(si, csTarget) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
});
|
|
410
|
+
async function _computeCountersigBytes(si, csTarget) {
|
|
411
|
+
// async, not `_pResolve().then`: a synchronous throw still becomes a rejection, and the digest await
|
|
412
|
+
// below dispatches through no caller-replaceable Promise.prototype.then (see _verifyAgainstCandidates).
|
|
413
|
+
if (!si.signedAttrsBytes) return csTarget; // no signed attributes: sign over the target signature octets directly
|
|
414
|
+
var reTagged = intrinsic.bufferFrom(si.signedAttrsBytes);
|
|
415
|
+
reTagged[0] = 0x31; // [0] IMPLICIT -> universal SET OF
|
|
416
|
+
var attrs;
|
|
417
|
+
try {
|
|
418
|
+
attrs = _decodeSignedAttrs(reTagged);
|
|
419
|
+
} catch (e) { if (e instanceof CmsError) throw e; throw _err("cms/bad-signed-attrs", "the countersignature signedAttrs is not a valid SET OF Attribute", e); }
|
|
420
|
+
// sec. 11.4: a content-type attribute MUST NOT appear (the parser already rejects it at parse, but
|
|
421
|
+
// verify never relies on the parse alone: a parsed-object input could carry one).
|
|
422
|
+
if (intrinsic.filter(attrs, function (a) { return a.type === OID_CONTENT_TYPE; }).length) throw _err("cms/misplaced-attr", "a countersignature must not carry a content-type attribute (RFC 5652 sec. 11.4)");
|
|
423
|
+
var mdAttr = intrinsic.filter(attrs, function (a) { return a.type === OID_MESSAGE_DIGEST; })[0];
|
|
424
|
+
if (!mdAttr || mdAttr.values.length !== 1) throw _err("cms/bad-signed-attrs", "a countersignature's signedAttrs must carry exactly one message-digest attribute (RFC 5652 sec. 11.4)");
|
|
425
|
+
var declared;
|
|
426
|
+
try {
|
|
427
|
+
declared = asn1.read.octetString(mdAttr.values[0]);
|
|
428
|
+
} catch (e) { throw _err("cms/bad-signed-attrs", "the message-digest attribute value is not an OCTET STRING", e); }
|
|
429
|
+
var d = await _computeDigest(si.digestAlgorithm.name, csTarget);
|
|
430
|
+
if (!intrinsic.bufferEquals(d, declared)) return { mismatch: { code: "cms/message-digest-mismatch", message: "the countersignature message-digest does not match the countersigned signature" } };
|
|
431
|
+
return reTagged;
|
|
418
432
|
}
|
|
419
433
|
|
|
420
434
|
// The exact bytes the signature covers (RFC 5652 sec. 5.4), shared by the classical and composite
|
|
421
435
|
// verify paths so both bind the message-digest / content-type checks to the VERIFIED preimage (not
|
|
422
436
|
// the caller-mutable parsed si.signedAttrs). Resolves the signed bytes, or { mismatch } when a
|
|
423
437
|
// content-type / message-digest attribute disagrees.
|
|
424
|
-
function _computeSignedBytes(si, content, eContentType) {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
"re-presented as one over content (RFC 5652 sec. 5.4); sign such content WITH signed attributes",
|
|
440
|
-
} };
|
|
441
|
-
}
|
|
442
|
-
return content;
|
|
438
|
+
async function _computeSignedBytes(si, content, eContentType, digestByName) {
|
|
439
|
+
// async, not `_pResolve().then`: a synchronous throw still becomes a rejection, and every digest step
|
|
440
|
+
// dispatches through no caller-replaceable Promise.prototype.then (see _verifyAgainstCandidates).
|
|
441
|
+
if (!si.signedAttrsBytes) {
|
|
442
|
+
// No signed attributes: the signature is over the content itself (RFC 5652 sec. 5.4). A streamed
|
|
443
|
+
// content is not retained, so there are no content bytes to verify that signature against; this
|
|
444
|
+
// is a per-signer verdict, not a whole-message abort, so the other signers of a mixed message
|
|
445
|
+
// still verify (the same shape the buffered path gives every per-signer condition). `digestByName`
|
|
446
|
+
// is the streaming marker: an object (possibly empty) when streamed, null when buffered.
|
|
447
|
+
if (digestByName) {
|
|
448
|
+
return { mismatch: {
|
|
449
|
+
code: "cms/streamed-content-unverifiable",
|
|
450
|
+
message: "a SignerInfo that signs the content directly (no signed attributes) cannot be " +
|
|
451
|
+
"verified against a streamed content, whose bytes are not retained; pass opts.content as a Buffer",
|
|
452
|
+
} };
|
|
443
453
|
}
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
//
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
454
|
+
// Signing content directly is also exactly what a stripped-attributes forgery looks like, because
|
|
455
|
+
// a CMS signature does not commit to whether attributes were present -- so a signature made over a
|
|
456
|
+
// SignedAttributes block re-presented as one made over content verifies, and with no attributes
|
|
457
|
+
// there is no message-digest or content-type attribute left to disagree. Refuse the shape: content
|
|
458
|
+
// that parses as a SignedAttributes block cannot be told apart from that forgery, and it is not
|
|
459
|
+
// the verifier's place to guess which one it is holding.
|
|
460
|
+
if (cms.looksLikeSignedAttributes(content)) {
|
|
461
|
+
return { mismatch: {
|
|
462
|
+
code: "cms/ambiguous-content",
|
|
463
|
+
message: "the content of a SignerInfo with no signed attributes is itself an encoded " +
|
|
464
|
+
"SignedAttributes block, which is indistinguishable from a signature over attributes " +
|
|
465
|
+
"re-presented as one over content (RFC 5652 sec. 5.4); sign such content WITH signed attributes",
|
|
466
|
+
} };
|
|
467
|
+
}
|
|
468
|
+
return content;
|
|
469
|
+
}
|
|
470
|
+
// With signed attributes: decode them from the EXACT bytes the signature covers, namely the
|
|
471
|
+
// SignedAttributes SET OF with the on-wire [0] IMPLICIT tag replaced by a universal SET OF
|
|
472
|
+
// (RFC 5652 sec. 5.4). The content-type / message-digest checks then bind the same bytes
|
|
473
|
+
// that are verified. Reading the caller-mutable parsed si.signedAttrs instead would let a
|
|
474
|
+
// parsed-object input desync the checked attributes from the verified preimage.
|
|
475
|
+
var reTagged = intrinsic.bufferFrom(si.signedAttrsBytes);
|
|
476
|
+
reTagged[0] = 0x31; // [0] IMPLICIT -> universal SET OF
|
|
477
|
+
var attrs;
|
|
478
|
+
try {
|
|
479
|
+
attrs = _decodeSignedAttrs(reTagged);
|
|
480
|
+
} catch (e) { if (e instanceof CmsError) throw e; throw _err("cms/bad-signed-attrs", "signedAttrs is not a valid SET OF Attribute", e); }
|
|
481
|
+
// The content-type attribute MUST be present, single-valued, and equal the eContentType
|
|
482
|
+
// (RFC 5652 sec. 5.3).
|
|
483
|
+
var ctAttr = intrinsic.filter(attrs, function (a) { return a.type === OID_CONTENT_TYPE; });
|
|
484
|
+
if (ctAttr.length !== 1 || ctAttr[0].values.length !== 1) throw _err("cms/bad-signed-attrs", "signedAttrs must carry exactly one content-type attribute (RFC 5652 sec. 5.3)");
|
|
485
|
+
var ctOid;
|
|
486
|
+
try {
|
|
487
|
+
ctOid = asn1.read.oid(ctAttr[0].values[0]);
|
|
488
|
+
} catch (e) { throw _err("cms/bad-signed-attrs", "the content-type attribute value is not an OBJECT IDENTIFIER", e); }
|
|
489
|
+
if (ctOid !== eContentType) return { mismatch: { code: "cms/content-type-mismatch", message: "the content-type signed attribute does not match the SignedData eContentType (RFC 5652 sec. 5.3)" } };
|
|
490
|
+
// The message-digest attribute MUST be present, single-valued, and equal the digest of the
|
|
491
|
+
// content (RFC 5652 sec. 5.4).
|
|
492
|
+
var mdAttr = intrinsic.filter(attrs, function (a) { return a.type === OID_MESSAGE_DIGEST; })[0];
|
|
493
|
+
if (!mdAttr || mdAttr.values.length !== 1) throw _err("cms/bad-signed-attrs", "signedAttrs must carry exactly one message-digest attribute (RFC 5652 sec. 5.4)");
|
|
494
|
+
var declared;
|
|
495
|
+
try {
|
|
496
|
+
declared = asn1.read.octetString(mdAttr.values[0]);
|
|
497
|
+
} catch (e) { throw _err("cms/bad-signed-attrs", "the message-digest attribute value is not an OCTET STRING", e); }
|
|
498
|
+
// Over a buffered content the digest is computed here; over a streamed content it was computed once,
|
|
499
|
+
// incrementally, before any signer was verified, and looked up by algorithm.
|
|
500
|
+
var d = digestByName ? digestByName[si.digestAlgorithm.name] : await _computeDigest(si.digestAlgorithm.name, content);
|
|
501
|
+
// Captured equality, not d.equals: over a streamed content the untrusted iterator's next() ran during
|
|
502
|
+
// the digest await and could have replaced Buffer.prototype.equals with one that always answers true,
|
|
503
|
+
// passing this message-digest check for content that was never signed (CWE-347).
|
|
504
|
+
if (!intrinsic.bufferEquals(d, declared)) return { mismatch: { code: "cms/message-digest-mismatch", message: "the message-digest attribute does not match the content digest" } };
|
|
505
|
+
return reTagged;
|
|
477
506
|
}
|
|
478
507
|
|
|
479
508
|
// Verify a composite ML-DSA SignerInfo (draft-ietf-lamps-cms-composite-sigs). The composite
|
|
@@ -484,14 +513,14 @@ function _computeSignedBytes(si, content, eContentType) {
|
|
|
484
513
|
// pre-hash. It reuses the sec. 5.4 preimage + message-digest binding UNCHANGED, then dispatches
|
|
485
514
|
// compositeVerify per candidate certificate (both components must verify, with no AND->OR
|
|
486
515
|
// downgrade).
|
|
487
|
-
function _verifyComposite(si, comp, content, eContentType, parsedCerts, csTarget) {
|
|
516
|
+
async function _verifyComposite(si, comp, content, eContentType, parsedCerts, csTarget, digestByName) {
|
|
488
517
|
if (si.signatureAlgorithm.parameters !== null && si.signatureAlgorithm.parameters !== undefined) {
|
|
489
|
-
return
|
|
518
|
+
return _pResolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "the composite signatureAlgorithm parameters must be absent (draft-ietf-lamps-cms-composite-sigs sec. 3.4)" });
|
|
490
519
|
}
|
|
491
520
|
// The 2 arms Node's WebCrypto surface cannot verify (the brainpool curves) fail closed --
|
|
492
521
|
// never a silent single-component accept (the AND->OR downgrade this feature prevents).
|
|
493
522
|
if (comp.trad.unsupported) {
|
|
494
|
-
return
|
|
523
|
+
return _pResolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "composite " + comp.name + ": " + comp.trad.unsupported });
|
|
495
524
|
}
|
|
496
525
|
// draft sec. 3.4: for a composite SignerInfo the digestAlgorithm parameters MUST be OMITTED
|
|
497
526
|
// (id-sha256/id-sha512 and id-shake256). That is stricter than the generic RFC 5754 absent-OR-NULL
|
|
@@ -500,7 +529,7 @@ function _verifyComposite(si, comp, content, eContentType, parsedCerts, csTarget
|
|
|
500
529
|
// (it is outside the signed preimage, so a parser differential must not verify).
|
|
501
530
|
var dp = si.digestAlgorithm.parameters;
|
|
502
531
|
if (dp !== null && dp !== undefined) {
|
|
503
|
-
return
|
|
532
|
+
return _pResolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "the composite " + si.digestAlgorithm.name + " digestAlgorithm parameters must be omitted (draft-ietf-lamps-cms-composite-sigs sec. 3.4)" });
|
|
504
533
|
}
|
|
505
534
|
// sec. 3.4 / sec. 5 (the one CMS-specific MUST the X.509 path does not cover): the SignerInfo
|
|
506
535
|
// digestAlgorithm MUST equal the arm's pre-hash (Table 1) -- compositeVerify takes the pre-hash
|
|
@@ -508,15 +537,15 @@ function _verifyComposite(si, comp, content, eContentType, parsedCerts, csTarget
|
|
|
508
537
|
// message-digest attribute under the WRONG algorithm. The sec. 5 SHOULD-reject is taken
|
|
509
538
|
// fail-closed; the MAY-verify-anyway leniency is deliberately not taken.
|
|
510
539
|
if (si.digestAlgorithm.name !== comp.phCms) {
|
|
511
|
-
return
|
|
540
|
+
return _pResolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "the SignerInfo digestAlgorithm " + JSON.stringify(si.digestAlgorithm.name) + " is not the composite " + comp.name + " pre-hash " + JSON.stringify(comp.phCms) + " (draft-ietf-lamps-cms-composite-sigs sec. 3.4)" });
|
|
512
541
|
}
|
|
513
542
|
var signers = _findSignerCerts(si.sid, parsedCerts);
|
|
514
|
-
if (!signers.length) return
|
|
543
|
+
if (!signers.length) return _pResolve({ ok: false, code: "cms/signer-cert-not-found", sid: si.sid, message: "no certificate matches this SignerInfo's signer identifier" });
|
|
515
544
|
var sigBytes = _toBuf(si.signature, "the SignerInfo signature");
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
545
|
+
// `await`, not `.then`: see _verifyAgainstCandidates.
|
|
546
|
+
var signedBytes = await (csTarget != null ? _computeCountersigBytes(si, csTarget) : _computeSignedBytes(si, content, eContentType, digestByName));
|
|
547
|
+
if (signedBytes && signedBytes.mismatch) return { ok: false, code: signedBytes.mismatch.code, sid: si.sid, cert: signers[0].der, message: signedBytes.mismatch.message };
|
|
548
|
+
return _verifyCompositeAgainstCandidates(comp, sigBytes, signedBytes, si.sid, signers, si.signatureAlgorithm.oid);
|
|
520
549
|
}
|
|
521
550
|
|
|
522
551
|
// Dispatch compositeVerify against each matching candidate; the signer is valid iff one candidate
|
|
@@ -524,26 +553,21 @@ function _verifyComposite(si, comp, content, eContentType, parsedCerts, csTarget
|
|
|
524
553
|
// OID is skipped with a precise verdict (RFC 9814 sec. 4 key<->signature agreement); a candidate that
|
|
525
554
|
// returns a structural fault code (bad split length, unsupported) falls through, its code surfaced if
|
|
526
555
|
// none verifies. A clean both-components-checked failure (the AND-downgrade) is a code-less false.
|
|
527
|
-
function _verifyCompositeAgainstCandidates(comp, sigBytes, signedBytes, sid, candidates, expectedKeyOid) {
|
|
556
|
+
async function _verifyCompositeAgainstCandidates(comp, sigBytes, signedBytes, sid, candidates, expectedKeyOid) {
|
|
528
557
|
var lastErr = null;
|
|
529
|
-
|
|
530
|
-
if (idx >= candidates.length) {
|
|
531
|
-
return lastErr ? { ok: false, code: lastErr.code, sid: sid, cert: candidates[0].der, message: lastErr.message }
|
|
532
|
-
: { ok: false, sid: sid, cert: candidates[0].der };
|
|
533
|
-
}
|
|
558
|
+
for (var idx = 0; idx < candidates.length; idx++) {
|
|
534
559
|
var c = candidates[idx];
|
|
535
560
|
if (c.cert.subjectPublicKeyInfo.algorithm.oid !== expectedKeyOid) {
|
|
536
561
|
lastErr = _err("cms/unsupported-algorithm", "the signer certificate public-key algorithm does not match the SignerInfo signatureAlgorithm");
|
|
537
|
-
|
|
562
|
+
continue;
|
|
538
563
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
return attempt(idx + 1);
|
|
544
|
-
});
|
|
564
|
+
// `await`, not `.then`: see _verifyAgainstCandidates.
|
|
565
|
+
var r = await compositeSig.compositeVerify(c.cert.subjectPublicKeyInfo.bytes, sigBytes, signedBytes, comp, CmsError, "cms/unsupported-algorithm", "cms/bad-signature");
|
|
566
|
+
if (r.ok === true) return { ok: true, sid: sid, cert: c.der };
|
|
567
|
+
if (r.code) lastErr = (r.error instanceof CmsError) ? r.error : _err(r.code, r.error && r.error.message ? r.error.message : "the composite signature could not be evaluated");
|
|
545
568
|
}
|
|
546
|
-
return
|
|
569
|
+
return lastErr ? { ok: false, code: lastErr.code, sid: sid, cert: candidates[0].der, message: lastErr.message }
|
|
570
|
+
: { ok: false, sid: sid, cert: candidates[0].der };
|
|
547
571
|
}
|
|
548
572
|
|
|
549
573
|
// The decoded unsigned attributes of a SignerInfo, surfaced UNAUTHENTICATED (they are outside
|
|
@@ -551,7 +575,7 @@ function _verifyCompositeAgainstCandidates(comp, sigBytes, signedBytes, sid, can
|
|
|
551
575
|
// timestamp token or inspect a countersignature attribute. They are never counted toward a
|
|
552
576
|
// signer's ok / res.valid.
|
|
553
577
|
function _surfaceUnsignedAttrs(si) {
|
|
554
|
-
return (si.unsignedAttrs || []
|
|
578
|
+
return intrinsic.map(si.unsignedAttrs || [], function (a) {
|
|
555
579
|
return { type: a.type, typeName: oid.name(a.type) || null, values: a.values };
|
|
556
580
|
});
|
|
557
581
|
}
|
|
@@ -566,19 +590,26 @@ function _surfaceUnsignedAttrs(si) {
|
|
|
566
590
|
// the FINITE parsed structure: each nested countersignature value is a sub-encoding of its parent,
|
|
567
591
|
// and the strict decoder already bounds total nesting by C.LIMITS.DER_MAX_DEPTH at parse
|
|
568
592
|
// (CWE-834/770), so a hostile deep chain fails closed before verify.
|
|
569
|
-
function _verifyCountersignatures(si, parsedCerts) {
|
|
593
|
+
async function _verifyCountersignatures(si, parsedCerts) {
|
|
570
594
|
// si.signature is always a Buffer from the strict parser (and from walkCountersignature on the
|
|
571
595
|
// recursive path), so _toBuf is a pass-through here. The SignerInfo is already well-formed.
|
|
572
596
|
var targetSig = _toBuf(si.signature, "the countersigned signature");
|
|
573
597
|
var values = [];
|
|
574
|
-
|
|
598
|
+
// Captured enumeration: over a streamed content the untrusted iterator's next() ran during the
|
|
599
|
+
// digest await and could replace Array.prototype.forEach / push to drop a failing countersignature.
|
|
600
|
+
intrinsic.forEach(si.unsignedAttrs || [], function (a) {
|
|
575
601
|
if (a.type !== OID_COUNTERSIGNATURE) return;
|
|
576
|
-
a.values
|
|
602
|
+
intrinsic.forEach(a.values, function (v) { intrinsic.push(values, v && v.bytes ? v.bytes : v); });
|
|
577
603
|
});
|
|
578
|
-
|
|
604
|
+
// Sequential, started only as awaited (see _verify): not Promise.all (its element handling dispatches
|
|
605
|
+
// through the replaceable Promise.prototype.then), and not started-all-then-awaited (an early
|
|
606
|
+
// rejection would leave a later one unobserved).
|
|
607
|
+
var out = [];
|
|
608
|
+
for (var _cp = 0; _cp < values.length; _cp++) intrinsic.push(out, await _verifyOneCountersig(values[_cp], targetSig, parsedCerts));
|
|
609
|
+
return out;
|
|
579
610
|
}
|
|
580
611
|
|
|
581
|
-
function _verifyOneCountersig(vDer, targetSig, parsedCerts) {
|
|
612
|
+
async function _verifyOneCountersig(vDer, targetSig, parsedCerts) {
|
|
582
613
|
var csSi;
|
|
583
614
|
// Coverage residual, and deliberately kept. Every value reaching here was already walked by the
|
|
584
615
|
// decoder, which validates an id-countersignature by content and not by the attribute type. A
|
|
@@ -588,17 +619,16 @@ function _verifyOneCountersig(vDer, targetSig, parsedCerts) {
|
|
|
588
619
|
// whole instead of surfaced as one failed countersignature.
|
|
589
620
|
// cms-verify.test.js pins that through the shipped verbs; this stays because the walk is also
|
|
590
621
|
// reachable recursively and a backstop that returns a NEGATIVE verdict costs nothing.
|
|
591
|
-
try { csSi = cms.walkCountersignature(asn1.decode(
|
|
592
|
-
catch (e) { return
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
});
|
|
622
|
+
try { csSi = cms.walkCountersignature(asn1.decode(intrinsic.isBuffer(vDer) ? vDer : intrinsic.bufferFrom(vDer))); }
|
|
623
|
+
catch (e) { return { ok: false, code: (e instanceof CmsError ? e.code : "cms/bad-countersignature"), message: e && e.message }; }
|
|
624
|
+
// `await`, not `.then`: see _verifyAgainstCandidates.
|
|
625
|
+
var verdict = await _verifyOne(csSi, targetSig, null, parsedCerts, targetSig);
|
|
626
|
+
var nested = await _verifyCountersignatures(csSi, parsedCerts);
|
|
627
|
+
var node = { ok: verdict.ok, sid: verdict.sid, cert: verdict.cert, digestAlgorithm: csSi.digestAlgorithm.name };
|
|
628
|
+
if (verdict.code) node.code = verdict.code;
|
|
629
|
+
node.unsignedAttrs = _surfaceUnsignedAttrs(csSi);
|
|
630
|
+
node.countersignatures = nested;
|
|
631
|
+
return node;
|
|
602
632
|
}
|
|
603
633
|
|
|
604
634
|
/**
|
|
@@ -646,8 +676,11 @@ function _verifyOneCountersig(vDer, targetSig, parsedCerts) {
|
|
|
646
676
|
* ML-DSA (draft-ietf-lamps-cms-composite-sigs), which pairs ML-DSA with a traditional
|
|
647
677
|
* RSA / ECDSA / EdDSA and verifies only when both components pass (never an AND-to-OR downgrade).
|
|
648
678
|
*
|
|
649
|
-
* @opts content The detached content
|
|
650
|
-
*
|
|
679
|
+
* @opts content The detached content when the SignedData carries no encapsulated eContent, and
|
|
680
|
+
* required for a detached signature. A `Buffer`, or an async iterable of byte chunks
|
|
681
|
+
* to verify a large payload without holding it in memory. The streamed form is hashed
|
|
682
|
+
* once for every signer, so every signer must carry signed attributes; a signer that
|
|
683
|
+
* signs the content directly needs the content as a `Buffer`.
|
|
651
684
|
* @opts certs Extra signer certificates (an array of DER `Buffer`s) to match against, in
|
|
652
685
|
* addition to the certificates embedded in the SignedData.
|
|
653
686
|
* @opts trustAnchors The roots the caller accepts (DER `Buffer`s or anchor tuples). Supplying
|
|
@@ -685,6 +718,45 @@ function _snapshotIfBytes(input, label) {
|
|
|
685
718
|
return input;
|
|
686
719
|
}
|
|
687
720
|
|
|
721
|
+
// For a streamed (async-iterable) detached content: hash the stream ONCE, incrementally, under every
|
|
722
|
+
// distinct SUPPORTED SignerInfo digest algorithm, returning { <digestName>: Buffer } (streaming), or
|
|
723
|
+
// null (not streaming; digest per-signer over the buffered content). No per-signer condition aborts
|
|
724
|
+
// the whole message here, matching the buffered path's per-signer verdicts: a content-only signer
|
|
725
|
+
// (no signed attributes, whose signature is over content a stream does not retain) and an unsupported
|
|
726
|
+
// digest each contribute no name and are verdicted per-signer by _verifyOne / _computeSignedBytes.
|
|
727
|
+
// An empty map is still the streaming marker, so those per-signer verdicts are produced. `streamSource`
|
|
728
|
+
// is the lazy stream classified at the door (null when not streaming, buffered per-signer); its
|
|
729
|
+
// iterator is acquired only if a digest is actually needed, so an all-content-only message never opens it.
|
|
730
|
+
async function _resolveContentDigests(parsed, streamSource) {
|
|
731
|
+
if (streamSource == null) return null;
|
|
732
|
+
var names = [], seen = {};
|
|
733
|
+
intrinsic.forEach(parsed.signerInfos, function (si) {
|
|
734
|
+
var dn = si.digestAlgorithm.name;
|
|
735
|
+
if (si.signedAttrsBytes && _supportedDigest(dn) && !seen[dn]) { seen[dn] = 1; intrinsic.push(names, dn); }
|
|
736
|
+
});
|
|
737
|
+
// No signer needs a streamed digest (all content-only or unsupported): nothing to hash, but the
|
|
738
|
+
// empty map still marks streaming mode so each signer gets its per-signer verdict.
|
|
739
|
+
if (!names.length) return {};
|
|
740
|
+
// `await`, not `.then`: this is where the UNTRUSTED stream is consumed, so the very next promise step
|
|
741
|
+
// must not dispatch through a Promise.prototype.then the stream may have replaced.
|
|
742
|
+
var ds;
|
|
743
|
+
try {
|
|
744
|
+
ds = await subtle.digestStream(intrinsic.map(names, function (n) { return DIGEST_HASH[n]; }), streamSource);
|
|
745
|
+
} catch (e) {
|
|
746
|
+
// The engine reports malformed streamed content in its own domain; report it in this verb's domain
|
|
747
|
+
// instead, exactly as a buffered content's bad bytes are, rather than leaking the engine code. A
|
|
748
|
+
// non-byte chunk is webcrypto/data; a source whose async-iterator protocol is malformed (a
|
|
749
|
+
// non-callable next, or a factory returning a non-iterator) is webcrypto/syntax. The digest
|
|
750
|
+
// algorithm names are this module's own, so webcrypto/syntax here is never an algorithm fault.
|
|
751
|
+
if (e && e.code === "webcrypto/data") throw _err("cms/bad-input", "a streamed content chunk is not a byte source", e);
|
|
752
|
+
if (e && e.code === "webcrypto/syntax") throw _err("cms/bad-input", "the streamed content is not a valid async iterable of byte chunks", e);
|
|
753
|
+
throw e;
|
|
754
|
+
}
|
|
755
|
+
var byName = {};
|
|
756
|
+
intrinsic.forEach(names, function (n, i) { byName[n] = intrinsic.bufferFrom(ds[i]); });
|
|
757
|
+
return byName;
|
|
758
|
+
}
|
|
759
|
+
|
|
688
760
|
// Every option pki.cms.verify reads. Adding one here is the only way to make it accepted, so a
|
|
689
761
|
// capability cannot arrive with its option silently ignored at this boundary.
|
|
690
762
|
var _VERIFY_OPTS = { certs: 1, content: 1, trustAnchors: 1, time: 1, requiredEku: 1, checkPurpose: 1 };
|
|
@@ -696,7 +768,7 @@ function verify(input, opts) {
|
|
|
696
768
|
return guard.async.deferred(function () { return _verify(input, opts); });
|
|
697
769
|
}
|
|
698
770
|
|
|
699
|
-
function _verify(input, opts) {
|
|
771
|
+
async function _verify(input, opts) {
|
|
700
772
|
opts = guard.identifier.optionsObject(opts, _err, "cms/bad-input", "pki.cms.verify options");
|
|
701
773
|
// An unrecognized option is refused, not swallowed. This is what kept the missing trust seam
|
|
702
774
|
// silent: a caller writing `trustAnchors` before it existed -- or `trustAnchor` now -- got a
|
|
@@ -720,15 +792,43 @@ function _verify(input, opts) {
|
|
|
720
792
|
return cms.parse(_snapshotIfBytes(bytes, "pki.cms.verify"));
|
|
721
793
|
}, _err, "cms/bad-input", "the SignedData");
|
|
722
794
|
if (!Array.isArray(parsed.signerInfos)) throw _err("cms/bad-input", "input is not a CMS SignedData");
|
|
795
|
+
// The trust configuration is snapshotted HERE, before opts.content is read at all. A streamed
|
|
796
|
+
// content's Symbol.asyncIterator accessor runs the moment asyncStreamOf reads it, one turn before a
|
|
797
|
+
// snapshot taken further down would run; from that accessor a caller can replace a global the snapshot
|
|
798
|
+
// itself relies on -- the Date it defaults the validation instant from, or the Array.isArray /
|
|
799
|
+
// Array.prototype.map / slice it copies the anchors and requiredEku with -- and steer the trust
|
|
800
|
+
// decision. Taken before the content object is touched, every value the snapshot reads comes from a
|
|
801
|
+
// pristine global. It also stays caller-owned across the later await: the array cannot be re-pointed,
|
|
802
|
+
// an anchor's DER rewritten, or the instant moved, once copied -- the same defense the input and the
|
|
803
|
+
// certificates already have.
|
|
804
|
+
var trustCfg = _snapshotTrust(opts);
|
|
805
|
+
// Pre-parse every candidate certificate (embedded + caller-supplied) once, BEFORE opts.content is
|
|
806
|
+
// touched. The caller-supplied set (opts.certs) is read here; a streamed content's accessor -- run the
|
|
807
|
+
// moment asyncStreamOf reads it below -- could otherwise mutate that array, or replace the
|
|
808
|
+
// Array.prototype.forEach this loop uses, and change which certificates the signer match and the trust
|
|
809
|
+
// build see. Taken first, the candidate pool is the one the caller passed.
|
|
810
|
+
var parsedCerts = [];
|
|
811
|
+
(parsed.certificates || []).forEach(function (c) { _addCert(parsedCerts, c && c.bytes ? c.bytes : c); });
|
|
812
|
+
(opts.certs || []).forEach(function (c) { _addCert(parsedCerts, c); });
|
|
723
813
|
var content = parsed.encapContentInfo.eContent;
|
|
814
|
+
var streamSource = null;
|
|
724
815
|
if (content == null) {
|
|
725
816
|
if (opts.content == null) throw _err("cms/detached-content-required", "this SignedData is detached; opts.content (the external content) is required");
|
|
726
|
-
//
|
|
727
|
-
//
|
|
728
|
-
//
|
|
729
|
-
//
|
|
730
|
-
//
|
|
731
|
-
|
|
817
|
+
// A streamed (async-iterable) detached content is classified by asyncStreamOf, which reads
|
|
818
|
+
// Symbol.asyncIterator a single time and returns a LAZY stream whose iterator is acquired only when
|
|
819
|
+
// it is driven below, after the per-signer digest algorithms are known, so a single-use stream
|
|
820
|
+
// serves every signer in one pass and a message needing no streamed digest never acquires it. It is
|
|
821
|
+
// not snapshotted: a stream has no bytes to copy, and it is consumed exactly once. A byte source (or
|
|
822
|
+
// any non-iterable) yields null and is buffered.
|
|
823
|
+
streamSource = guard.bytes.asyncStreamOf(opts.content);
|
|
824
|
+
if (streamSource == null) {
|
|
825
|
+
// Copied, not aliased: the digest over this content is computed in a later promise turn,
|
|
826
|
+
// and a detached signature's content is the whole of what the signature speaks about. A
|
|
827
|
+
// caller who rewrote the buffer in that gap would have the signature checked against the
|
|
828
|
+
// original bytes while holding the replacement, and the verdict would report a valid
|
|
829
|
+
// signature over content that was never signed.
|
|
830
|
+
content = _toBuf(_snapshotIfBytes(opts.content, "opts.content"), "opts.content");
|
|
831
|
+
}
|
|
732
832
|
} else if (opts.content != null) {
|
|
733
833
|
// A caller supplied external content AND the SignedData is attached. A redundant COPY that equals
|
|
734
834
|
// the encapsulated content is fine; content that DIFFERS is a substitution trap -- silently
|
|
@@ -738,47 +838,46 @@ function _verify(input, opts) {
|
|
|
738
838
|
throw _err("cms/content-conflict", "opts.content disagrees with this SignedData's own encapsulated content (a detached signature must omit eContent)");
|
|
739
839
|
}
|
|
740
840
|
}
|
|
741
|
-
// Pre-parse every candidate certificate (embedded + caller-supplied) once.
|
|
742
|
-
var parsedCerts = [];
|
|
743
|
-
(parsed.certificates || []).forEach(function (c) { _addCert(parsedCerts, c && c.bytes ? c.bytes : c); });
|
|
744
|
-
(opts.certs || []).forEach(function (c) { _addCert(parsedCerts, c); });
|
|
745
841
|
var eContentType = parsed.encapContentInfo.eContentType;
|
|
746
|
-
//
|
|
747
|
-
//
|
|
748
|
-
//
|
|
749
|
-
//
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
842
|
+
// `await` throughout the aggregation, never `.then`: the streamed content is consumed inside
|
|
843
|
+
// _resolveContentDigests, so from here on every promise step must dispatch through no
|
|
844
|
+
// caller-replaceable Promise.prototype.then. Captured enumeration (intrinsic.map) guards
|
|
845
|
+
// Array.prototype.map; awaiting each promise (never Promise.all) guards the chaining (CWE-347).
|
|
846
|
+
var digestByName = await _resolveContentDigests(parsed, streamSource);
|
|
847
|
+
// Each signer is verified SEQUENTIALLY -- started only as it is awaited. NOT Promise.all: its
|
|
848
|
+
// spec-internal element handling calls the caller-replaceable Promise.prototype.then, so a streamed
|
|
849
|
+
// content that replaced it could fabricate a resolved `{ ok: true }` for every signer; await
|
|
850
|
+
// dispatches through no such method. And NOT started-all-then-awaited-each: an earlier signer's
|
|
851
|
+
// rejection would leave a later one's rejection unobserved, which Node can escalate to a crash.
|
|
852
|
+
// Per-signer work is small, so the sequential cost is negligible.
|
|
853
|
+
var signers = [];
|
|
854
|
+
for (var _sp = 0; _sp < parsed.signerInfos.length; _sp++) {
|
|
855
|
+
var siOne = parsed.signerInfos[_sp];
|
|
753
856
|
// res.valid reflects only the PRIMARY signerInfos; a countersignature / unsigned attribute is
|
|
754
857
|
// outside the signature and NEVER flips the top-level verdict. Both are surfaced per signer.
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
//
|
|
771
|
-
//
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
signers: signers,
|
|
779
|
-
};
|
|
780
|
-
return _applyTrust(res, parsedCerts, trustCfg).then(function () { return res; });
|
|
781
|
-
});
|
|
858
|
+
var verdict = await _verifyOne(siOne, content, eContentType, parsedCerts, null, digestByName);
|
|
859
|
+
verdict.countersignatures = await _verifyCountersignatures(siOne, parsedCerts);
|
|
860
|
+
verdict.unsignedAttrs = _surfaceUnsignedAttrs(siOne);
|
|
861
|
+
// Whether THIS signer signed attributes or signed the content directly. The two are different
|
|
862
|
+
// claims -- attributes bind a content type and a signing time alongside the digest, and content-only
|
|
863
|
+
// binds nothing but the bytes -- and RFC 5652 lets a message carry one signer of each. A caller
|
|
864
|
+
// whose profile requires attributes (RFC 8551 S/MIME does) can only enforce it if the verdict says
|
|
865
|
+
// which they got.
|
|
866
|
+
verdict.signedAttributesPresent = !!siOne.signedAttrsBytes;
|
|
867
|
+
intrinsic.push(signers, verdict);
|
|
868
|
+
}
|
|
869
|
+
// The content type travels with the verdict. An operator applying a policy of their own -- "I only
|
|
870
|
+
// accept id-data", or a profile that names its own type -- otherwise had to parse the message a
|
|
871
|
+
// second time to learn it, and a check that needs a second parse is a check most callers will not write.
|
|
872
|
+
var res = {
|
|
873
|
+
// Signature soundness is decided by comparison for the same reason the trust verdict below is:
|
|
874
|
+
// `every` replaced after load reports the whole set sound without reading one signer.
|
|
875
|
+
valid: signers.length > 0 && guard.list.allMatch(signers, function (s) { return s.ok === true; }),
|
|
876
|
+
eContentType: parsed.encapContentInfo.eContentType,
|
|
877
|
+
signers: signers,
|
|
878
|
+
};
|
|
879
|
+
await _applyTrust(res, parsedCerts, trustCfg);
|
|
880
|
+
return res;
|
|
782
881
|
}
|
|
783
882
|
|
|
784
883
|
// The full path build + validate, injected by path-validate the way it injects into crl-verify and
|
|
@@ -816,7 +915,7 @@ function _cloneAnchorValue(v, depth) {
|
|
|
816
915
|
// neither throw out of the snapshot nor put a different instant in the copy than the original
|
|
817
916
|
// holds -- which would leave the anchor policy that was checked and the one that is applied two
|
|
818
917
|
// different values.
|
|
819
|
-
if (guard.time.isDate(v)) return new Date(guard.time.instantOf(v));
|
|
918
|
+
if (guard.time.isDate(v)) return new intrinsic.Date(guard.time.instantOf(v));
|
|
820
919
|
// A structure too deep to copy is REFUSED, not shared. Returning it by reference at the cap
|
|
821
920
|
// would leave part of the anchor caller-mutable across the chain walk while the rest was
|
|
822
921
|
// snapshotted -- a guarantee that holds for the shallow fields and quietly lapses for the deep
|
|
@@ -844,7 +943,11 @@ function _snapshotTrust(opts) {
|
|
|
844
943
|
}
|
|
845
944
|
return {
|
|
846
945
|
trustAnchors: anchors,
|
|
847
|
-
|
|
946
|
+
// The validation instant, read from the captured Date intrinsic, never the live global Date. The
|
|
947
|
+
// snapshot is taken before opts.content is touched (see _verify), so no content accessor has run to
|
|
948
|
+
// replace a global yet; reading the instant through the captured constructor holds even if that
|
|
949
|
+
// ordering regresses. A caller Date is cloned; undefined becomes now.
|
|
950
|
+
time: guard.time.isDate(opts.time) ? new intrinsic.Date(guard.time.instantOf(opts.time)) : (opts.time === undefined ? new intrinsic.Date() : opts.time),
|
|
848
951
|
requiredEku: Array.isArray(opts.requiredEku) ? opts.requiredEku.slice() : opts.requiredEku,
|
|
849
952
|
checkPurpose: opts.checkPurpose,
|
|
850
953
|
};
|
|
@@ -864,7 +967,11 @@ function _snapshotTrust(opts) {
|
|
|
864
967
|
var _CERT_EXT_DECODERS = pkix.certExtensionDecoders(pkix.makeNS("cms", CmsError, oid)).byOid;
|
|
865
968
|
|
|
866
969
|
function _keyUsagePermitsSigning(parsedCerts, der) {
|
|
867
|
-
|
|
970
|
+
// Captured filter + equality: this runs after the streamed content was consumed, and the fail-open
|
|
971
|
+
// `if (!entry) return true` below would otherwise be reachable by a hostile iterator that replaced
|
|
972
|
+
// Buffer.prototype.equals to return false, making the signer certificate un-findable and its
|
|
973
|
+
// keyUsage un-read -- a keyEncipherment-only certificate would then be reported trusted (CWE-347).
|
|
974
|
+
var entry = intrinsic.filter(parsedCerts, function (c) { return intrinsic.bufferEquals(c.der, der); })[0];
|
|
868
975
|
if (!entry) return true; // not among the candidates: nothing to read
|
|
869
976
|
var kuOid = oid.byName("keyUsage");
|
|
870
977
|
var exts = entry.cert.extensions || [];
|
|
@@ -886,22 +993,27 @@ function _allTrusted(signers) {
|
|
|
886
993
|
|
|
887
994
|
// `cfg` is verify's SNAPSHOT, never the caller's options object -- the parameter is named for that
|
|
888
995
|
// so a future caller cannot hand it the live one without noticing.
|
|
889
|
-
function _applyTrust(res, parsedCerts, cfg) {
|
|
996
|
+
async function _applyTrust(res, parsedCerts, cfg) {
|
|
890
997
|
res.trusted = false;
|
|
891
998
|
// Every signer carries the field too, whether or not anchors were supplied. A verdict whose
|
|
892
999
|
// per-signer shape depends on which options were passed makes a caller iterating `signers` write
|
|
893
1000
|
// a different loop for each case -- and the absent field reads as "unknown" where the answer is
|
|
894
1001
|
// a definite "nothing anchored this".
|
|
895
|
-
|
|
896
|
-
|
|
1002
|
+
// async + captured enumeration + await (see _verify): the trust verdict is decided after the streamed
|
|
1003
|
+
// content was consumed, so a hostile iterator must not steer it through a replaced Array.prototype or
|
|
1004
|
+
// Promise.prototype.then.
|
|
1005
|
+
intrinsic.forEach(res.signers, function (s) { s.trusted = false; });
|
|
1006
|
+
if (cfg.trustAnchors == null) return;
|
|
897
1007
|
if (!_engine) {
|
|
898
1008
|
throw _err("cms/bad-input", "opts.trustAnchors requires the path validator; load pki.path before verifying (require the toolkit through its index)");
|
|
899
1009
|
}
|
|
900
1010
|
// Every signer, not merely one: a multi-signer message's signers are independent claims, and
|
|
901
1011
|
// reporting the whole as trusted because one of them anchored would let an unanchored signer ride
|
|
902
1012
|
// out on another's chain -- the same rule the per-signer `ok` already follows for signatures.
|
|
903
|
-
var pool =
|
|
904
|
-
|
|
1013
|
+
var pool = intrinsic.map(parsedCerts, function (c) { return c.der; });
|
|
1014
|
+
// The default instant was captured at snapshot time (see _snapshotTrust), before the stream ran, so
|
|
1015
|
+
// no live `new Date()` is read here where a replaced Date could steer the validation instant.
|
|
1016
|
+
var at = cfg.time;
|
|
905
1017
|
var anchors = cfg.trustAnchors;
|
|
906
1018
|
// The configuration is checked HERE, once, before any signer is looked at. Leaving it to the
|
|
907
1019
|
// chain walk would make a caller's mistake depend on the message: a SignedData whose every signer
|
|
@@ -927,7 +1039,7 @@ function _applyTrust(res, parsedCerts, cfg) {
|
|
|
927
1039
|
// through the same definition the walk uses. Checking one and not the other is how this rule has
|
|
928
1040
|
// repeatedly come back: each new part of the configuration has to join the preflight, not wait
|
|
929
1041
|
// to be caught where it is consumed.
|
|
930
|
-
|
|
1042
|
+
intrinsic.forEach(anchors, function (a) {
|
|
931
1043
|
_engine.toAnchor(a);
|
|
932
1044
|
if (purposes.checkPurpose != null) _engine.assertAnchorConstraints(a, purposes.checkPurpose);
|
|
933
1045
|
});
|
|
@@ -936,9 +1048,10 @@ function _applyTrust(res, parsedCerts, cfg) {
|
|
|
936
1048
|
// anchor's own constraint metadata -- all judged before a single signer is looked at, and all
|
|
937
1049
|
// through the definitions the walk itself uses. A new option joins it here; validating one only
|
|
938
1050
|
// where it is consumed is what made a caller's mistake depend on the message.
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1051
|
+
for (var _si = 0; _si < res.signers.length; _si++) {
|
|
1052
|
+
var s = res.signers[_si];
|
|
1053
|
+
{
|
|
1054
|
+
if (s.ok !== true || !s.cert) { s.trusted = false; continue; }
|
|
942
1055
|
var buildOpts = { trustAnchors: anchors, intermediates: pool, validate: true, time: at };
|
|
943
1056
|
// A key purpose, when the caller names one. "Trusted" is not a property of a chain alone: a
|
|
944
1057
|
// certificate restricted to serverAuth chains perfectly well and is still the wrong key to
|
|
@@ -963,27 +1076,29 @@ function _applyTrust(res, parsedCerts, cfg) {
|
|
|
963
1076
|
// Permitted to have signed, as well as chained. Checked BEFORE the chain walk because a
|
|
964
1077
|
// certificate that forbids signing cannot become trusted by chaining, so there is nothing to
|
|
965
1078
|
// learn from walking it.
|
|
966
|
-
if (!_keyUsagePermitsSigning(parsedCerts, s.cert)) { s.trusted = false;
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1079
|
+
if (!_keyUsagePermitsSigning(parsedCerts, s.cert)) { s.trusted = false; continue; }
|
|
1080
|
+
// `await`, not `.then`: see _verify.
|
|
1081
|
+
try {
|
|
1082
|
+
var r = await _engine.build(s.cert, buildOpts);
|
|
1083
|
+
s.trusted = !!(r && r.valid);
|
|
1084
|
+
} catch (e) {
|
|
1085
|
+
// A CONFIG fault -- unusable anchors, an invalid time -- is the caller's mistake and must
|
|
1086
|
+
// not be absorbed into "untrusted", which would read as a verdict about the message. A
|
|
1087
|
+
// chain that simply does not reach an anchor is not that: it is the answer.
|
|
1088
|
+
if (e && e.code) {
|
|
1089
|
+
var code = intrinsic.String(e.code);
|
|
1090
|
+
if (intrinsic.stringIndexOf(code, "bad-input") !== -1 ||
|
|
1091
|
+
intrinsic.stringIndexOf(code, "bad-anchor") !== -1 ||
|
|
1092
|
+
intrinsic.stringIndexOf(code, "bad-time") !== -1) throw e;
|
|
1093
|
+
}
|
|
1094
|
+
s.trusted = false;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
// The trust verdict is decided by comparison, not by a prototype method: `every` replaced after
|
|
1099
|
+
// load answers true without consulting a single signer, and a message no anchor covers reports
|
|
1100
|
+
// trusted:true.
|
|
1101
|
+
res.trusted = res.valid && res.signers.length > 0 && _allTrusted(res.signers);
|
|
987
1102
|
}
|
|
988
1103
|
// Parse a candidate cert DER and index its SKI; a cert that will not parse is skipped (it
|
|
989
1104
|
// simply cannot be a signer match, and a malformed embedded cert must not fail the verify).
|
|
@@ -1041,6 +1156,12 @@ function _addCert(out, der) {
|
|
|
1041
1156
|
* (draft-ietf-lamps-cms-composite-sigs), where the arm's pre-hash fixes the digestAlgorithm and
|
|
1042
1157
|
* the two component keys are signed together.
|
|
1043
1158
|
*
|
|
1159
|
+
* `content` may instead be an async iterable of byte chunks, such as an async generator, which signs
|
|
1160
|
+
* a large payload without holding it in memory. This form requires `detached: true` and signed
|
|
1161
|
+
* attributes. The payload is hashed incrementally to build the message-digest attribute, in a single
|
|
1162
|
+
* pass that serves every signer, so signers may use different digest algorithms. The signature covers
|
|
1163
|
+
* the same content as the buffered form.
|
|
1164
|
+
*
|
|
1044
1165
|
* @opts detached Omit the encapsulated content (a detached signature; the verifier
|
|
1045
1166
|
* supplies the content). Default false.
|
|
1046
1167
|
* @opts eContentType The encapsulated content type (an OID name). Default `data`.
|