@blamejs/pki 0.8.52 → 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/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/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");
package/lib/guard-der.js CHANGED
@@ -96,6 +96,31 @@ function paramsAbsentOrNull(params) {
96
96
  return params === null || params === undefined || isNull(params);
97
97
  }
98
98
 
99
+ /** Whether two decoded `AlgorithmIdentifier` records name the same algorithm with the same
100
+ * parameters, comparing the parameters AS THEY ARE: absent equals absent, and an explicit DER NULL
101
+ * is a different identifier from one with no parameters field.
102
+ *
103
+ * This is the comparison for an identifier whose parameters carry meaning. A digest identifier has
104
+ * the opposite rule, because RFC 3370 clause 2.1 and RFC 5754 clause 2 require an implementation to
105
+ * accept both spellings of the same digest, and `schema-cms.sameDigestAlgorithm` grants that. A
106
+ * signature identifier does not: RFC 4055 requires the `*WithRSAEncryption` identifiers to carry a
107
+ * NULL, RFC 5758 and RFC 8410 require the `ecdsaWith*` and the Edwards and post-quantum ones to omit
108
+ * the field, and RSASSA-PSS carries its hash, mask generator, salt length and trailer field there
109
+ * under RFC 4056 clause 2.2. Collapsing the two spellings for one of those is admitting a different
110
+ * algorithm, which is why the grant does not travel.
111
+ *
112
+ * @enforced-by behavioral -- an equality that is too permissive has no rename-proof shape; the RED
113
+ * vector is an identifier pair differing only in an explicit NULL, which must compare unequal here
114
+ * and equal through the digest helper.
115
+ */
116
+ function sameAlgorithmExactly(a, c) {
117
+ if (!a || !c || a.oid !== c.oid) return false;
118
+ var ap = a.parameters === undefined ? null : a.parameters;
119
+ var cp = c.parameters === undefined ? null : c.parameters;
120
+ if (ap === null || cp === null) return ap === cp;
121
+ return _isBuffer(ap) && _isBuffer(cp) && _sizeOf(ap) === _sizeOf(cp) && _compare(ap, cp) === 0;
122
+ }
123
+
99
124
  // @enforced-by behavioral -- see paramsAbsentOrNull.
