@blamejs/pki 0.8.52 → 0.8.54

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/lib/cms-sign.js CHANGED
@@ -40,13 +40,14 @@ function O(name) { return oid.byName(name); }
40
40
 
41
41
  var KNOWN_SIGN_OPTS = intrinsic.assign(intrinsic.create(null), {
42
42
  signedAttributes: 1, signingTime: 1, additionalSignedAttributes: 1, unsignedAttributes: 1,
43
+ algorithmProtection: 1,
43
44
  sid: 1, eContentType: 1, detached: 1, certificates: 1, pem: 1,
44
45
  });
45
46
  var KNOWN_SIGNER_CERT_KEYS = intrinsic.assign(intrinsic.create(null), { cert: 1, key: 1, pss: 1, digestAlgorithm: 1, combinedRsaSig: 1 });
46
47
  var KNOWN_SIGNER_KEY_ONLY_KEYS = intrinsic.assign(intrinsic.create(null), { spki: 1, keyIdentifier: 1, key: 1, pss: 1, digestAlgorithm: 1, combinedRsaSig: 1 });
47
48
  var KNOWN_COUNTERSIGN_OPTS = intrinsic.assign(intrinsic.create(null), {
48
49
  signerIndex: 1, countersignatureOf: 1, signingTime: 1, certificates: 1, pem: 1,
49
- signedAttributes: 1, additionalSignedAttributes: 1, sid: 1,
50
+ signedAttributes: 1, additionalSignedAttributes: 1, sid: 1, algorithmProtection: 1,
50
51
  });
51
52
  var NS = pkix.makeNS("cms", CmsError, oid);
