@blamejs/pki 0.8.51 → 0.8.53
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/README.md +1 -1
- package/index.js +2 -1
- package/lib/cms-decrypt.js +40 -3
- package/lib/cms-encrypt.js +64 -2
- package/lib/cms-sign.js +321 -2
- package/lib/cms-verify.js +602 -30
- package/lib/constants.js +15 -0
- package/lib/ct.js +3 -2
- package/lib/guard-der.js +26 -0
- package/lib/guard-identifier.js +27 -0
- package/lib/guard-time.js +98 -0
- package/lib/lint.js +69 -13
- package/lib/oid.js +11 -1
- package/lib/path-validate.js +82 -12
- package/lib/revocation-fetch.js +1 -3
- package/lib/scep.js +8 -3
- package/lib/schema-all.js +3 -2
- package/lib/schema-cms.js +377 -6
- package/lib/schema-smime.js +215 -5
- package/lib/tsp-sign.js +58 -95
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/cms-verify.js
CHANGED
|
@@ -31,6 +31,7 @@ var asn1 = require("./asn1-der");
|
|
|
31
31
|
var oid = require("./oid");
|
|
32
32
|
var x509 = require("./schema-x509");
|
|
33
33
|
var cms = require("./schema-cms");
|
|
34
|
+
var smime = require("./schema-smime");
|
|
34
35
|
var webcrypto = require("./webcrypto");
|
|
35
36
|
var subtle = webcrypto.webcrypto.subtle;
|
|
36
37
|
var edwardsPoint = require("./edwards-point");
|
|
@@ -46,6 +47,7 @@ var validator = require("./validator-all");
|
|
|
46
47
|
var compositeSig = require("./composite-sig");
|
|
47
48
|
var guard = require("./guard-all");
|
|
48
49
|
var intrinsic = require("./guard-intrinsic");
|
|
50
|
+
var constants = require("./constants");
|
|
49
51
|
var frameworkError = require("./framework-error");
|
|
50
52
|
var pkix = require("./schema-pkix");
|
|
51
53
|
|
|
@@ -69,6 +71,15 @@ function _row(o) { return guard.verdict.of(o); }
|
|
|
69
71
|
var OID_MESSAGE_DIGEST = oid.byName("messageDigest");
|
|
70
72
|
var OID_CONTENT_TYPE = oid.byName("contentType");
|
|
71
73
|
var OID_COUNTERSIGNATURE = oid.byName("countersignature");
|
|
74
|
+
/** @internal `id-aa-signatureTimeStampToken`, which the registry already carries under the S/MIME
|
|
75
|
+
* authenticated-attribute arc as `timeStampToken`: `oid.byName("timeStampToken")` is
|
|
76
|
+
* `1.2.840.113549.1.9.16.2.14`, the OID RFC 5126 clause 6.1.1 assigns. A second name for one OID
|
|
77
|
+
* would give `oid.name()` two answers, so the existing name is used rather than a CAdES alias. */
|
|
78
|
+
var OID_TIMESTAMP_TOKEN = oid.byName("timeStampToken");
|
|
79
|
+
var OID_ID_DATA = oid.byName("data");
|
|
80
|
+
var OID_SIGNING_CERTIFICATE = oid.byName("signingCertificate");
|
|
81
|
+
var OID_SIGNING_CERTIFICATE_V2 = oid.byName("signingCertificateV2");
|
|
82
|
+
var OID_SIGNING_TIME = oid.byName("signingTime");
|
|
72
83
|
|
|
73
84
|
var DIGEST_HASH = intrinsic.assign(intrinsic.create(null), {
|
|
74
85
|
sha1: "SHA-1", sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512",
|
|
@@ -83,9 +94,16 @@ function _digestOidTable(named) {
|
|
|
83
94
|
});
|
|
84
95
|
return t;
|
|
85
96
|
}
|
|
86
|
-
|
|
97
|
+
/** @internal Both tables read the one membership list in `C.NAMES.WEAK_DIGESTS`, keyed the two ways
|
|
98
|
+
* the two checks ask the question: by the digest OID a SignerInfo declares, and by the WebCrypto
|
|
99
|
+
* hash name a signature algorithm resolves to. A digest added to that list reaches both. */
|
|
100
|
+
var _WEAK_DIGEST_NAMES = intrinsic.map(intrinsic.keys(constants.NAMES.WEAK_DIGESTS), function (n) {
|
|
101
|
+
return [n, constants.NAMES.WEAK_DIGESTS[n]];
|
|
102
|
+
});
|
|
103
|
+
var WEAK_DIGEST_OIDS = _digestOidTable(_WEAK_DIGEST_NAMES);
|
|
87
104
|
var UNVERIFIABLE_DIGEST_OIDS = _digestOidTable([["md5", "MD5"], ["md4", "MD4"], ["md2", "MD2"]]);
|
|
88
|
-
var WEAK_SIGNATURE_HASHES = intrinsic.
|
|
105
|
+
var WEAK_SIGNATURE_HASHES = intrinsic.create(null);
|
|
106
|
+
intrinsic.forEach(_WEAK_DIGEST_NAMES, function (pair) { WEAK_SIGNATURE_HASHES[pair[1]] = 1; });
|
|
89
107
|
function _supportedDigest(name) { return intrinsic.hasOwn(DIGEST_HASH, name) && !!DIGEST_HASH[name]; }
|
|
90
108
|
async function _computeDigest(name, content) {
|
|
91
109
|
return intrinsic.bufferFrom(await subtle.digest(DIGEST_HASH[name], content));
|
|
@@ -155,6 +173,35 @@ async function _verifyAgainstCandidates(scheme, sigHash, sigBytes, signedBytes,
|
|
|
155
173
|
: { ok: false, sid: sid, cert: candidates[0].der });
|
|
156
174
|
}
|
|
157
175
|
|
|
176
|
+
/** @internal Where a signer identifier matches several certificates, the signing-certificate
|
|
177
|
+
* attribute is what says which of them made the signature, so one it names is tried first. A
|
|
178
|
+
* subjectKeyIdentifier names a KEY: two certificates holding that key verify the same signature
|
|
179
|
+
* equally, and without this the one reported as `signers[i].cert` would be whichever candidate came
|
|
180
|
+
* first, taking with it every decision keyed to that certificate, its validity window, its key
|
|
181
|
+
* usage, and the chain built from it. RFC 5035 clause 5.4 makes the first identifier "the
|
|
182
|
+
* certificate used to verify the signature", which is exactly this question.
|
|
183
|
+
*
|
|
184
|
+
* Order is all that changes: a candidate the attribute names still has to verify the signature, and
|
|
185
|
+
* where none is named or none matches, the list is returned as it came. */
|
|
186
|
+
function _preferBoundCandidate(si, candidates) {
|
|
187
|
+
if (candidates.length < 2) return candidates;
|
|
188
|
+
/** @internal The attribute is read ONCE and each candidate compared against what it read. Reading
|
|
189
|
+
* it per candidate would multiply its size by the candidate count, and both are attacker-chosen:
|
|
190
|
+
* a message can embed many certificates carrying one subject key identifier and a long
|
|
191
|
+
* certificate list inside the attribute, so the work would be their product, spent before the
|
|
192
|
+
* signature is checked. */
|
|
193
|
+
var claims = smime.certBindingClaims(si.signedAttrs || []);
|
|
194
|
+
if (!claims.present) return candidates;
|
|
195
|
+
var bound = [];
|
|
196
|
+
var rest = [];
|
|
197
|
+
intrinsic.forEach(candidates, function (c) {
|
|
198
|
+
var entry = smime.certBindingAgainst(claims.chosen, c.der);
|
|
199
|
+
var names = entry.matches === true && entry.issuerSerialMatches !== false;
|
|
200
|
+
guard.list.append(names ? bound : rest, c);
|
|
201
|
+
});
|
|
202
|
+
return bound.length ? intrinsic.concat(bound, rest) : candidates;
|
|
203
|
+
}
|
|
204
|
+
|
|
158
205
|
function _certSki(cert) {
|
|
159
206
|
var ext = (cert.extensions || []).filter(function (e) { return e.oid === oid.byName("subjectKeyIdentifier"); })[0];
|
|
160
207
|
if (!ext) return null;
|
|
@@ -185,23 +232,22 @@ function _hashAlgOid(seq) {
|
|
|
185
232
|
return o;
|
|
186
233
|
}
|
|
187
234
|
|
|
235
|
+
/** @internal The parameter fields come from `cms.taggedParameterFields`, which holds the DER rule
|
|
236
|
+
* for a SEQUENCE of tagged OPTIONAL and DEFAULT fields: one element per tag, ascending, no repeat,
|
|
237
|
+
* exactly one value each. The comparison that reads these same fields for their defaults walks the
|
|
238
|
+
* same reader, so the verifier and the comparison cannot disagree about what a field is. */
|
|
188
239
|
function _resolvePss(paramsBytes) {
|
|
189
240
|
if (!paramsBytes) return null;
|
|
190
|
-
var
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
var hash = null, saltLen = null, mgfNode = null, trailer = 1n, last = -1;
|
|
196
|
-
for (var i = 0; i < n.children.length; i++) {
|
|
197
|
-
var f = n.children[i];
|
|
198
|
-
if (f.tagClass !== "context" || f.tagNumber > 3 || f.tagNumber <= last || !f.children || f.children.length !== 1) return null;
|
|
199
|
-
last = f.tagNumber;
|
|
241
|
+
var fields = cms.taggedParameterFields(paramsBytes, 3);
|
|
242
|
+
if (fields === null) return null;
|
|
243
|
+
var hash = null, saltLen = null, mgfNode = null, trailer = 1n;
|
|
244
|
+
for (var i = 0; i < fields.length; i++) {
|
|
245
|
+
var f = fields[i];
|
|
200
246
|
try {
|
|
201
|
-
if (f.
|
|
202
|
-
else if (f.
|
|
203
|
-
else if (f.
|
|
204
|
-
else { trailer = asn1.read.integer(f.
|
|
247
|
+
if (f.tag === 0) { hash = PSS_HASH[_hashAlgOid(f.node)]; if (!hash) return null; }
|
|
248
|
+
else if (f.tag === 1) { mgfNode = f.node; }
|
|
249
|
+
else if (f.tag === 2) { saltLen = asn1.read.integer(f.node); }
|
|
250
|
+
else { trailer = asn1.read.integer(f.node); }
|
|
205
251
|
} catch (_e3) { return null; }
|
|
206
252
|
}
|
|
207
253
|
if (hash === null || mgfNode === null) return null;
|
|
@@ -295,7 +341,7 @@ async function _verifyOne(si, content, eContentType, parsedCerts, csTarget, dige
|
|
|
295
341
|
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) });
|
|
296
342
|
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)" });
|
|
297
343
|
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)" });
|
|
298
|
-
var signers = _findSignerCerts(si.sid, parsedCerts);
|
|
344
|
+
var signers = _preferBoundCandidate(si, _findSignerCerts(si.sid, parsedCerts));
|
|
299
345
|
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" });
|
|
300
346
|
var sigBytes = _toBuf(si.signature, "the SignerInfo signature");
|
|
301
347
|
|
|
@@ -381,7 +427,7 @@ async function _verifyComposite(si, comp, content, eContentType, parsedCerts, cs
|
|
|
381
427
|
if (si.digestAlgorithm.name !== comp.phCms) {
|
|
382
428
|
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)" });
|
|
383
429
|
}
|
|
384
|
-
var signers = _findSignerCerts(si.sid, parsedCerts);
|
|
430
|
+
var signers = _preferBoundCandidate(si, _findSignerCerts(si.sid, parsedCerts));
|
|
385
431
|
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" });
|
|
386
432
|
var sigBytes = _toBuf(si.signature, "the SignerInfo signature");
|
|
387
433
|
var signedBytes = await (csTarget != null ? _computeCountersigBytes(si, csTarget) : _computeSignedBytes(si, content, eContentType, digestByName));
|
|
@@ -417,7 +463,375 @@ function _surfaceSignedAttrs(si) {
|
|
|
417
463
|
});
|
|
418
464
|
}
|
|
419
465
|
|
|
420
|
-
|
|
466
|
+
/** @internal What the RFC 6211 algorithm protection attribute says about the structure carrying it,
|
|
467
|
+
* read ONCE per signer and used for both the verdict and the comparison, so the row an operator
|
|
468
|
+
* reads and the decision the verdict rests on cannot disagree.
|
|
469
|
+
*
|
|
470
|
+
* The attribute is a copy of the algorithm identifiers the structure already names, placed where
|
|
471
|
+
* the signature covers them. Sec. 3.1 makes both comparisons mandatory for a validator that
|
|
472
|
+
* supports the attribute, and support is claimed the moment this runs: "If the fields are not the
|
|
473
|
+
* same (modulo encoding), then signature validation MUST fail."
|
|
474
|
+
*
|
|
475
|
+
* Both arms compare modulo encoding, in the two ways sec. 3 names. A field with a default compares
|
|
476
|
+
* the same written out as omitted, which the clause makes a MUST and says a binary compare cannot
|
|
477
|
+
* satisfy; RSASSA-PSS is where that arises, since RFC 4055 clause 3.1 defaults all four of its
|
|
478
|
+
* parameter fields. Absent and DER NULL parameters read as one algorithm on the digest arm, because
|
|
479
|
+
* RFC 3370 clause 2.1 and RFC 5754 clause 2 require both encodings to be ACCEPTED; sec. 3 leaves
|
|
480
|
+
* that case to the implementer. Anything else in a parameter field is read as written: that is
|
|
481
|
+
* where RSASSA-PSS puts its hash, mask generator, salt length and trailer field, which the
|
|
482
|
+
* signature itself does not cover, so collapsing two values there would admit the substitution the
|
|
483
|
+
* attribute exists to refuse.
|
|
484
|
+
*
|
|
485
|
+
* A `[2]` MAC arm inside `signedAttrs` is a mismatch rather than an absent signature arm: sec. 2
|
|
486
|
+
* populates that arm "only if the attribute is placed in an AuthenticatedData.authAttrs sequence",
|
|
487
|
+
* so the attribute is making a claim about a structure this is not. */
|
|
488
|
+
function _algorithmProtectionReport(si, armName, armIdentifier) {
|
|
489
|
+
return cms.algorithmProtectionReport(si.signedAttrs || si.authAttrs || [],
|
|
490
|
+
si.digestAlgorithm, armName, armIdentifier);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** @internal The report applied to a verdict. A mismatch fails the signer, and the option that
|
|
494
|
+
* requires the attribute fails an absent one; a verdict that already failed keeps its own code,
|
|
495
|
+
* since the signature's verdict is the stronger statement and masking it would tell an operator the
|
|
496
|
+
* wrong thing about why the message was refused. */
|
|
497
|
+
function _applyAlgorithmProtection(verdict, report, required) {
|
|
498
|
+
var fields = { algorithmProtection: guard.verdict.of(report) };
|
|
499
|
+
if (verdict.ok !== true) return guard.verdict.of(verdict, fields);
|
|
500
|
+
if (report.mismatch !== null) {
|
|
501
|
+
return guard.verdict.of(verdict, intrinsic.assign(fields,
|
|
502
|
+
{ ok: false, code: report.mismatch.code, message: report.mismatch.message }));
|
|
503
|
+
}
|
|
504
|
+
if (required && !report.present) {
|
|
505
|
+
return guard.verdict.of(verdict, intrinsic.assign(fields, { ok: false,
|
|
506
|
+
code: "cms/missing-algorithm-protection",
|
|
507
|
+
message: "this signature carries no RFC 6211 algorithm protection attribute, which " +
|
|
508
|
+
"opts.requireAlgorithmProtection asks for; RFC 8933 sec. 4.1 makes including it a SHOULD, " +
|
|
509
|
+
"so a message without one is not malformed" }));
|
|
510
|
+
}
|
|
511
|
+
return guard.verdict.of(verdict, fields);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** @internal The CAdES baseline requirements this verb decides from one SignerInfo's own signed
|
|
515
|
+
* attributes, reported beside the signature verdict rather than folded into it: a signature can be
|
|
516
|
+
* cryptographically sound and still not be a CAdES signature, and the two answers are separate.
|
|
517
|
+
*
|
|
518
|
+
* `requirementsChecked` names the scope in the data, so a caller reads which requirements were
|
|
519
|
+
* evaluated instead of taking `conformant` for a full ETSI EN 319 122-1 clause 6.3 audit. Each
|
|
520
|
+
* finding carries the `level` of the requirement it is against, and `conformant` answers for the
|
|
521
|
+
* shall-level ones alone, so a SHOULD the document states as guidance is reported without being
|
|
522
|
+
* reported as non-conformance.
|
|
523
|
+
*
|
|
524
|
+
* - requirement f, shall: "The content-type attribute shall have value id-data".
|
|
525
|
+
* - clauses 5.2.2.2 and 5.2.2.3, shall: the signing certificate is protected by a
|
|
526
|
+
* `signing-certificate` or `signing-certificate-v2` attribute, and the identifier it carries
|
|
527
|
+
* names the certificate the signature resolved to. Clause 5.2.2.2 NOTE 2: "the binding
|
|
528
|
+
* information is the digest of the certificate".
|
|
529
|
+
* - requirement h, shall: "The ESS signing-certificate attribute shall be used if the SHA-1 hash
|
|
530
|
+
* algorithm is used". The two attributes are defined BY their hash algorithm, clause 5.2.2.2 as
|
|
531
|
+
* "a signing certificate attribute using the SHA-1 hash algorithm" and clause 5.2.2.3 as one
|
|
532
|
+
* "using a hash algorithm different from SHA-1", so what the requirement reads is the digest the
|
|
533
|
+
* identifier binds with and not the digest the signature was computed under. Its sibling
|
|
534
|
+
* requirement i, the v2 form "when another hash algorithm than SHA-1 is used", has no verifiable
|
|
535
|
+
* counterexample: a v1 ESSCertID carries no algorithm field, so a non-SHA-1 binding cannot be
|
|
536
|
+
* expressed in one, and it is not claimed as checked.
|
|
537
|
+
* - requirement g, should: "The issuerSerial field should not be included in the encoding of the
|
|
538
|
+
* ESSCertID, ESSCertIDv2 or OtherCertID type".
|
|
539
|
+
* - requirement j, should: "The generator should migrate to the use of ESS signing-certificate-v2
|
|
540
|
+
* in preference to ESS signing-certificate in line with the guidance regarding limited lifetime
|
|
541
|
+
* for the use of SHA-1", which is what a SHA-1 binding is reported against.
|
|
542
|
+
*/
|
|
543
|
+
var CADES_BASELINE_CHECKED = intrinsic.freeze([
|
|
544
|
+
"ETSI EN 319 122-1 clause 6.3 requirement f",
|
|
545
|
+
"ETSI EN 319 122-1 clauses 5.2.2.2 and 5.2.2.3",
|
|
546
|
+
"ETSI EN 319 122-1 clause 6.3 requirement g",
|
|
547
|
+
"ETSI EN 319 122-1 clause 6.3 requirement h",
|
|
548
|
+
"ETSI EN 319 122-1 clause 6.3 requirement j",
|
|
549
|
+
]);
|
|
550
|
+
|
|
551
|
+
function _cadesBaseline(si, signerCert) {
|
|
552
|
+
var attrs = si.signedAttrs || [];
|
|
553
|
+
var findings = [];
|
|
554
|
+
var contentType = null;
|
|
555
|
+
intrinsic.forEach(attrs, function (a) {
|
|
556
|
+
if (a.type !== OID_CONTENT_TYPE || contentType !== null || !a.values || a.values.length !== 1) return;
|
|
557
|
+
try { contentType = asn1.read.oid(asn1.decode(a.values[0])); }
|
|
558
|
+
// allow:swallow-unverified a content-type that does not read is reported as the unmet requirement below
|
|
559
|
+
catch (_e) { contentType = null; }
|
|
560
|
+
});
|
|
561
|
+
if (contentType !== OID_ID_DATA) {
|
|
562
|
+
guard.list.append(findings, {
|
|
563
|
+
code: "cms/cades-content-type", level: "shall",
|
|
564
|
+
message: "the content-type attribute shall have value id-data (ETSI EN 319 122-1 clause 6.3 " +
|
|
565
|
+
"requirement f), and this signature carries " +
|
|
566
|
+
(contentType === null ? "no readable content-type attribute" : (oid.name(contentType) || contentType)),
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
var v1 = null;
|
|
570
|
+
var v2 = null;
|
|
571
|
+
intrinsic.forEach(attrs, function (a) {
|
|
572
|
+
if (a.type === OID_SIGNING_CERTIFICATE && v1 === null && a.values && a.values.length === 1) v1 = a.values[0];
|
|
573
|
+
if (a.type === OID_SIGNING_CERTIFICATE_V2 && v2 === null && a.values && a.values.length === 1) v2 = a.values[0];
|
|
574
|
+
});
|
|
575
|
+
if (v1 === null && v2 === null) {
|
|
576
|
+
guard.list.append(findings, {
|
|
577
|
+
code: "cms/cades-no-signing-certificate", level: "shall",
|
|
578
|
+
message: "the signing certificate shall be protected by a signing-certificate or " +
|
|
579
|
+
"signing-certificate-v2 signed attribute (ETSI EN 319 122-1 clauses 5.2.2.2 and 5.2.2.3)",
|
|
580
|
+
});
|
|
581
|
+
} else {
|
|
582
|
+
intrinsic.forEach(_bindingFindings(attrs, signerCert), function (f) {
|
|
583
|
+
guard.list.append(findings, f);
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
return guard.verdict.of({
|
|
587
|
+
conformant: guard.list.allMatch(findings, function (f) { return f.level !== "shall"; }),
|
|
588
|
+
signingCertificateAttribute: v2 !== null ? "v2" : (v1 !== null ? "v1" : null),
|
|
589
|
+
findings: findings,
|
|
590
|
+
requirementsChecked: CADES_BASELINE_CHECKED,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** @internal What the signing-certificate attribute claims, checked against the certificate this
|
|
595
|
+
* SignerInfo selected. The requirement is that the certificate IS protected, so the attribute's
|
|
596
|
+
* presence is not the answer: an identifier naming other bytes binds the signature to a certificate
|
|
597
|
+
* nobody resolved it to, which is the substitution the attribute exists to prevent (RFC 5035
|
|
598
|
+
* clause 5.4: "The first certificate identified in the sequence of certificate identifiers MUST be
|
|
599
|
+
* the certificate used to verify the signature").
|
|
600
|
+
*
|
|
601
|
+
* The claims are read by `pki.schema.smime.certBindingFacts`, the one implementation of "does this
|
|
602
|
+
* identifier name that certificate", which `pki.tsp.verify` holds a timestamp token to. What differs
|
|
603
|
+
* is the policy over the facts, not the facts: that verb refuses a token on the first failing claim,
|
|
604
|
+
* and this one reports every claim that failed, because a conformance verdict is read to find out
|
|
605
|
+
* what is wrong rather than to decide one thing. A superseded v1 attribute is held to the same claim
|
|
606
|
+
* as the v2 that supersedes it, so a structure naming one certificate in each is reported rather
|
|
607
|
+
* than resolved in favor of whichever a reader prefers. An identifier this build cannot recompute,
|
|
608
|
+
* and a signature whose certificate was never matched, are reported as the undecided claims they
|
|
609
|
+
* are rather than passed over. */
|
|
610
|
+
function _bindingFindings(attrs, signerCert) {
|
|
611
|
+
if (signerCert == null) {
|
|
612
|
+
return [{ code: "cms/cades-signing-certificate-unverifiable", level: "shall",
|
|
613
|
+
message: "no certificate was matched for this signer, so what the signing-certificate " +
|
|
614
|
+
"attribute binds cannot be checked" }];
|
|
615
|
+
}
|
|
616
|
+
var facts = smime.certBindingFacts(attrs, signerCert);
|
|
617
|
+
var out = [];
|
|
618
|
+
var entries = [facts.chosen];
|
|
619
|
+
intrinsic.forEach(facts.superseded, function (s) { guard.list.append(entries, s); });
|
|
620
|
+
intrinsic.forEach(entries, function (e) {
|
|
621
|
+
var which = e.form === "v2" ? "signing-certificate-v2" : "signing-certificate";
|
|
622
|
+
if (!e.readable) {
|
|
623
|
+
guard.list.append(out, { code: "cms/cades-signing-certificate-unreadable", level: "shall",
|
|
624
|
+
message: "the " + which + " attribute value does not decode, so what it binds cannot be checked" });
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
if (!e.supported) {
|
|
628
|
+
guard.list.append(out, { code: "cms/cades-signing-certificate-unreadable", level: "shall",
|
|
629
|
+
message: "the " + which + " identifier names the digest " + e.digestName +
|
|
630
|
+
", which this build does not bind a certificate with, so what it binds cannot be checked" });
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
/** @internal The form is held to the digest it binds with, which is what defines each of the two
|
|
634
|
+
* attributes: a v2 identifier naming SHA-1 is the case requirement h sends to the v1 attribute.
|
|
635
|
+
* A v1 identifier carries no algorithm field, so it is SHA-1 by the type and can never be the
|
|
636
|
+
* wrong form for its own digest; what it draws is requirement j's migration guidance. */
|
|
637
|
+
if (e.weak && e.form === "v2") {
|
|
638
|
+
guard.list.append(out, { code: "cms/cades-signing-certificate-form", level: "shall",
|
|
639
|
+
message: "the signing-certificate attribute shall be used if the SHA-1 hash algorithm is " +
|
|
640
|
+
"used (ETSI EN 319 122-1 clause 6.3 requirement h), and this signature carries a " +
|
|
641
|
+
"signing-certificate-v2 identifier binding with " + e.digestName });
|
|
642
|
+
} else if (e.weak) {
|
|
643
|
+
guard.list.append(out, { code: "cms/cades-weak-certificate-binding", level: "should",
|
|
644
|
+
message: "the " + which + " identifier binds the certificate with " + e.digestName +
|
|
645
|
+
", and a generator should migrate to signing-certificate-v2 in preference to " +
|
|
646
|
+
"signing-certificate given the limited lifetime for the use of SHA-1 (ETSI EN 319 122-1 " +
|
|
647
|
+
"clause 6.3 requirement j)" });
|
|
648
|
+
}
|
|
649
|
+
if (e.issuerSerialPresent) {
|
|
650
|
+
guard.list.append(out, { code: "cms/cades-issuer-serial-included", level: "should",
|
|
651
|
+
message: "the issuerSerial field should not be included in the encoding of the " +
|
|
652
|
+
(e.form === "v2" ? "ESSCertIDv2" : "ESSCertID") + " type (ETSI EN 319 122-1 clause 6.3 " +
|
|
653
|
+
"requirement g); clause 5.2.2.2 NOTE 2 has it as a hint, the binding being the digest of " +
|
|
654
|
+
"the certificate" });
|
|
655
|
+
}
|
|
656
|
+
if (e.certUnreadable) {
|
|
657
|
+
guard.list.append(out, { code: "cms/cades-signing-certificate-unverifiable", level: "shall",
|
|
658
|
+
message: "the " + which + " identifier states an issuer and serial, and the certificate " +
|
|
659
|
+
"this signer resolved to does not parse, so that claim cannot be checked" });
|
|
660
|
+
}
|
|
661
|
+
if (e.matches !== true || e.issuerSerialMatches === false) {
|
|
662
|
+
guard.list.append(out, { code: "cms/cades-signing-certificate-mismatch", level: "shall",
|
|
663
|
+
message: "the first " + which + " identifier does not name the certificate this signature " +
|
|
664
|
+
"was resolved to: " + (e.matches !== true ? "the certificate hash disagrees"
|
|
665
|
+
: "the hash agrees but the issuer and serial name another certificate") +
|
|
666
|
+
" (RFC 5035 clause 5.4), so the signing certificate is not protected (ETSI EN 319 122-1 " +
|
|
667
|
+
"clauses 5.2.2.2 and 5.2.2.3)" });
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
return out;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/** @internal The CAdES signature timestamps on one SignerInfo, each as its own row.
|
|
674
|
+
*
|
|
675
|
+
* `id-aa-signatureTimeStampToken` is `{ 1 2 840 113549 1 9 16 2 14 }` (RFC 5126 clause 6.1.1; ETSI
|
|
676
|
+
* EN 319 122-1 clause 5.3 and Annex D) and its imprint is "the hash value of the signature field
|
|
677
|
+
* (without the ASN.1 tag and length) within SignerInfo for which the signature-time-stamp attribute
|
|
678
|
+
* is created" (EN 319 122-1 clause 5.3).
|
|
679
|
+
*
|
|
680
|
+
* Read HERE, in the loop that already holds `targetSig`, rather than in a verb of its own. The
|
|
681
|
+
* imprint preimage is the same octets the countersignature row covers, and a separate verb would
|
|
682
|
+
* re-derive it from its own parse: two derivations of one preimage that can disagree is a silent
|
|
683
|
+
* fail-open, since the one that disagrees is the one that accepts.
|
|
684
|
+
*
|
|
685
|
+
* Plurality is the opposite of countersignature: EN 319 122-1 clause 5.3 has "exactly one component
|
|
686
|
+
* of AttributeValue type" per attribute, and RFC 5126 clause 6.1.1 allows "several instances of this
|
|
687
|
+
* attribute ... from different TSAs", so each instance is one row and an instance carrying two values
|
|
688
|
+
* is refused rather than read as two timestamps.
|
|
689
|
+
*
|
|
690
|
+
* Every failure is a ROW verdict and never a throw: a signature whose timestamp cannot be read still
|
|
691
|
+
* has its own signature verdict, and collapsing the two would lose it.
|
|
692
|
+
*
|
|
693
|
+
* `0.4.0.1733.2.4`, the archive timestamp, is NOT read here. EN 319 122-1 clause 5.5.3 contains the
|
|
694
|
+
* sentence "The archive-time-stamp-v3 attribute shall be identified by the
|
|
695
|
+
* id-aa-signatureTimeStampToken OID" and then defines `id-aa-ets-archiveTimestampV3` under the ETSI
|
|
696
|
+
* arc, as does Annex D. The ASN.1 is followed: the two attributes hash different preimages, and
|
|
697
|
+
* clause 5.5.3 step 3 has the archive timestamp cover six SignerInfo fields "including the tag,
|
|
698
|
+
* length and value octets" where this one excludes the tag and length of one of them, so a dispatcher
|
|
699
|
+
* following that sentence would hash the wrong octets for every B-T signature. */
|
|
700
|
+
async function _verifySignatureTimestamps(si, targetSig, opts) {
|
|
701
|
+
var rows = [];
|
|
702
|
+
var attrs = [];
|
|
703
|
+
intrinsic.forEach(si.unsignedAttrs || [], function (a) { if (a.type === OID_TIMESTAMP_TOKEN) guard.list.append(attrs, a); });
|
|
704
|
+
if (!attrs.length) return rows;
|
|
705
|
+
/** @internal The signer's own claimed signing time and certificate window, read once here and
|
|
706
|
+
* passed to every row, so each row answers the coherence questions against the same values. */
|
|
707
|
+
var claimed = _signingTimeOf(si);
|
|
708
|
+
for (var i = 0; i < attrs.length; i++) {
|
|
709
|
+
guard.list.append(rows, await _verifyOneTimestamp(attrs[i], targetSig, claimed, opts));
|
|
710
|
+
}
|
|
711
|
+
return rows;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/** @internal The signer's `signing-time` attribute as a Date, or null where it carries none. It is the
|
|
715
|
+
* signer's own assertion and is not evidence of the real time, but it sits inside the signature, so a
|
|
716
|
+
* timestamp that precedes it is a contradiction within one signature. */
|
|
717
|
+
function _signingTimeOf(si) {
|
|
718
|
+
var found = null;
|
|
719
|
+
intrinsic.forEach(si.signedAttrs || [], function (a) {
|
|
720
|
+
if (a.type !== OID_SIGNING_TIME || found !== null || !a.values || !a.values.length) return;
|
|
721
|
+
/** @internal Read without a guard around it: `pki.schema.cms.parse` holds a signing-time
|
|
722
|
+
* attribute's value to a Time through this same reader before any of this runs, so a value
|
|
723
|
+
* that reaches here reads. A parser that stopped checking would surface the failure rather
|
|
724
|
+
* than have this quietly report no claimed time. */
|
|
725
|
+
found = asn1.read.time(asn1.decode(a.values[0]));
|
|
726
|
+
});
|
|
727
|
+
return found;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/** @internal The instant a Date names, or null where the value is absent or names no instant. Every
|
|
731
|
+
* ordering rule below reads its operands through this, so "no value" and "a value that cannot be
|
|
732
|
+
* ordered" reach the comparison as the same absence rather than as a comparison that is false both
|
|
733
|
+
* ways. */
|
|
734
|
+
function _instantOrNull(value) {
|
|
735
|
+
if (!guard.time.isDate(value)) return null;
|
|
736
|
+
var at = guard.time.instantOf(value);
|
|
737
|
+
return intrinsic.numberIsNaN(at) ? null : at;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
async function _verifyOneTimestamp(attr, targetSig, claimedSigningTime, opts) {
|
|
741
|
+
if (!attr.values || attr.values.length !== 1) {
|
|
742
|
+
return guard.verdict.of({ valid: false, trusted: false, genTime: null,
|
|
743
|
+
code: "cms/timestamp-multi-valued",
|
|
744
|
+
message: "a signature-time-stamp attribute shall contain exactly one component of AttributeValue " +
|
|
745
|
+
"type (ETSI EN 319 122-1 clause 5.3), got " + ((attr.values && attr.values.length) || 0) });
|
|
746
|
+
}
|
|
747
|
+
var tokenDer = attr.values[0];
|
|
748
|
+
tokenDer = tokenDer && tokenDer.bytes ? tokenDer.bytes : tokenDer;
|
|
749
|
+
var res;
|
|
750
|
+
try {
|
|
751
|
+
/** @internal Reached inside the function rather than at the top of the file: `tsp-sign` requires
|
|
752
|
+
* THIS module to verify a token's own CMS signature, so a top-level require here is a cycle and
|
|
753
|
+
* one of the two would see a half-built export object. */
|
|
754
|
+
var tspVerify = require("./tsp-sign"); // allow:inline-require -- circular load: tsp-sign requires this module
|
|
755
|
+
res = await tspVerify.verify(tokenDer, targetSig, {
|
|
756
|
+
certs: opts.timestampCerts,
|
|
757
|
+
trustAnchors: opts.timestampTrustAnchors,
|
|
758
|
+
revocationChecker: opts.timestampRevocationChecker,
|
|
759
|
+
allowWeakDigests: opts.allowWeakDigests === true,
|
|
760
|
+
});
|
|
761
|
+
} catch (e) {
|
|
762
|
+
return guard.verdict.of({ valid: false, trusted: false, genTime: null,
|
|
763
|
+
code: (e && e.code) || "cms/bad-timestamp", message: e && e.message });
|
|
764
|
+
}
|
|
765
|
+
if (res.valid !== true) {
|
|
766
|
+
return guard.verdict.of({ valid: false, trusted: res.trusted === true, genTime: res.genTime || null,
|
|
767
|
+
code: res.code === "tsp/imprint-mismatch" ? "cms/timestamp-imprint-mismatch" : res.code,
|
|
768
|
+
message: res.reason || null });
|
|
769
|
+
}
|
|
770
|
+
/** @internal Two coherence rules the token alone cannot answer, each against a value read from the
|
|
771
|
+
* signature this attribute hangs on. The first is the signer's claimed time; the second is
|
|
772
|
+
* EN 319 122-1 clause 6.3 requirement m, "the tokens ... shall be created before the signing
|
|
773
|
+
* certificate has been revoked or has expired", whose expiry half is decidable from the signer
|
|
774
|
+
* certificate alone and whose revocation half needs status this verb does not fetch.
|
|
775
|
+
*
|
|
776
|
+
* Each instant is taken once, through the captured read, and a genTime that is not a usable
|
|
777
|
+
* instant is refused rather than compared: an ordering test against it answers false in both
|
|
778
|
+
* directions, so a token carrying one would pass both rules by being uncomparable. */
|
|
779
|
+
var genAt = _instantOrNull(res.genTime);
|
|
780
|
+
if (res.genTime != null && genAt === null) {
|
|
781
|
+
return guard.verdict.of({ valid: false, trusted: res.trusted === true, genTime: null,
|
|
782
|
+
code: "cms/bad-timestamp-gentime",
|
|
783
|
+
message: "the token's genTime is not a usable instant, so the times this attribute is held " +
|
|
784
|
+
"to cannot be ordered against it" });
|
|
785
|
+
}
|
|
786
|
+
var claimedAt = _instantOrNull(claimedSigningTime);
|
|
787
|
+
if (claimedAt !== null && genAt !== null && genAt < claimedAt) {
|
|
788
|
+
return guard.verdict.of({ valid: false, trusted: res.trusted === true, genTime: res.genTime,
|
|
789
|
+
code: "cms/timestamp-before-signing-time",
|
|
790
|
+
message: "the token's genTime " + guard.time.isoOf(res.genTime) + " precedes the signing-time " +
|
|
791
|
+
"the signature itself asserts, " + guard.time.isoOf(claimedSigningTime) });
|
|
792
|
+
}
|
|
793
|
+
/** @internal The upper bound is the LATEST instant the token could have been created at, not the
|
|
794
|
+
* instant it names. Two things widen it past a `Date`: RFC 3161 clause 2.4.2 lets genTime carry a
|
|
795
|
+
* fraction finer than a Date holds, so a time one microsecond past a notAfter truncates to the
|
|
796
|
+
* notAfter itself, and the accuracy the token states is a window around genTime rather than a
|
|
797
|
+
* point, so a genTime half a second inside the window with two seconds of accuracy establishes
|
|
798
|
+
* nothing about which side of it the token was made on. Requirement m asks that the token WAS
|
|
799
|
+
* created before the certificate expired, so a bound that cannot establish it refuses. */
|
|
800
|
+
var notAfterAt = _instantOrNull(opts.signerNotAfter);
|
|
801
|
+
var genUpper = genAt === null ? null
|
|
802
|
+
: guard.time.latestInstantOf(res.genTime, res.tstInfo && res.tstInfo.genTimeFraction, res.accuracy || null);
|
|
803
|
+
if (notAfterAt !== null && genUpper !== null && genUpper > notAfterAt) {
|
|
804
|
+
return guard.verdict.of({ valid: false, trusted: res.trusted === true, genTime: res.genTime,
|
|
805
|
+
code: "cms/timestamp-after-signer-expiry",
|
|
806
|
+
/** @internal The message names the BOUND, not just the genTime: this verdict is reached
|
|
807
|
+
* whenever the latest instant the token could have been created at is past the notAfter, and
|
|
808
|
+
* with a wide accuracy or a sub-millisecond fraction that is true of tokens whose stated
|
|
809
|
+
* genTime is inside the window. Reporting the genTime alone would give an operator a reason
|
|
810
|
+
* they can check and find false. */
|
|
811
|
+
message: "the token's genTime " + guard.time.isoOf(res.genTime) + ", with the accuracy it " +
|
|
812
|
+
"states, puts the latest it could have been created at " +
|
|
813
|
+
guard.time.isoOf(new intrinsic.Date(genUpper)) + ", past the signing certificate's notAfter " +
|
|
814
|
+
guard.time.isoOf(opts.signerNotAfter) + ", so it does not establish that the token was " +
|
|
815
|
+
"created before that certificate expired (ETSI EN 319 122-1 clause 6.3 requirement m)" });
|
|
816
|
+
}
|
|
817
|
+
return guard.verdict.of({
|
|
818
|
+
valid: true, trusted: res.trusted === true, genTime: res.genTime,
|
|
819
|
+
accuracy: res.accuracy, serialNumber: res.serialNumber, policy: res.policy,
|
|
820
|
+
revocationChecked: res.revocationChecked, tsa: res.tsa, signer: res.signer,
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/** @internal The signing certificate's `notAfter`, where the signer row matched one. The expiry half
|
|
825
|
+
* of EN 319 122-1 clause 6.3 requirement m is decidable from this certificate alone; the revocation
|
|
826
|
+
* half needs status this verb does not fetch and is not claimed. */
|
|
827
|
+
function _signerNotAfter(verdict) {
|
|
828
|
+
if (!verdict || !verdict.cert) return null;
|
|
829
|
+
try { return x509.parse(verdict.cert).validity.notAfter; }
|
|
830
|
+
// allow:swallow-unverified a signer certificate that does not parse already failed its own verdict
|
|
831
|
+
catch (_e) { return null; }
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
async function _verifyCountersignatures(si, parsedCerts, allowWeakDigests, requireProtection) {
|
|
421
835
|
var targetSig = _toBuf(si.signature, "the countersigned signature");
|
|
422
836
|
var values = [];
|
|
423
837
|
intrinsic.forEach(si.unsignedAttrs || [], function (a) {
|
|
@@ -425,17 +839,24 @@ async function _verifyCountersignatures(si, parsedCerts, allowWeakDigests) {
|
|
|
425
839
|
intrinsic.forEach(a.values, function (v) { guard.list.append(values, v && v.bytes ? v.bytes : v); });
|
|
426
840
|
});
|
|
427
841
|
var out = [];
|
|
428
|
-
for (var _cp = 0; _cp < values.length; _cp++) guard.list.append(out, await _verifyOneCountersig(values[_cp], targetSig, parsedCerts, allowWeakDigests));
|
|
842
|
+
for (var _cp = 0; _cp < values.length; _cp++) guard.list.append(out, await _verifyOneCountersig(values[_cp], targetSig, parsedCerts, allowWeakDigests, requireProtection));
|
|
429
843
|
return out;
|
|
430
844
|
}
|
|
431
845
|
|
|
432
|
-
async function _verifyOneCountersig(vDer, targetSig, parsedCerts, allowWeakDigests) {
|
|
846
|
+
async function _verifyOneCountersig(vDer, targetSig, parsedCerts, allowWeakDigests, requireProtection) {
|
|
433
847
|
var csSi;
|
|
434
848
|
try { csSi = cms.walkCountersignature(asn1.decode(intrinsic.isBuffer(vDer) ? vDer : intrinsic.bufferFrom(vDer))); }
|
|
435
849
|
catch (e) { return _row({ ok: false, code: (e instanceof CmsError ? e.code : "cms/bad-countersignature"), message: e && e.message }); }
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
850
|
+
/** @internal A countersignature is its own SignerInfo with its own algorithm identifiers, so the
|
|
851
|
+
* attribute inside it protects those and is compared against them rather than against the
|
|
852
|
+
* signature it countersigns. */
|
|
853
|
+
var apReport = _algorithmProtectionReport(csSi, "signatureAlgorithm", csSi.signatureAlgorithm);
|
|
854
|
+
var verdict = _applyAlgorithmProtection(
|
|
855
|
+
await _verifyOne(csSi, targetSig, null, parsedCerts, targetSig, undefined, allowWeakDigests),
|
|
856
|
+
apReport, requireProtection === true);
|
|
857
|
+
var nested = await _verifyCountersignatures(csSi, parsedCerts, allowWeakDigests, requireProtection);
|
|
858
|
+
var head = { ok: verdict.ok, sid: verdict.sid, cert: verdict.cert, digestAlgorithm: csSi.digestAlgorithm.name,
|
|
859
|
+
algorithmProtection: verdict.algorithmProtection };
|
|
439
860
|
return guard.verdict.of(
|
|
440
861
|
verdict.code ? guard.verdict.of(head, { code: verdict.code }) : head,
|
|
441
862
|
{ unsignedAttrs: _surfaceUnsignedAttrs(csSi), countersignatures: nested });
|
|
@@ -455,7 +876,7 @@ async function _verifyOneCountersig(vDer, targetSig, parsedCerts, allowWeakDiges
|
|
|
455
876
|
* Verify a CMS SignedData signature (RFC 5652 sec. 5). `input` is a PEM string, a DER
|
|
456
877
|
* `Buffer`, or a parsed `pki.schema.cms` object. Returns `{ valid, trusted, eContentType, eContent, signers }`
|
|
457
878
|
* (`eContent` the encapsulated content when present, else null) where each `signers[i]` is
|
|
458
|
-
* `{ ok, sid, cert, trusted, signedAttributesPresent, signedAttributes, unsignedAttrs, digestAlgorithm, countersignatures }`
|
|
879
|
+
* `{ ok, sid, cert, trusted, signedAttributesPresent, signedAttributes, unsignedAttrs, digestAlgorithm, countersignatures, signatureTimeStamps, cadesBaseline, algorithmProtection }`
|
|
459
880
|
* (`cert` the matched signer certificate DER) or carries a `code` on a structural failure; `valid` is true when
|
|
460
881
|
* there is at least one signer and every signer verified.
|
|
461
882
|
*
|
|
@@ -480,12 +901,52 @@ async function _verifyOneCountersig(vDer, targetSig, parsedCerts, allowWeakDiges
|
|
|
480
901
|
* `subjectKeyIdentifier` names a key, and several certificates can hold it with different
|
|
481
902
|
* validity windows, key usage and policies; deciding from a sibling would let an expired or
|
|
482
903
|
* wrong-purpose signer certificate be reported trusted because a different certificate chained.
|
|
483
|
-
* Supply the certificate you want used.
|
|
904
|
+
* Supply the certificate you want used. Where several candidates match the identifier and the
|
|
905
|
+
* signature carries an ESS signing-certificate attribute, the candidate that attribute names is the
|
|
906
|
+
* one selected, since RFC 5035 clause 5.4 makes its first identifier "the certificate used to verify
|
|
907
|
+
* the signature"; a named candidate must still verify the signature like any other. RSA (PKCS#1 v1.5 and RSASSA-PSS), ECDSA, EdDSA, and the post-quantum
|
|
484
908
|
* ML-DSA (ML-DSA-44/65/87, RFC 9882) and SLH-DSA (the twelve FIPS 205 sets, RFC 9814) signatures
|
|
485
909
|
* are recognized, the post-quantum families in pure mode with an empty context, as is composite
|
|
486
910
|
* ML-DSA (draft-ietf-lamps-cms-composite-sigs), which pairs ML-DSA with a traditional
|
|
487
911
|
* RSA / ECDSA / EdDSA and verifies only when both components pass (never an AND-to-OR downgrade).
|
|
488
912
|
*
|
|
913
|
+
* `signatureTimeStamps` is one row per `id-aa-signatureTimeStampToken` unsigned attribute (RFC 5126
|
|
914
|
+
* clause 6.1.1, ETSI EN 319 122-1 clause 5.3), each `{ valid, trusted, genTime, accuracy,
|
|
915
|
+
* serialNumber, policy, revocationChecked, tsa, signer }` or `{ valid: false, code, message }`. A
|
|
916
|
+
* token that does not verify is a row verdict and does not take the signer's own `ok` down: the
|
|
917
|
+
* signature and the time it was made at are separate claims. Each row's imprint is checked against
|
|
918
|
+
* the signature octets this verb just verified, and a row is refused when its `genTime` precedes the
|
|
919
|
+
* signer's own `signing-time` attribute or falls after the signing certificate's `notAfter`
|
|
920
|
+
* (EN 319 122-1 clause 6.3 requirement m).
|
|
921
|
+
*
|
|
922
|
+
* What a `valid: true` row establishes is that the token was issued over the signature octets THIS
|
|
923
|
+
* SignerInfo carries. It does not say that signature is sound, which is `signers[i].ok`, and rows are
|
|
924
|
+
* reported beside a failed signature rather than withheld, so a caller acting on a timestamp reads
|
|
925
|
+
* both. The imprint names signature octets rather than a signer, which is what EN 319 122-1
|
|
926
|
+
* clause 5.3 specifies: two certificates holding one key produce one signature value over one
|
|
927
|
+
* content, and a token over it answers for either. `pki.cms.attachTimestamp` adds such an attribute to an
|
|
928
|
+
* existing message and `pki.cms.timestampImprint` produces the imprint a TSA signs.
|
|
929
|
+
*
|
|
930
|
+
* `algorithmProtection` reports the RFC 6211 attribute, `{ present, oid, compared, mismatch }`. The
|
|
931
|
+
* attribute is a copy of the digest and signature algorithm identifiers this signer names, placed
|
|
932
|
+
* where the signature covers them, and `compared` lists the identifiers checked against it. A
|
|
933
|
+
* disagreement fails the signer with `cms/algorithm-protection-mismatch`, which sec. 3.1 requires of
|
|
934
|
+
* any validator that supports the attribute: the identifiers the attribute protects are otherwise
|
|
935
|
+
* covered by nothing, and RFC 8933 sec. 6 names RSASSA-PSS as the case where the signature itself
|
|
936
|
+
* pins no digest. An absent attribute is not a failure unless `requireAlgorithmProtection` asks for
|
|
937
|
+
* one, and `present: false` says so rather than looking like a comparison that passed.
|
|
938
|
+
*
|
|
939
|
+
* `cadesBaseline` is a third claim, `{ conformant, signingCertificateAttribute, findings,
|
|
940
|
+
* requirementsChecked }`: a signature can be sound and still not be a CAdES signature.
|
|
941
|
+
* `requirementsChecked` lists the EN 319 122-1 requirements decided here, each unmet one carrying a
|
|
942
|
+
* `{ code, message }` finding, so `conformant` is read against that list and not as a full
|
|
943
|
+
* clause 6.3 audit. The signing-certificate requirement is the attribute's CLAIM checked against the
|
|
944
|
+
* certificate this SignerInfo selected, the one reported as `signers[i].cert`, not the attribute's
|
|
945
|
+
* presence. Like the timestamp rows it is reported beside a failed signature rather than withheld,
|
|
946
|
+
* so `conformant: true` on a signer whose `ok` is false says the CAdES attributes are in order and
|
|
947
|
+
* nothing about the signature. `pki.cms.sign` reaches `conformant: true` with a
|
|
948
|
+
* `signingCertificateV2` attribute built by `pki.schema.smime.buildSigningCertificateV2`.
|
|
949
|
+
*
|
|
489
950
|
* @opts content The detached content when the SignedData carries no encapsulated eContent, and
|
|
490
951
|
* required for a detached signature. A `Buffer`, or an async iterable of byte chunks
|
|
491
952
|
* to verify a large payload without holding it in memory. The streamed form is hashed
|
|
@@ -503,6 +964,21 @@ async function _verifyOneCountersig(vDer, targetSig, parsedCerts, allowWeakDiges
|
|
|
503
964
|
* question from `requiredEku`, since a root distributed with NSS trust bits can be
|
|
504
965
|
* marked untrusted for one purpose and good for another. Those bits and
|
|
505
966
|
* `distrustAfter` are consulted only when this names a purpose.
|
|
967
|
+
* @opts timestampCerts Extra TSA certificates for the signature-timestamp rows, in addition to the
|
|
968
|
+
* ones each token embeds.
|
|
969
|
+
* @opts timestampTrustAnchors The roots a time-stamping authority must chain to for a timestamp row
|
|
970
|
+
* to report `trusted`. Named apart from `trustAnchors` because a TSA is a different
|
|
971
|
+
* trust decision from the signer: an operator may anchor one and not the other.
|
|
972
|
+
* @opts timestampRevocationChecker A revocation checker for the TSA chain, as `pki.path.validate`
|
|
973
|
+
* takes one. Each row reports what it established in `revocationChecked`.
|
|
974
|
+
* @opts requireAlgorithmProtection Fail a signer that carries no RFC 6211
|
|
975
|
+
* `CMSAlgorithmProtection` attribute, with `cms/missing-algorithm-protection`. Off
|
|
976
|
+
* by default: RFC 6211 says nothing about absence and RFC 8933 sec. 4.1 makes
|
|
977
|
+
* including it a SHOULD, so a message without one is not malformed and most CMS in
|
|
978
|
+
* the field carries none. A signer that DOES carry one is always held to it, with no
|
|
979
|
+
* option, because sec. 3.1's comparisons are MUSTs once a validator supports the
|
|
980
|
+
* attribute. The option is the boolean it documents: `false`, `null` and an absent
|
|
981
|
+
* option are the off forms, and any other value is refused `cms/bad-input`.
|
|
506
982
|
* @example
|
|
507
983
|
* var pair = await pki.key.generate("Ed25519");
|
|
508
984
|
* var key = await pki.key.export(pair.privateKey);
|
|
@@ -540,7 +1016,13 @@ async function _resolveContentDigests(parsed, streamSource) {
|
|
|
540
1016
|
return byName;
|
|
541
1017
|
}
|
|
542
1018
|
|
|
543
|
-
var _VERIFY_OPTS = intrinsic.assign(intrinsic.create(null), { certs: 1, content: 1, trustAnchors: 1, time: 1, requiredEku: 1, checkPurpose: 1, allowWeakDigests: 1
|
|
1019
|
+
var _VERIFY_OPTS = intrinsic.assign(intrinsic.create(null), { certs: 1, content: 1, trustAnchors: 1, time: 1, requiredEku: 1, checkPurpose: 1, allowWeakDigests: 1,
|
|
1020
|
+
/** @internal The timestamp inputs are named apart from the signer's own, because a time-stamping
|
|
1021
|
+
* authority is a different trust decision from the signer's: an operator may anchor the TSA while
|
|
1022
|
+
* leaving the signer unanchored, or the reverse, and one `trustAnchors` for both would make those
|
|
1023
|
+
* two answers one. */
|
|
1024
|
+
timestampCerts: 1, timestampTrustAnchors: 1, timestampRevocationChecker: 1,
|
|
1025
|
+
requireAlgorithmProtection: 1 });
|
|
544
1026
|
|
|
545
1027
|
function verify(input, opts) {
|
|
546
1028
|
return guard.async.deferred(function () { return _verify(input, opts); });
|
|
@@ -549,12 +1031,20 @@ function verify(input, opts) {
|
|
|
549
1031
|
async function _verify(input, opts) {
|
|
550
1032
|
opts = guard.identifier.optionsObject(opts, _err, "cms/bad-input", "pki.cms.verify options");
|
|
551
1033
|
guard.identifier.assertKnownKeys(opts, _VERIFY_OPTS, _err, "cms/bad-input", "pki.cms.verify has an unknown option ");
|
|
1034
|
+
/** @internal The two policy switches are taken FIRST, ahead of everything below that can run the
|
|
1035
|
+
* caller's own code. Deriving the message runs accessors on a parsed input, copying the trust
|
|
1036
|
+
* material runs them on each anchor, and a streamed detached content runs the generator while
|
|
1037
|
+
* this verb is pending; any of those can write to an options object the caller still holds, so a
|
|
1038
|
+
* switch read after them is read from whatever they left behind rather than from the call. */
|
|
1039
|
+
var allowWeakDigests = opts.allowWeakDigests === true;
|
|
1040
|
+
var requireProtection = guard.identifier.booleanOption(opts.requireAlgorithmProtection, _err,
|
|
1041
|
+
"cms/bad-input", "pki.cms.verify opts.requireAlgorithmProtection");
|
|
552
1042
|
var parsed = guard.parsed.acceptDerived(input, "cms", function (bytes) {
|
|
553
1043
|
return cms.parse(_snapshotIfBytes(bytes, "pki.cms.verify"));
|
|
554
1044
|
}, _err, "cms/bad-input", "the SignedData");
|
|
555
1045
|
if (!Array.isArray(parsed.signerInfos)) throw _err("cms/bad-input", "input is not a CMS SignedData");
|
|
556
1046
|
var trustCfg = _snapshotTrust(opts);
|
|
557
|
-
var
|
|
1047
|
+
var tsTrustCfg = _snapshotTimestampTrust(opts, allowWeakDigests);
|
|
558
1048
|
var parsedCerts = [];
|
|
559
1049
|
(parsed.certificates || []).forEach(function (c) { _addCert(parsedCerts, c && c.bytes ? c.bytes : c); });
|
|
560
1050
|
(opts.certs || []).forEach(function (c) { _addCert(parsedCerts, c); });
|
|
@@ -580,13 +1070,25 @@ async function _verify(input, opts) {
|
|
|
580
1070
|
var signers = [];
|
|
581
1071
|
for (var _sp = 0; _sp < parsed.signerInfos.length; _sp++) {
|
|
582
1072
|
var siOne = parsed.signerInfos[_sp];
|
|
1073
|
+
/** @internal Read once, before the signature is checked, and used for both the verdict row and
|
|
1074
|
+
* the decision: the attribute is an independent requirement on the same signer, the way trust
|
|
1075
|
+
* is, so it composes onto the verdict rather than being folded into the signature check. */
|
|
1076
|
+
var apReport = _algorithmProtectionReport(siOne, "signatureAlgorithm", siOne.signatureAlgorithm);
|
|
583
1077
|
var verdict = await _verifyOne(siOne, content, eContentType, parsedCerts, null, digestByName, allowWeakDigests);
|
|
584
|
-
|
|
1078
|
+
verdict = _applyAlgorithmProtection(verdict, apReport, requireProtection);
|
|
1079
|
+
var counters = await _verifyCountersignatures(siOne, parsedCerts, allowWeakDigests, requireProtection);
|
|
1080
|
+
/** @internal The timestamp rows are built from the same `signature` octets the signature verdict
|
|
1081
|
+
* above was computed over, which is why they are here and not in a verb of their own. The
|
|
1082
|
+
* signer's own certificate supplies the expiry half of EN 319 122-1 clause 6.3 requirement m. */
|
|
1083
|
+
var stamps = await _verifySignatureTimestamps(siOne, _toBuf(siOne.signature, "the timestamped signature"),
|
|
1084
|
+
intrinsic.assign(intrinsic.create(null), tsTrustCfg, { signerNotAfter: _signerNotAfter(verdict) }));
|
|
585
1085
|
guard.list.append(signers, guard.verdict.of(verdict, {
|
|
586
1086
|
countersignatures: counters,
|
|
1087
|
+
signatureTimeStamps: stamps,
|
|
587
1088
|
unsignedAttrs: _surfaceUnsignedAttrs(siOne),
|
|
588
1089
|
signedAttributesPresent: !!siOne.signedAttrsBytes,
|
|
589
1090
|
signedAttributes: _surfaceSignedAttrs(siOne),
|
|
1091
|
+
cadesBaseline: _cadesBaseline(siOne, verdict && verdict.cert ? verdict.cert : null),
|
|
590
1092
|
digestAlgorithm: siOne.digestAlgorithm.name,
|
|
591
1093
|
trusted: false,
|
|
592
1094
|
}));
|
|
@@ -640,6 +1142,30 @@ function _snapshotTrust(opts) {
|
|
|
640
1142
|
};
|
|
641
1143
|
}
|
|
642
1144
|
|
|
1145
|
+
/** @internal The time-stamping authority's trust inputs, taken at the call like the signer's own and
|
|
1146
|
+
* for the same reason: they are first read inside the per-signer loop, after the signature and
|
|
1147
|
+
* countersignature checks have awaited, so an options object that changes between rows, or one
|
|
1148
|
+
* whose properties are accessors, would have two rows of one verdict judged against two different
|
|
1149
|
+
* authorities. The certificates are copied by value, since a caller holding the array could swap an
|
|
1150
|
+
* entry after the row that read it. The checker is a function and is captured as the one reference
|
|
1151
|
+
* every row calls. */
|
|
1152
|
+
function _snapshotTimestampTrust(opts, allowWeakDigests) {
|
|
1153
|
+
var certs = opts.timestampCerts;
|
|
1154
|
+
var anchors = opts.timestampTrustAnchors;
|
|
1155
|
+
return {
|
|
1156
|
+
timestampCerts: certs == null ? certs
|
|
1157
|
+
: guard.list.copyMap(intrinsic.isArray(certs) ? certs : [certs], function (c) {
|
|
1158
|
+
return _snapshotIfBytes(c, "opts.timestampCerts[]");
|
|
1159
|
+
}),
|
|
1160
|
+
timestampTrustAnchors: anchors == null ? anchors
|
|
1161
|
+
: guard.list.copyMap(intrinsic.isArray(anchors) ? anchors : [anchors], function (a) {
|
|
1162
|
+
return typeof a === "string" ? a : _cloneAnchorValue(a, 0);
|
|
1163
|
+
}),
|
|
1164
|
+
timestampRevocationChecker: opts.timestampRevocationChecker,
|
|
1165
|
+
allowWeakDigests: allowWeakDigests,
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
|
|
643
1169
|
/** @internal A signature over content is authorized by digitalSignature, contentCommitment or
|
|
644
1170
|
* nonRepudiation alike; the shared helper carries the rest of the rule. */
|
|
645
1171
|
var _permitsSigning = pkix.keyUsagePermits(pkix.makeNS("cms", CmsError, oid),
|
|
@@ -762,6 +1288,17 @@ function _addCert(out, der) {
|
|
|
762
1288
|
* (`timeStampToken`); content-type / message-digest / signing-time are
|
|
763
1289
|
* rejected here (RFC 5652 sec. 11). Use `pki.cms.countersign` for a
|
|
764
1290
|
* countersignature specifically.
|
|
1291
|
+
* @opts algorithmProtection Emit the RFC 6211 `CMSAlgorithmProtection` signed attribute, a copy of
|
|
1292
|
+
* this signer's digest and signature algorithm identifiers placed where the
|
|
1293
|
+
* signature covers them. RFC 8933 sec. 4.1 makes including it a SHOULD, and
|
|
1294
|
+
* RFC 9882 sec. 4 and RFC 9814 sec. 5 repeat that for ML-DSA and SLH-DSA,
|
|
1295
|
+
* which carry no digest identifier inside the signature. Off by default,
|
|
1296
|
+
* because turning it on changes the bytes of every signature. `true` emits
|
|
1297
|
+
* the identifier RFC 6211 sec. 2 states, which OpenSSL and Bouncy Castle
|
|
1298
|
+
* use; `"registry"` emits the S/MIME arc value the IANA registry and
|
|
1299
|
+
* draft-ietf-lamps-rfc6211-update give it. A verifier here reads both.
|
|
1300
|
+
* `false`, `null` and an absent option emit none, and any other value is
|
|
1301
|
+
* refused `cms/bad-input`.
|
|
765
1302
|
* @example
|
|
766
1303
|
* var pair = await pki.key.generate("Ed25519");
|
|
767
1304
|
* var signerKeyPkcs8 = await pki.key.export(pair.privateKey);
|
|
@@ -804,6 +1341,10 @@ var sign = cmsSign.sign;
|
|
|
804
1341
|
* is rejected, RFC 5652 sec. 11.4).
|
|
805
1342
|
* @opts sid `"issuerAndSerial"` (default) or `"ski"`.
|
|
806
1343
|
* @opts certificates Embed the countersigner certificate(s) in the SignedData. Default true.
|
|
1344
|
+
* @opts algorithmProtection Emit the RFC 6211 `CMSAlgorithmProtection` attribute on the
|
|
1345
|
+
* countersignature, copying ITS own digest and signature algorithm
|
|
1346
|
+
* identifiers rather than the countersigned signature's. Off by
|
|
1347
|
+
* default; `"registry"` selects the IANA registry identifier.
|
|
807
1348
|
* @opts pem Return a PEM string instead of a DER Buffer.
|
|
808
1349
|
* @example
|
|
809
1350
|
* var pair = await pki.key.generate("Ed25519");
|
|
@@ -817,6 +1358,12 @@ var sign = cmsSign.sign;
|
|
|
817
1358
|
*/
|
|
818
1359
|
var countersign = cmsSign.countersign;
|
|
819
1360
|
|
|
1361
|
+
/** @internal The two CAdES signature-timestamp verbs, re-exported here with `sign` and `countersign`
|
|
1362
|
+
* because this module is the `pki.cms` namespace's assembly point. Their documentation blocks live
|
|
1363
|
+
* with their implementations in `cms-sign.js`. */
|
|
1364
|
+
var timestampImprint = cmsSign.timestampImprint;
|
|
1365
|
+
var attachTimestamp = cmsSign.attachTimestamp;
|
|
1366
|
+
|
|
820
1367
|
/**
|
|
821
1368
|
* @primitive pki.cms.encrypt
|
|
822
1369
|
* @signature pki.cms.encrypt(content, recipients, opts?) -> Promise<Buffer | string>
|
|
@@ -893,6 +1440,18 @@ var encrypt = cmsEncrypt.encrypt;
|
|
|
893
1440
|
* @opts keyIdentifier `"issuerAndSerial"` (default) or `"subjectKeyIdentifier"`.
|
|
894
1441
|
* @opts oaepHash The RSAES-OAEP hash for ktri recipients wrapping the MAC key: `"sha256"` (default) / `"sha384"` / `"sha512"`.
|
|
895
1442
|
* @opts ukm User keying material (a Buffer) for kari / kemri recipients.
|
|
1443
|
+
* @opts algorithmProtection Emit the RFC 6211 `CMSAlgorithmProtection` authenticated attribute, a
|
|
1444
|
+
* copy of the digest and MAC algorithm identifiers this structure names. It matters more here
|
|
1445
|
+
* than on a signature: RFC 5652 sec. 9.2 makes the MAC input the DER encoding of `authAttrs`
|
|
1446
|
+
* alone, so `digestAlgorithm` and `macAlgorithm` are covered by nothing, and rewriting the
|
|
1447
|
+
* digest algorithm leaves the MAC verifying while the recipient recomputes the content digest
|
|
1448
|
+
* under the attacker's choice, no key required. `true` emits the identifier RFC 6211 sec. 2
|
|
1449
|
+
* states, `"registry"` the IANA registry value. Off by default, because it changes the bytes
|
|
1450
|
+
* of every message. `false`, `null` and an absent option emit none, and any other value is
|
|
1451
|
+
* refused `cms/bad-input`. With `authenticatedAttributes: false` the MAC covers the content
|
|
1452
|
+
* octets directly and no attribute set is built, so asking for the attribute there is refused
|
|
1453
|
+
* while the off forms are still accepted. `pki.cms.decrypt` reads both identifiers and
|
|
1454
|
+
* compares the copies it finds.
|
|
896
1455
|
* @opts pem Return a PEM string (`-----BEGIN CMS-----`) instead of a DER Buffer.
|
|
897
1456
|
* @example
|
|
898
1457
|
* // authenticate a message to an RSA recipient, then verify the MAC via decrypt
|
|
@@ -955,6 +1514,19 @@ var authenticate = cmsEncrypt.authenticate;
|
|
|
955
1514
|
* @opts maxIterations Lower the PBKDF2 iteration cap (a DoS bound; downward only).
|
|
956
1515
|
* @opts stream Return `content` as an async iterable of plaintext `Buffer` chunks instead of a single `Buffer`.
|
|
957
1516
|
* @opts allowUnauthenticatedRsa15 Read a message that uses RSAES-PKCS1-v1_5 key transport over a content with no integrity tag, such as AES-CBC. Refused by default with `cms/unauthenticated-rsa-v15`: the content padding is that combination's only integrity check, so decrypting it either returns the plaintext of a substituted key or tells an attacker submitting chosen ciphertexts that theirs decoded (RFC 8017 sec. 7.2). `openssl cms -encrypt` emits exactly this combination when no algorithm is named, so reading one of its messages needs this option. An AEAD content or an RSAES-OAEP recipient needs none.
|
|
1517
|
+
* @opts requireAlgorithmProtection For an AuthenticatedData, refuse one that carries no RFC 6211
|
|
1518
|
+
* `CMSAlgorithmProtection` authenticated attribute, with `cms/missing-algorithm-protection`.
|
|
1519
|
+
* Off by default, since RFC 8933 sec. 4.1 makes including it a SHOULD. A message that DOES
|
|
1520
|
+
* carry one is always held to it, with no option: its `digestAlgorithm` and `macAlgorithm`
|
|
1521
|
+
* copies are compared against the fields of the `AuthenticatedData` itself, and a disagreement
|
|
1522
|
+
* is `cms/algorithm-protection-mismatch`. That comparison runs before the MAC, because a
|
|
1523
|
+
* mismatch is a statement about the message rather than about the key. The result's
|
|
1524
|
+
* `algorithmProtection` field reports what was compared. The option is the boolean it
|
|
1525
|
+
* documents: `false`, `null` and an absent option are the off forms, and any other value is
|
|
1526
|
+
* refused `cms/bad-input`. Asking for it while decrypting any other content type is refused
|
|
1527
|
+
* the same way: RFC 8933 sec. 6 gives the attribute a place in an `AuthenticatedData` alone,
|
|
1528
|
+
* so an `EnvelopedData`, `AuthEnvelopedData` or `EncryptedData` cannot satisfy the
|
|
1529
|
+
* requirement and returning its plaintext would answer neither question.
|
|
958
1530
|
* @example
|
|
959
1531
|
* var rsa = { name: "RSA-OAEP", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" };
|
|
960
1532
|
* var pair = await pki.key.generate(rsa);
|
|
@@ -1126,4 +1698,4 @@ function isCertsOnly(input) {
|
|
|
1126
1698
|
}
|
|
1127
1699
|
|
|
1128
1700
|
// @internal
|
|
1129
|
-
module.exports = { verify: verify, sign: sign, countersign: countersign, encrypt: encrypt, authenticate: authenticate, decrypt: decrypt, compress: compress, decompress: decompress, digest: cmsDigest.digest, verifyDigest: cmsDigest.verifyDigest, certsOnly: certsOnly, parseCertsOnly: parseCertsOnly, isCertsOnly: isCertsOnly, setEngine: setEngine };
|
|
1701
|
+
module.exports = { verify: verify, sign: sign, countersign: countersign, encrypt: encrypt, authenticate: authenticate, decrypt: decrypt, compress: compress, decompress: decompress, digest: cmsDigest.digest, verifyDigest: cmsDigest.verifyDigest, certsOnly: certsOnly, parseCertsOnly: parseCertsOnly, isCertsOnly: isCertsOnly, setEngine: setEngine, timestampImprint: timestampImprint, attachTimestamp: attachTimestamp };
|