100
125
  function assertParams(params, shape, E, code, label) {
101
126
  var ok = shape === "null" ? isNull(params) : (shape === "absent"
@@ -109,4 +134,5 @@ function assertParams(params, shape, E, code, label) {
109
134
  module.exports = _freeze({
110
135
  element: element, tlv: tlv,
111
136
  isNull: isNull, paramsAbsentOrNull: paramsAbsentOrNull, assertParams: assertParams,
137
+ sameAlgorithmExactly: sameAlgorithmExactly,
112
138
  });
@@ -600,6 +600,32 @@ function assertCallableOption(opts, key, E, code, label, shape) {
600
600
  return assertCallable(opts[key], E, code, label, shape);
601
601
  }
602
602
 
603
+ /** @internal How a switch is read: as the boolean it documents, with an absent value standing for
604
+ * off and every other value refused. A switch that turns a requirement on is the case that decides
605
+ * the shape. `requireRevocation: "true"` out of a config file is truthy to JavaScript and unequal
606
+ * to `true`, so a boundary testing `=== true` runs without the requirement and says nothing, while
607
+ * one testing truthiness turns the requirement on for `0` and `""` as well. The value is rendered
608
+ * from its own type rather than coerced, so a caller value with a throwing `toString` reports the
609
+ * typed refusal. A boundary that reads `null` as a value its caller supplied keeps its own check:
610
+ * this one reads it as absence. */
611
+ // @enforced-by behavioral -- reading a caller switch has no rename-proof code shape; `=== true` is
612
+ // @enforced-by behavioral -- the correct read for a switch that turns a control OFF, so a lexical
613
+ // @enforced-by behavioral -- rule would flag the safe direction. The per-verb vectors are the
614
+ // @enforced-by behavioral -- guard: each drives the shipped verb with a non-boolean and asserts the
615
+ // @enforced-by behavioral -- typed refusal, and with false and absence to pin the two off forms.
616
+ function booleanOption(value, E, code, label) {
617
+ if (value === undefined || value === null) return false;
618
+ if (value === true || value === false) return value;
619
+ throw E(code, label + " must be true or false, got " + _showSwitch(value));
620
+ }
621
+
622
+ /** @internal The value's own type, for the message above. Nothing here calls into the value. */
623
+ function _showSwitch(value) {
624
+ if (typeof value === "string") return _stringify(value);
625
+ if (typeof value === "number" || typeof value === "bigint") return _String(value);
626
+ return typeof value;
627
+ }
628
+
603
629
  /** @internal Whether a caller's object says something about itself, as opposed to inheriting the
604
630
  * answer. A declaration is a promise the author of that object made, so it has to sit on the object
605
631
  * they wrote: reading the flag through the object finds a value on `Function.prototype` that every
@@ -619,6 +645,7 @@ function declaresOwn(obj, name) {
619
645
 
620
646
  module.exports = intrinsic.freeze({
621
647
  assertCallable: assertCallable,
648
+ booleanOption: booleanOption,
622
649
  declaresOwn: declaresOwn,
623
650
  assertCallableOption: assertCallableOption,
624
651
  assertCanonicalOid: assertCanonicalOid,
package/lib/lint.js CHANGED
@@ -4929,12 +4929,21 @@ var ALL_CRL_RULES = RFC5280_CRL_RULES.concat(CNSA2_CRL_RULES);
4929
4929
  * for YY >= 50 and 20YY otherwise, so every UTCTime names a year inside 1950..2049 and only the
4930
4930
  * GeneralizedTime direction of that clause can be broken. */
4931
4931
  var OID_SIGNING_TIME = oid.byName("signingTime");
4932
+ /** @internal The two identifiers RFC 6211's attribute is assigned, both read: the body's PKCS 9 leaf
4933
+ * and the S/MIME attribute arc value the IANA registry and the update draft give it. */
4934
+ var OID_ALG_PROTECTION = oid.byName("cmsAlgorithmProtection");
4935
+ var OID_ALG_PROTECTION_AA = oid.byName("cmsAlgorithmProtect");
4932
4936
  var OID_SIGNED_DATA = oid.byName("signedData");
4933
4937
 
4934
4938
  /** @internal The content type decides which of these rows apply, and the parser sets `contentType`
4935
4939
  * as an own property on every content it builds. A content type that carries no signers has no own
4936
4940
  * `signerInfos`, so testing for that field answers from whatever the prototype chain holds. */
4937
4941
  function _isSignedData(cms) { return cms.contentType === OID_SIGNED_DATA; }
4942
+ var OID_AUTH_DATA = oid.byName("authData");
4943
+ /** @internal The other content type whose originator carries attributes, and the one whose algorithm
4944
+ * fields nothing else covers: RFC 5652 sec. 9.2 makes the MAC input the DER encoding of `authAttrs`
4945
+ * alone, so `digestAlgorithm` and `macAlgorithm` sit outside it. */
4946
+ function _isAuthData(cms) { return cms.contentType === OID_AUTH_DATA; }
4938
4947
  var OID_COUNTERSIGNATURE = oid.byName("countersignature");
4939
4948
  /** @internal Every SignerInfo a message carries: its own signers, and the countersignatures their
4940
4949
  * unsigned attributes hold, which sec. 11.4 defines as SignerInfo values and which carry their own
@@ -4969,14 +4978,16 @@ function _cmsAllSignerInfos(cms) {
4969
4978
  visit(cms.signerInfos, 0, false, 0);
4970
4979
  return out;
4971
4980
  }
4972
- function _cmsSignedAttr(signerInfo, dotted) {
4973
- var attrs = signerInfo.signedAttrs;
4981
+ function _cmsAttrInList(attrs, dotted) {
4974
4982
  if (!Array.isArray(attrs)) return null;
4975
4983
  for (var i = 0; i < attrs.length; i++) {
4976
4984
  if (attrs[i].type === dotted) return attrs[i];
4977
4985
  }
4978
4986
  return null;
4979
4987
  }
4988
+ function _cmsSignedAttr(signerInfo, dotted) {
4989
+ return _cmsAttrInList(signerInfo.signedAttrs, dotted);
4990
+ }
4980
4991
  /** @internal Whether two DIGEST AlgorithmIdentifiers name one algorithm, under RFC 5754 sec. 2. The
4981
4992
  * rule lives in the schema module because the set this row reads is also WRITTEN there, by the verb
4982
4993
  * that adds a countersigner's digest to it, and a set written under one rule and read under another
@@ -5078,8 +5089,50 @@ var RFC5652_CMS_RULES = [
5078
5089
  },
5079
5090
  ];
5080
5091
 
5081
- var CMS_PROFILES = Object.assign(Object.create(null), { "rfc5652": RFC5652_CMS_RULES });
5082
- var ALL_CMS_RULES = RFC5652_CMS_RULES;
5092
+ /** @internal RFC 8933 updates RFC 5652 rather than restating it, so its rows are their own profile:
5093
+ * `pki.lint.cms(der, { profile: "rfc8933" })` asks only what that document added. */
5094
+ var RFC8933_CMS_RULES = [
5095
+ {
5096
+ id: "lint/rfc8933/algorithm-protection-absent", severity: "notice", source: "rfc8933", citation: "RFC 8933 4.1",
5097
+ /** @internal "the originator of a signed-data content type that includes signed attributes
5098
+ * SHOULD include the CMSAlgorithmProtection attribute [RFC6211] as one of the signed
5099
+ * attributes. Likewise, the originator of an authenticated-data content type that includes
5100
+ * authenticated attributes SHOULD include the CMSAlgorithmProtection attribute [RFC6211] as one
5101
+ * of the authenticated attributes." RFC 9882 sec. 4 and RFC 9814 sec. 5 repeat the signed-data
5102
+ * half for ML-DSA and SLH-DSA. Both content types are read, and the authenticated-data one is
5103
+ * where the exposure is widest: sec. 9.2 makes the MAC input `authAttrs` alone, so the two outer
5104
+ * algorithm fields are covered by nothing. The SHOULD is conditioned on attributes being
5105
+ * present, so an originator that signs or authenticates the content directly has nowhere to
5106
+ * carry it and draws no finding. Both identifiers count, since the two spell one attribute. A
5107
+ * notice rather than a warning: it is a SHOULD, and most CMS in the field carries no such
5108
+ * attribute. */
5109
+ message: "an originator with signed or authenticated attributes carries no CMSAlgorithmProtection attribute, which this section says it should include",
5110
+ appliesTo: function (cms) { return _isSignedData(cms) || _isAuthData(cms); },
5111
+ check: function (cms) {
5112
+ var out = [];
5113
+ if (_isAuthData(cms)) {
5114
+ var authAttrs = cms.authAttrs;
5115
+ if (!Array.isArray(authAttrs) || !authAttrs.length) return null;
5116
+ if (_cmsAttrInList(authAttrs, OID_ALG_PROTECTION) ||
5117
+ _cmsAttrInList(authAttrs, OID_ALG_PROTECTION_AA)) return null;
5118
+ return { context: { placement: "authAttrs" } };
5119
+ }
5120
+ _cmsAllSignerInfos(cms).forEach(function (entry) {
5121
+ if (!Array.isArray(entry.signerInfo.signedAttrs) || !entry.signerInfo.signedAttrs.length) return;
5122
+ if (_cmsSignedAttr(entry.signerInfo, OID_ALG_PROTECTION) ||
5123
+ _cmsSignedAttr(entry.signerInfo, OID_ALG_PROTECTION_AA)) return;
5124
+ out.push({ context: { signerIndex: entry.signerIndex, countersignature: entry.countersignature } });
5125
+ });
5126
+ return out.length ? out : null;
5127
+ },
5128
+ },
5129
+ ];
5130
+
5131
+ var CMS_PROFILES = Object.assign(Object.create(null), {
5132
+ "rfc5652": RFC5652_CMS_RULES,
5133
+ "rfc8933": RFC8933_CMS_RULES,
5134
+ });
5135
+ var ALL_CMS_RULES = RFC5652_CMS_RULES.concat(RFC8933_CMS_RULES);
5083
5136
 
5084
5137
  /** @internal The RFC 3161 profile is the shortest of the three, for the reason the CMS one is
5085
5138
  * short: `schema.tsp.parseToken` refuses nearly everything sec. 2.4.2 requires of a token before a
@@ -6978,9 +7031,10 @@ function _ingestCrl(input) {
6978
7031
  * `lint/rfc2986/extension-request-ambiguous`; every one of them is read, so nothing a later
6979
7032
  * attribute asks for escapes the rows below.
6980
7033
  *
6981
- * @opts profile `"rfc2986"` for the structural rows alone, or `"cabf-tls"` to add the TLS rows
6982
- * (default runs the structural rows). A certificate, CRL or OCSP profile name is
6983
- * refused rather than run against a request.
7034
+ * @opts profile `"rfc2986"` for the structural rows alone, `"cabf-tls"` to add the TLS rows, or
7035
+ * `"rfc9883"` for the rows about a request carrying a possession statement
7036
+ * (default runs the structural rows and the rfc9883 rows). A certificate, CRL
7037
+ * or OCSP profile name is refused rather than run against a request.
6984
7038
  * @opts severity Suppress findings below this floor (default `"notice"`). `counts` and `worst`
6985
7039
  * always reflect the complete, unfiltered result.
6986
7040
  * @example
@@ -7217,8 +7271,9 @@ function _ingestCms(input) {
7217
7271
  * `pki.cms.verify`'s, and the certificates a message carries are linted with
7218
7272
  * `pki.lint.certificate`.
7219
7273
  *
7220
- * @opts profile `"rfc5652"`, which is also what the default runs. A certificate, CRL or OCSP
7221
- * profile name is refused rather than run against a message.
7274
+ * @opts profile `"rfc5652"`, or `"rfc8933"` for the rows that update it (default runs both).
7275
+ * A certificate, CRL or OCSP profile name is refused rather than run against a
7276
+ * message.
7222
7277
  * @opts severity Suppress findings below this floor (default `"notice"`). `counts` and `worst`
7223
7278
  * always reflect the complete, unfiltered result.
7224
7279
  * @example
@@ -7517,10 +7572,11 @@ function rules(profile, artifact) {
7517
7572
  * and `"default"`, are not among them.
7518
7573
  * @example
7519
7574
  * pki.lint.profiles();
7520
- * // -> ["rfc5280", "rfc9881", "rfc9909", "rfc9935", "cabf-tls", "cabf-smime", "cabf-cs",
7521
- * // "etsi-qc", "mozilla-root", "chrome-root", "microsoft-root", "apple-root",
7522
- * // "nist-pqc-transition", "cnsa-2.0", "rfc2986", "rfc5280-crl", "rfc6960", "rfc5019",
7523
- * // "rfc5652", "rfc3161", "rfc5755"]
7575
+ * // -> ["rfc5280", "rfc9881", "rfc9909", "rfc9935", "rfc9763", "rfc7633", "x509-altsig",
7576
+ * // "cabf-tls", "cabf-smime", "cabf-cs", "etsi-qc", "mozilla-root", "chrome-root",
7577
+ * // "microsoft-root", "apple-root", "nist-pqc-transition", "cnsa-2.0", "rfc2986",
7578
+ * // "rfc9883", "rfc5280-crl", "rfc6960", "rfc5019", "rfc5652", "rfc8933", "rfc3161",
7579
+ * // "rfc5755"]
7524
7580
  */
7525
7581
  /** @internal `cabf-tls` names the certificate profile in this list and is not repeated for the
7526
7582
  * request form: `pki.lint.csr` takes the same name and runs those rows against what a request
package/lib/oid.js CHANGED
@@ -232,7 +232,14 @@ var FAMILIES = Object.assign(Object.create(null), {
232
232
  pkcs9: { base: [1, 2, 840, 113549, 1, 9], of: {
233
233
  emailAddress: 1, contentType: 3, messageDigest: 4, signingTime: 5,
234
234
  countersignature: 6, challengePassword: 7, extensionRequest: 14,
235
- smimeCapabilities: 15, friendlyName: 20, localKeyId: 21 } },
235
+ smimeCapabilities: 15, friendlyName: 20, localKeyId: 21,
236
+ /** @internal RFC 6211 sec. 2 and Appendix A both state the CMS algorithm protection attribute
237
+ * here, directly under PKCS 9, and that is the value OpenSSL and Bouncy Castle register. The
238
+ * same document's sec. 4 says "All identifiers are assigned out of the S/MIME OID arc", which
239
+ * the IANA registry, errata 9144 and 9145, and draft-ietf-lamps-rfc6211-update-03 read as
240
+ * `1.2.840.113549.1.9.16.2.52`, registered beside it as `cmsAlgorithmProtect`. Both are
241
+ * accepted on parse, because a mismatched identifier is equivalent to no protection. */
242
+ cmsAlgorithmProtection: 52 } },
236
243
 
237
244
  keyPackageContentTypes: { base: [2, 16, 840, 1, 101, 2, 1, 2, 78], of: { aKeyPackage: 5 } },
238
245
 
@@ -324,6 +331,9 @@ var FAMILIES = Object.assign(Object.create(null), {
324
331
  cmcUnsignedData: 34,
325
332
  binarySigningTime: 46,
326
333
  asymmDecryptKeyID: 54, relatedCertRequest: 60,
334
+ /** @internal The second value the CMS algorithm protection attribute is identified by. See
335
+ * `pkcs9.cmsAlgorithmProtection` for which document states which, and why both are read. */
336
+ cmsAlgorithmProtect: 52,
327
337
  certificationRequestInfoTemplate: 61, extensionReqTemplate: 62 } },
328
338
 
329
339
  ansiX962: { base: [1, 2, 840, 10045], of: {