52
53
  var _b = pkiBuild.makeBuilder({
@@ -118,6 +119,12 @@ var UNSIGNED_FORBIDDEN = intrinsic.create(null);
118
119
  UNSIGNED_FORBIDDEN[O("contentType")] = "content-type";
119
120
  UNSIGNED_FORBIDDEN[O("messageDigest")] = "message-digest";
120
121
  UNSIGNED_FORBIDDEN[O("signingTime")] = "signing-time";
122
+ /** @internal RFC 6211 sec. 2: the algorithm protection attribute "MUST NOT be an unsigned
123
+ * attribute", and sec. 5 adds that one placed there "does not provide any additional security".
124
+ * Both identifiers, since the two spell one attribute. The parser refuses the placement, so without
125
+ * the door here this verb would emit a message its own verifier rejects. */
126
+ UNSIGNED_FORBIDDEN[O("cmsAlgorithmProtection")] = "algorithm protection";
127
+ UNSIGNED_FORBIDDEN[O("cmsAlgorithmProtect")] = "algorithm protection";
121
128
 
122
129
  function _buildUnsignedAttrs(list) {
123
130
  if (list == null) return null;
@@ -141,6 +148,49 @@ function _buildUnsignedAttrs(list) {
141
148
  return wire;
142
149
  }
143
150
 
151
+ /** @internal The RFC 6211 attribute built from the very TLVs the structure's own fields are built
152
+ * from. Sec. 2 says the attribute "contains a copy of the SignerInfo.digestAlgorithm field ...
153
+ * including any parameters associated with it", so the copy is made from the value in hand rather
154
+ * than re-derived from a registry row: a re-derivation can differ from the field it claims to copy,
155
+ * and the parameters are the substitution target the attribute exists to pin.
156
+ *
157
+ * `armTag` is 1 for a `SignerInfo` signature algorithm and 2 for an `AuthenticatedData` MAC
158
+ * algorithm, which sec. 2 makes exclusive to their own placements. `attrOid` is the identifier the
159
+ * caller's option resolved to, taken at the door so the pair carries the one that was asked for. */
160
+ function _algorithmProtectionPair(attrOid, digestAlgId, armAlgId, armTag) {
161
+ return { type: attrOid, values: [b.sequence([digestAlgId, b.implicit(armTag, armAlgId)])] };
162
+ }
163
+
164
+ /** @internal An asked-for protection that cannot be placed is a refusal rather than a message that
165
+ * silently lacks it. The attribute rides in the signed attributes, so with none there is nowhere to
166
+ * put it, which is why RFC 8933 sec. 4.1 conditions its SHOULD on their presence. A caller who
167
+ * names both options is asking for two things that cannot both hold.
168
+ *
169
+ * The option is resolved FIRST, on every route, and the placement question asked second. Resolving
170
+ * it inside the placement test would only read it on the route that cannot place it: a streamed
171
+ * content is consumed and hashed before the attributes are built, so a misspelled value would be
172
+ * refused after the stream had been read, which for a stream that cannot be replayed is a refusal
173
+ * the caller cannot act on, and for one that never ends is no refusal at all.
174
+ *
175
+ * The emitter below reads the same option again, which is sound because the options door refuses
176
+ * one supplied through an accessor, own or inherited, before any value is read: two reads of a
177
+ * plain field are one answer. */
178
+ function _assertProtectionPlaceable(opts) {
179
+ var wanted = cms.algorithmProtectionOid(opts.algorithmProtection, _err);
180
+ if (opts.signedAttributes === false && wanted) {
181
+ throw _err("cms/bad-input", "algorithmProtection needs signed attributes to ride in, and " +
182
+ "signedAttributes: false signs the content directly (RFC 6211 sec. 2 places the attribute in " +
183
+ "SignerInfo.signedAttrs); drop one of the two");
184
+ }
185
+ }
186
+
187
+ /** @internal The producer door, applied to this verb's attribute records. The policy itself lives in
188
+ * `schema-cms`, so the signing verbs and the authenticate verb enforce one rule rather than three
189
+ * copies of it. */
190
+ function _assertAlgorithmProtectionPairs(pairs, digestAlgId, armAlgId, armTag) {
191
+ cms.assertAlgorithmProtectionEmission(pairs, b.sequence([digestAlgId, b.implicit(armTag, armAlgId)]), _err);
192
+ }
193
+
144
194
  function _resolveSignerContext(signer, opts) {
145
195
  var so = signer || {};
146
196
  var keyOnly = so.cert == null && so.spki != null;
@@ -166,7 +216,13 @@ async function _finishSignerInfo(rc, md, content, eContentType, opts) {
166
216
  { type: O("messageDigest"), values: [b.octetString(md)] },
167
217
  ];
168
218
  if (opts.signingTime !== false) intrinsic.push(pairs, { type: O("signingTime"), values: [_timeValue(opts.signingTime)] });
219
+ var apOid = cms.algorithmProtectionOid(opts.algorithmProtection, _err);
220
+ if (apOid) {
221
+ intrinsic.push(pairs, _algorithmProtectionPair(apOid,
222
+ rc.scheme.digestAlgId, rc.scheme.sigAlgId, 1));
223
+ }
169
224
  pairs = intrinsic.concat(pairs, _resolveAttrPairs(opts.additionalSignedAttributes, "a signed attribute value"));
225
+ _assertAlgorithmProtectionPairs(pairs, rc.scheme.digestAlgId, rc.scheme.sigAlgId, 1);
170
226
  toSign = _buildSignedAttrs(pairs);
171
227
  }
172
228
  var signedBytes = toSign.setOf ? toSign.setOf : toSign;
@@ -290,6 +346,7 @@ function _signDispatch(content, signers, opts) {
290
346
  async function _signStream(stream, signers, opts) {
291
347
  opts = guard.identifier.optionsObject(opts, _err, "cms/bad-input", "pki.cms.sign options");
292
348
  guard.identifier.assertKnownKeys(opts, KNOWN_SIGN_OPTS, _err, "cms/bad-input", "unknown opts field ");
349
+ _assertProtectionPlaceable(opts);
293
350
  if (opts.detached !== true) {
294
351
  throw _err("cms/bad-input", "a streaming (async-iterable) content requires opts.detached: true; an attached " +
295
352
  "SignedData embeds the content as a definite-length OCTET STRING, which cannot be produced without buffering it");
@@ -329,6 +386,7 @@ async function _signStream(stream, signers, opts) {
329
386
  function _sign(content, signers, opts) {
330
387
  opts = guard.identifier.optionsObject(opts, _err, "cms/bad-input", "pki.cms.sign options");
331
388
  guard.identifier.assertKnownKeys(opts, KNOWN_SIGN_OPTS, _err, "cms/bad-input", "unknown opts field ");
389
+ _assertProtectionPlaceable(opts);
332
390
  var contentBuf = _toBuf(content, "content");
333
391
  var list = Array.isArray(signers) ? _b.reqDenseArray(signers, "the signer list") : [signers];
334
392
  if (!list.length) throw _err("cms/bad-input", "pki.cms.sign requires at least one signer");
@@ -465,9 +523,15 @@ function _buildCountersignature(targetSigOctets, countersigner, opts) {
465
523
  return _digest(scheme.digest, targetSigOctets).then(function (md) {
466
524
  var pairs = [{ type: O("messageDigest"), values: [b.octetString(md)] }];
467
525
  if (opts.signingTime !== false) pairs.push({ type: O("signingTime"), values: [_timeValue(opts.signingTime)] });
526
+ var csApOid = cms.algorithmProtectionOid(opts.algorithmProtection, _err);
527
+ if (csApOid) {
528
+ intrinsic.push(pairs, _algorithmProtectionPair(csApOid, scheme.digestAlgId, scheme.sigAlgId, 1));
529
+ }
468
530
  var extra = _resolveAttrPairs(opts.additionalSignedAttributes, "a countersignature signed attribute value");
469
531
  extra.forEach(function (p) { if (p.type === O("contentType")) throw _err("cms/bad-input", "a countersignature must not carry a content-type attribute (RFC 5652 sec. 11.4)"); });
470
- return _buildSignedAttrs(pairs.concat(extra));
532
+ var all = intrinsic.concat(pairs, extra);
533
+ _assertAlgorithmProtectionPairs(all, scheme.digestAlgId, scheme.sigAlgId, 1);
534
+ return _buildSignedAttrs(all);
471
535
  });
472
536
  }).then(function (attrs) {
473
537
  var preimage = attrs ? attrs.setOf : targetSigOctets;
@@ -559,6 +623,7 @@ function countersign(cmsInput, signers, opts) {
559
623
  function _countersign(cmsInput, signers, opts) {
560
624
  opts = guard.identifier.optionsObject(opts, _err, "cms/bad-input", "pki.cms.countersign options");
561
625
  guard.identifier.assertKnownKeys(opts, KNOWN_COUNTERSIGN_OPTS, _err, "cms/bad-input", "unknown opts field ");
626
+ _assertProtectionPlaceable(opts);
562
627
  var list = Array.isArray(signers) ? signers : [signers];
563
628
  if (!list.length) throw _err("cms/bad-input", "pki.cms.countersign requires at least one countersigner");
564
629
  if (opts.signingTime != null && opts.signingTime !== false) guard.time.assertEncodable(opts.signingTime, _err, "cms/bad-input", "signingTime");
package/lib/cms-verify.js CHANGED
@@ -232,23 +232,22 @@ function _hashAlgOid(seq) {
232
232
  return o;
233
233
  }
234
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. */
235
239
  function _resolvePss(paramsBytes) {
236
240
  if (!paramsBytes) return null;
237
- var n;
238
- try {
239
- n = asn1.decode(paramsBytes);
240
- } catch (_e) { return null; }
241
- if (n.tagClass !== "universal" || n.tagNumber !== asn1.TAGS.SEQUENCE || !n.children) return null;
242
- var hash = null, saltLen = null, mgfNode = null, trailer = 1n, last = -1;
243
- for (var i = 0; i < n.children.length; i++) {
244
- var f = n.children[i];
245
- if (f.tagClass !== "context" || f.tagNumber > 3 || f.tagNumber <= last || !f.children || f.children.length !== 1) return null;
246
- 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];
247
246
  try {
248
- if (f.tagNumber === 0) { hash = PSS_HASH[_hashAlgOid(f.children[0])]; if (!hash) return null; }
249
- else if (f.tagNumber === 1) { mgfNode = f.children[0]; }
250
- else if (f.tagNumber === 2) { saltLen = asn1.read.integer(f.children[0]); }
251
- else { trailer = asn1.read.integer(f.children[0]); }
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); }
252
251
  } catch (_e3) { return null; }
253
252
  }
254
253
  if (hash === null || mgfNode === null) return null;
@@ -464,6 +463,54 @@ function _surfaceSignedAttrs(si) {
464
463
  });
465
464
  }
466
465
 
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
+
467
514
  /** @internal The CAdES baseline requirements this verb decides from one SignerInfo's own signed
468
515
  * attributes, reported beside the signature verdict rather than folded into it: a signature can be
469
516
  * cryptographically sound and still not be a CAdES signature, and the two answers are separate.
@@ -784,7 +831,7 @@ function _signerNotAfter(verdict) {
784
831
  catch (_e) { return null; }
785
832
  }
786
833
 
787
- async function _verifyCountersignatures(si, parsedCerts, allowWeakDigests) {
834
+ async function _verifyCountersignatures(si, parsedCerts, allowWeakDigests, requireProtection) {
788
835
  var targetSig = _toBuf(si.signature, "the countersigned signature");
789
836
  var values = [];
790
837
  intrinsic.forEach(si.unsignedAttrs || [], function (a) {
@@ -792,17 +839,24 @@ async function _verifyCountersignatures(si, parsedCerts, allowWeakDigests) {
792
839
  intrinsic.forEach(a.values, function (v) { guard.list.append(values, v && v.bytes ? v.bytes : v); });
793
840
  });
794
841
  var out = [];
795
- 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));
796
843
  return out;
797
844
  }
798
845
 
799
- async function _verifyOneCountersig(vDer, targetSig, parsedCerts, allowWeakDigests) {
846
+ async function _verifyOneCountersig(vDer, targetSig, parsedCerts, allowWeakDigests, requireProtection) {
800
847
  var csSi;
801
848
  try { csSi = cms.walkCountersignature(asn1.decode(intrinsic.isBuffer(vDer) ? vDer : intrinsic.bufferFrom(vDer))); }
802
849
  catch (e) { return _row({ ok: false, code: (e instanceof CmsError ? e.code : "cms/bad-countersignature"), message: e && e.message }); }
803
- var verdict = await _verifyOne(csSi, targetSig, null, parsedCerts, targetSig, undefined, allowWeakDigests);
804
- var nested = await _verifyCountersignatures(csSi, parsedCerts, allowWeakDigests);
805
- var head = { ok: verdict.ok, sid: verdict.sid, cert: verdict.cert, digestAlgorithm: csSi.digestAlgorithm.name };
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 };
806
860
  return guard.verdict.of(
807
861
  verdict.code ? guard.verdict.of(head, { code: verdict.code }) : head,
808
862
  { unsignedAttrs: _surfaceUnsignedAttrs(csSi), countersignatures: nested });
@@ -822,7 +876,7 @@ async function _verifyOneCountersig(vDer, targetSig, parsedCerts, allowWeakDiges
822
876
  * Verify a CMS SignedData signature (RFC 5652 sec. 5). `input` is a PEM string, a DER
823
877
  * `Buffer`, or a parsed `pki.schema.cms` object. Returns `{ valid, trusted, eContentType, eContent, signers }`
824
878
  * (`eContent` the encapsulated content when present, else null) where each `signers[i]` is
825
- * `{ ok, sid, cert, trusted, signedAttributesPresent, signedAttributes, unsignedAttrs, digestAlgorithm, countersignatures, signatureTimeStamps, cadesBaseline }`
879
+ * `{ ok, sid, cert, trusted, signedAttributesPresent, signedAttributes, unsignedAttrs, digestAlgorithm, countersignatures, signatureTimeStamps, cadesBaseline, algorithmProtection }`
826
880
  * (`cert` the matched signer certificate DER) or carries a `code` on a structural failure; `valid` is true when
827
881
  * there is at least one signer and every signer verified.
828
882
  *
@@ -873,6 +927,15 @@ async function _verifyOneCountersig(vDer, targetSig, parsedCerts, allowWeakDiges
873
927
  * content, and a token over it answers for either. `pki.cms.attachTimestamp` adds such an attribute to an
874
928
  * existing message and `pki.cms.timestampImprint` produces the imprint a TSA signs.
875
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
+ *
876
939
  * `cadesBaseline` is a third claim, `{ conformant, signingCertificateAttribute, findings,
877
940
  * requirementsChecked }`: a signature can be sound and still not be a CAdES signature.
878
941
  * `requirementsChecked` lists the EN 319 122-1 requirements decided here, each unmet one carrying a
@@ -908,6 +971,14 @@ async function _verifyOneCountersig(vDer, targetSig, parsedCerts, allowWeakDiges
908
971
  * trust decision from the signer: an operator may anchor one and not the other.
909
972
  * @opts timestampRevocationChecker A revocation checker for the TSA chain, as `pki.path.validate`
910
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`.
911
982
  * @example
912
983
  * var pair = await pki.key.generate("Ed25519");
913
984
  * var key = await pki.key.export(pair.privateKey);
@@ -950,7 +1021,8 @@ var _VERIFY_OPTS = intrinsic.assign(intrinsic.create(null), { certs: 1, content:
950
1021
  * authority is a different trust decision from the signer's: an operator may anchor the TSA while
951
1022
  * leaving the signer unanchored, or the reverse, and one `trustAnchors` for both would make those
952
1023
  * two answers one. */
953
- timestampCerts: 1, timestampTrustAnchors: 1, timestampRevocationChecker: 1 });
1024
+ timestampCerts: 1, timestampTrustAnchors: 1, timestampRevocationChecker: 1,
1025
+ requireAlgorithmProtection: 1 });
954
1026
 
955
1027
  function verify(input, opts) {
956
1028
  return guard.async.deferred(function () { return _verify(input, opts); });
@@ -959,12 +1031,19 @@ function verify(input, opts) {
959
1031
  async function _verify(input, opts) {
960
1032
  opts = guard.identifier.optionsObject(opts, _err, "cms/bad-input", "pki.cms.verify options");
961
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");
962
1042
  var parsed = guard.parsed.acceptDerived(input, "cms", function (bytes) {
963
1043
  return cms.parse(_snapshotIfBytes(bytes, "pki.cms.verify"));
964
1044
  }, _err, "cms/bad-input", "the SignedData");
965
1045
  if (!Array.isArray(parsed.signerInfos)) throw _err("cms/bad-input", "input is not a CMS SignedData");
966
1046
  var trustCfg = _snapshotTrust(opts);
967
- var allowWeakDigests = opts.allowWeakDigests === true;
968
1047
  var tsTrustCfg = _snapshotTimestampTrust(opts, allowWeakDigests);
969
1048
  var parsedCerts = [];
970
1049
  (parsed.certificates || []).forEach(function (c) { _addCert(parsedCerts, c && c.bytes ? c.bytes : c); });
@@ -991,8 +1070,13 @@ async function _verify(input, opts) {
991
1070
  var signers = [];
992
1071
  for (var _sp = 0; _sp < parsed.signerInfos.length; _sp++) {
993
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);
994
1077
  var verdict = await _verifyOne(siOne, content, eContentType, parsedCerts, null, digestByName, allowWeakDigests);
995
- var counters = await _verifyCountersignatures(siOne, parsedCerts, allowWeakDigests);
1078
+ verdict = _applyAlgorithmProtection(verdict, apReport, requireProtection);
1079
+ var counters = await _verifyCountersignatures(siOne, parsedCerts, allowWeakDigests, requireProtection);
996
1080
  /** @internal The timestamp rows are built from the same `signature` octets the signature verdict
997
1081
  * above was computed over, which is why they are here and not in a verb of their own. The
998
1082
  * signer's own certificate supplies the expiry half of EN 319 122-1 clause 6.3 requirement m. */
@@ -1204,6 +1288,17 @@ function _addCert(out, der) {
1204
1288
  * (`timeStampToken`); content-type / message-digest / signing-time are
1205
1289
  * rejected here (RFC 5652 sec. 11). Use `pki.cms.countersign` for a
1206
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`.
1207
1302
  * @example
1208
1303
  * var pair = await pki.key.generate("Ed25519");
1209
1304
  * var signerKeyPkcs8 = await pki.key.export(pair.privateKey);
@@ -1246,6 +1341,10 @@ var sign = cmsSign.sign;
1246
1341
  * is rejected, RFC 5652 sec. 11.4).
1247
1342
  * @opts sid `"issuerAndSerial"` (default) or `"ski"`.
1248
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.
1249
1348
  * @opts pem Return a PEM string instead of a DER Buffer.
1250
1349
  * @example
1251
1350
  * var pair = await pki.key.generate("Ed25519");
@@ -1341,6 +1440,18 @@ var encrypt = cmsEncrypt.encrypt;
1341
1440
  * @opts keyIdentifier `"issuerAndSerial"` (default) or `"subjectKeyIdentifier"`.
1342
1441
  * @opts oaepHash The RSAES-OAEP hash for ktri recipients wrapping the MAC key: `"sha256"` (default) / `"sha384"` / `"sha512"`.
1343
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.
1344
1455
  * @opts pem Return a PEM string (`-----BEGIN CMS-----`) instead of a DER Buffer.
1345
1456
  * @example
1346
1457
  * // authenticate a message to an RSA recipient, then verify the MAC via decrypt
@@ -1403,6 +1514,19 @@ var authenticate = cmsEncrypt.authenticate;
1403
1514
  * @opts maxIterations Lower the PBKDF2 iteration cap (a DoS bound; downward only).
1404
1515
  * @opts stream Return `content` as an async iterable of plaintext `Buffer` chunks instead of a single `Buffer`.
1405
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.
1406
1530
  * @example
1407
1531
  * var rsa = { name: "RSA-OAEP", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" };
1408
1532
  * var pair = await pki.key.generate(rsa);
package/lib/crmf-sign.js CHANGED
@@ -943,7 +943,7 @@ async function _assertEnclosedKeyCorresponds(privateKeyDer, templateSpki) {
943
943
  * failing, not the input. */
944
944
  if (e && e.isPkiError === true && e.code === "key/bad-input") {
945
945
  throw _err("crmf/bad-input", "pop.privateKey could not be read as a private key to check against " +
946
- "certTemplate.publicKey: " + (e.message || e), e);
946
+ "certTemplate.publicKey: " + guard.text.describeThrown(e), e);
947
947
  }
948
948
  throw _err("crmf/bad-popo", "the private key enclosed in the encryptedKey proof of possession could not be " +
949
949
  "exercised with certTemplate.publicKey, so it cannot be shown to be that key's private half (RFC 4211 sec. 4.2)", e);
package/lib/ct.js CHANGED
@@ -2177,7 +2177,7 @@ function _b64Array(obj, name, code, label, exactLen, maxCount) {
2177
2177
  * @opts headers OPTIONAL extra request headers (the request-framing headers are stripped; the verb owns the GET method).
2178
2178
  * @opts timeout OPTIONAL ms budget, default 30s (cap-validated).
2179
2179
  * @opts maxResponseBytes OPTIONAL per-GET size cap, default 4 MiB, tightenable DOWNWARD only.
2180
- * @opts requireJsonContentType OPTIONAL boolean (default false) -- opt in to a strict `ct/bad-content-type` gate on the JSON GET.
2180
+ * @opts requireJsonContentType OPTIONAL boolean (default false) -- opt in to a strict `ct/bad-content-type` gate on the JSON GET. A value other than `true`, `false`, `null` or an absent option is refused `ct/bad-input` before the request goes out.
2181
2181
  * @example
2182
2182
  * // a live distributor uses the default pki.transport.https; here an injected transport returns the pair
2183
2183
  * var r = await pki.ct.fetchLogList({ url: "https://ct.example/log_list.json", signerKey: googleSignerSpki,
@@ -2241,7 +2241,8 @@ async function fetchLogList(opts) {
2241
2241
  var req = { headers: headers, tls: fetchTls, timeout: timeout, maxResponseBytes: maxResponseBytes };
2242
2242
  /** @internal Read before the request goes out, so the option that decides the check is the one the call
2243
2243
  * was made with rather than whatever it says once the response is back. */
2244
- var requireJsonContentType = opts.requireJsonContentType === true;
2244
+ var requireJsonContentType = guard.identifier.booleanOption(opts.requireJsonContentType, _ctErr,
2245
+ "ct/bad-input", "fetchLogList opts.requireJsonContentType");
2245
2246
  var jsonRes = await _fetchBody(transport, jsonUrl, req, "CT log-list JSON");
2246
2247
  if (requireJsonContentType && jsonRes.contentType !== "application/json") throw _ctErr("ct/bad-content-type", "the CT log-list JSON GET returned content-type " + JSON.stringify(jsonRes.contentType || null) + " (opts.requireJsonContentType is set)");
2247
2248
  var sigRes = await _fetchBody(transport, sigUrl, req, "CT log-list signature");