@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/schema-cms.js CHANGED
@@ -60,6 +60,11 @@ var NS = pkix.makeNS("cms", CmsError, oid);
60
60
 
61
61
  var ALGORITHM_IDENTIFIER = pkix.algorithmIdentifier(NS);
62
62
  var ATTRIBUTE = pkix.attribute(NS);
63
+ /** @internal The two arms of a CMSAlgorithmProtection, each an AlgorithmIdentifier under its own
64
+ * IMPLICIT tag. The Appendix A module is `DEFINITIONS IMPLICIT TAGS`, so the tag replaces the
65
+ * SEQUENCE tag rather than wrapping it, which is what the shared factory asserts. */
66
+ var ALGORITHM_IDENTIFIER_1 = pkix.algorithmIdentifier(NS, { implicitTag: 1 });
67
+ var ALGORITHM_IDENTIFIER_2 = pkix.algorithmIdentifier(NS, { implicitTag: 2 });
63
68
 
64
69
  var SIGNED_DATA_VERSION = pkix.versionReader(NS, { "1": 1, "3": 3, "4": 4, "5": 5 });
65
70
  var SIGNER_VERSION = pkix.versionReader(NS, { "1": 1, "3": 3 });
@@ -106,30 +111,116 @@ var OID_MESSAGE_DIGEST = oid.byName("messageDigest");
106
111
  var OID_SIGNING_TIME = oid.byName("signingTime");
107
112
  var OID_COUNTERSIGNATURE = oid.byName("countersignature");
108
113
 
114
+ /** @internal The two identifiers RFC 6211's own documents give the algorithm protection attribute.
115
+ * Sec. 2 and Appendix A state the PKCS 9 leaf; sec. 4's "All identifiers are assigned out of the
116
+ * S/MIME OID arc", the IANA registry, errata 9144 and 9145, and
117
+ * draft-ietf-lamps-rfc6211-update-03 state the S/MIME attribute arc. Both are read, because an
118
+ * implementation that recognizes one spelling and not the other checks nothing on a message using
119
+ * the other, which is the interop failure the update draft sec. 1 records between two shipped
120
+ * implementations of this attribute. */
121
+ var OID_ALG_PROTECTION = oid.byName("cmsAlgorithmProtection");
122
+ var OID_ALG_PROTECTION_AA = oid.byName("cmsAlgorithmProtect");
123
+ function _isAlgProtection(type) {
124
+ return type === OID_ALG_PROTECTION || type === OID_ALG_PROTECTION_AA;
125
+ }
126
+ /** @internal Whether a dotted OID is one of the two the algorithm protection attribute is assigned,
127
+ * exported so a verb policing its own emission asks the module that owns the attribute rather than
128
+ * keeping a second copy of the pair.
129
+ *
130
+ * @internal
131
+ */
132
+ function isAlgorithmProtectionType(type) { return _isAlgProtection(type); }
133
+
134
+ /** @internal The identifier an emitting verb writes, resolved from the option that chose it, or
135
+ * `null` when the option asks for no attribute. Only the four values are accepted. Any other
136
+ * truthy value read as the default would emit the PKCS 9 identifier to a caller who asked for the
137
+ * registry one, and a mismatched identifier is equivalent to no protection at all
138
+ * (draft-ietf-lamps-rfc6211-update sec. 5), so a misspelling would silently produce a message the
139
+ * reader it was meant for does not check. Any other falsy value read as absence would emit nothing
140
+ * for a caller who asked for the attribute, which is the same silence from the other direction.
141
+ *
142
+ * @internal
143
+ */
144
+ function algorithmProtectionOid(choice, E) {
145
+ if (choice === true) return OID_ALG_PROTECTION;
146
+ if (choice === "registry") return OID_ALG_PROTECTION_AA;
147
+ if (choice === undefined || choice === null || choice === false) return null;
148
+ throw E("cms/bad-input", "algorithmProtection is true for the identifier RFC 6211 sec. 2 states, " +
149
+ "or \"registry\" for the one the IANA registry gives it; got " + guard.text.showValue(choice));
150
+ }
151
+
109
152
  var ATTR_FORBIDDEN_IN = Object.create(null);
110
153
  ATTR_FORBIDDEN_IN[OID_CONTENT_TYPE] = { unsigned: true, unauth: true, unprotected: true };
111
154
  ATTR_FORBIDDEN_IN[OID_MESSAGE_DIGEST] = { unsigned: true, unauth: true, unprotected: true };
112
155
  ATTR_FORBIDDEN_IN[OID_SIGNING_TIME] = { unsigned: true, unauth: true, unprotected: true };
113
156
  ATTR_FORBIDDEN_IN[OID_COUNTERSIGNATURE] = { signed: true, auth: true, unauth: true, unprotected: true };
157
+ /** @internal RFC 6211 sec. 2: the attribute "MUST be a signed attribute or an authenticated
158
+ * attribute; it MUST NOT be an unsigned attribute, an unauthenticated attribute, or an unprotected
159
+ * attribute", and sec. 5 adds that an instance in such a place "does not provide any additional
160
+ * security". Refused at parse under both spellings. */
161
+ ATTR_FORBIDDEN_IN[OID_ALG_PROTECTION] = { unsigned: true, unauth: true, unprotected: true, authEnveloped: true };
162
+ ATTR_FORBIDDEN_IN[OID_ALG_PROTECTION_AA] = { unsigned: true, unauth: true, unprotected: true, authEnveloped: true };
163
+ /** @internal An `AuthEnvelopedData`'s authenticated attributes are their own place, distinct from an
164
+ * `AuthenticatedData`'s, because one attribute belongs in the second and in no other: RFC 6211
165
+ * sec. 2 populates the `macAlgorithm` arm "only if the attribute is placed in an
166
+ * AuthenticatedData.authAttrs sequence", and RFC 8933 sec. 6 says the attribute protects nothing in
167
+ * an `AuthEnvelopedData`, which carries no digest or MAC algorithm field for a copy to name. Every
168
+ * other attribute's row names neither place, so only this one is told apart. */
114
169
  var ATTR_PLACE_LABELS = Object.assign(Object.create(null), {
115
170
  signed: "a signed attribute", unsigned: "an unsigned attribute", auth: "an authenticated attribute",
116
171
  unauth: "an unauthenticated attribute", unprotected: "an unprotected attribute",
172
+ authEnveloped: "an authenticated attribute of an AuthEnvelopedData",
117
173
  });
118
174
 
119
- function _checkAttrPlacement(attrs, place) {
175
+ /** @internal `places` is every name this set answers to, because a set can be more than one thing:
176
+ * an `AuthEnvelopedData`'s is an authenticated-attribute set AND that content type's own, so it
177
+ * carries every restriction the first names plus the one the second adds. Naming only the narrower
178
+ * place would drop the rest, which is how a set that refused a countersignature stopped refusing
179
+ * it. A name a row does not mention restricts nothing, so adding one can only add refusals. */
180
+ function _checkAttrPlacement(attrs, places) {
181
+ var names = typeof places === "string" ? [places] : places;
120
182
  for (var i = 0; i < attrs.length; i++) {
121
183
  var row = ATTR_FORBIDDEN_IN[attrs[i].type];
122
- if (row && row[place]) {
184
+ if (!row) continue;
185
+ for (var p = 0; p < names.length; p++) {
186
+ if (!row[names[p]]) continue;
123
187
  throw NS.E("cms/misplaced-attr", "the " + (oid.name(attrs[i].type) || attrs[i].type) +
124
- " attribute must not be " + ATTR_PLACE_LABELS[place] + " (RFC 5652 sec. 11)");
188
+ " attribute must not be " + ATTR_PLACE_LABELS[names[p]] + " (RFC 5652 sec. 11)");
125
189
  }
126
190
  }
127
191
  }
128
192
 
129
193
  function _checkContentBindingAttrs(attrs, mode, ctx) {
130
- var ct = 0, md = 0, seen = Object.create(null);
194
+ var ct = 0, md = 0, ap = 0, seen = Object.create(null);
131
195
  for (var i = 0; i < attrs.length; i++) {
132
196
  var a = attrs[i];
197
+ /** @internal Counted across BOTH identifiers rather than by type, because the two spell one
198
+ * attribute: "The SignedAttributes in a signerInfo MUST include only one instance of the
199
+ * algorithm protection attribute" (RFC 6211 sec. 2), and a set carrying one under each
200
+ * spelling carries two instances of it. The per-type `seen` map below cannot see that. */
201
+ if (_isAlgProtection(a.type)) {
202
+ ap += 1;
203
+ if (ap > 1) {
204
+ throw NS.E("cms/duplicate-signed-attr", "the attribute set must include only one instance " +
205
+ "of the algorithm protection attribute (RFC 6211 sec. 2), counting both identifiers it " +
206
+ "is assigned");
207
+ }
208
+ if (a.values.length !== 1) {
209
+ throw NS.E("cms/bad-algorithm-protection-attr", "the algorithm protection attribute MUST " +
210
+ "have a single attribute value, and there MUST NOT be zero or multiple instances of " +
211
+ "AttributeValue present (RFC 6211 sec. 2), got " + a.values.length);
212
+ }
213
+ /** @internal Decoded during the walk, so a malformed value is a parse refusal rather than
214
+ * something a verify path meets later: the attribute is a statement about the structure it
215
+ * rides in, and a structure carrying one that does not decode is malformed. */
216
+ /** @internal WHICH arm the value carries is not decided here. RFC 6211 sec. 2 says which arm
217
+ * a producer fills, and sec. 3.1 and 3.2 give the consequence to the validator: an attribute
218
+ * holding the other arm makes a claim about a structure this is not, and the comparison in
219
+ * `algorithmProtectionReport` fails the signer or the message for it. Refusing the whole
220
+ * message here instead would leave an operator unable to parse one in order to see that. */
221
+ schema.walk(ALG_PROTECTION,
222
+ pkix.decodeNested(a.values[0], ctx, "an algorithm protection attribute value"), ctx);
223
+ }
133
224
  if (seen[a.type]) {
134
225
  if (a.type === OID_CONTENT_TYPE) throw NS.E("cms/duplicate-content-type", "the attribute set must not repeat the content-type attribute");
135
226
  if (a.type === OID_MESSAGE_DIGEST) throw NS.E("cms/duplicate-message-digest", "the attribute set must not repeat the message-digest attribute");
@@ -336,6 +427,153 @@ var SIGNER_IDENTIFIER = schema.choice([
336
427
  { when: { tagClass: "context", tagNumber: 0 }, schema: schema.implicitOctetString(0) },
337
428
  ], { code: "cms/bad-signer-identifier", what: "SignerIdentifier" });
338
429
 
430
+ /** @internal `CMSAlgorithmProtection ::= SEQUENCE { digestAlgorithm DigestAlgorithmIdentifier,
431
+ * signatureAlgorithm [1] SignatureAlgorithmIdentifier OPTIONAL, macAlgorithm [2]
432
+ * MessageAuthenticationCodeAlgorithm OPTIONAL }` with the RFC 6211 sec. 2 constraint "Exactly one
433
+ * of signatureAlgorithm or macAlgorithm SHALL be present", which the ASN.1 states as a
434
+ * `WITH COMPONENTS` pair and which no decoder generator enforces.
435
+ *
436
+ * The exclusive-or is decided in the build, where both fields are in hand, and each failure gets
437
+ * its own message: a value carrying neither arm protects one of the two identifiers it exists to
438
+ * protect, and one carrying both claims to be in two places at once. A trailing third element is
439
+ * refused by the engine, which consumes no element a field did not claim, so the SEQUENCE is the
440
+ * two elements Bouncy Castle's own size check requires. */
441
+ var ALG_PROTECTION = schema.seq([
442
+ schema.field("digestAlgorithm", ALGORITHM_IDENTIFIER),
443
+ schema.optional("signatureAlgorithm", ALGORITHM_IDENTIFIER_1, { tag: 1 }),
444
+ schema.optional("macAlgorithm", ALGORITHM_IDENTIFIER_2, { tag: 2 }),
445
+ ], {
446
+ assert: "sequence", code: "cms/bad-algorithm-protection-attr", what: "CMSAlgorithmProtection",
447
+ build: function (m, ctx) {
448
+ /** @internal The two arms hold ALGORITHM IDENTIFIERS, not a signature and not a MAC, and the
449
+ * tests below are presence tests on a decoded record. The names say which, since `sig` and
450
+ * `mac` read as the values those words usually name and a comparison against one of those
451
+ * would owe a constant-time one. */
452
+ var sigAlgId = m.fields.signatureAlgorithm.present ? m.fields.signatureAlgorithm.value.result : null;
453
+ var macAlgId = m.fields.macAlgorithm.present ? m.fields.macAlgorithm.value.result : null;
454
+ if (sigAlgId !== null && macAlgId !== null) {
455
+ throw ctx.E("cms/bad-algorithm-protection-attr", "a CMSAlgorithmProtection carries exactly " +
456
+ "one of signatureAlgorithm [1] or macAlgorithm [2] (RFC 6211 sec. 2), and this one carries both");
457
+ }
458
+ if (sigAlgId === null && macAlgId === null) {
459
+ throw ctx.E("cms/bad-algorithm-protection-attr", "a CMSAlgorithmProtection carries exactly " +
460
+ "one of signatureAlgorithm [1] or macAlgorithm [2] (RFC 6211 sec. 2), and this one carries neither");
461
+ }
462
+ return {
463
+ digestAlgorithm: m.fields.digestAlgorithm.value.result,
464
+ signatureAlgorithm: sigAlgId,
465
+ macAlgorithm: macAlgId,
466
+ };
467
+ },
468
+ });
469
+
470
+ /** @internal What the RFC 6211 attribute in `attrs` says about the structure carrying it, as a
471
+ * report rather than a verdict: `{ present, oid, compared, mismatch }`. Read ONCE per structure by
472
+ * each consumer, so the row an operator reads and the decision a verdict rests on cannot disagree.
473
+ *
474
+ * Sec. 3.1 and sec. 3.2 make both comparisons mandatory for a validator that supports the
475
+ * attribute, and support is claimed the moment this runs: "If the fields are not the same (modulo
476
+ * encoding), then signature validation MUST fail."
477
+ *
478
+ * Both arms compare modulo encoding, in the two ways sec. 3 names. A field with a default compares
479
+ * the same written out as omitted, which is the clause's own MUST and which it says a binary
480
+ * compare cannot satisfy; RSASSA-PSS is where that arises, since RFC 4055 clause 3.1 defaults all
481
+ * four of its parameter fields. Absent and DER NULL parameters read as one algorithm on the digest
482
+ * arm, because RFC 3370 clause 2.1 and RFC 5754 clause 2 require both encodings to be ACCEPTED, so
483
+ * both legitimately appear on either side; sec. 3 leaves that case to the implementer. Anything
484
+ * else in a parameter field is read as written: that is where RSASSA-PSS puts its hash, mask
485
+ * generator, salt length and trailer field, which the signature itself does not cover, so
486
+ * collapsing two values there would admit the substitution the attribute exists to refuse.
487
+ *
488
+ * An arm belonging to the other placement is a mismatch rather than an absent one: sec. 2 populates
489
+ * `signatureAlgorithm` "only if the attribute is placed in a SignerInfo.signedAttrs sequence" and
490
+ * `macAlgorithm` only in an `AuthenticatedData.authAttrs`, so an attribute carrying the wrong arm is
491
+ * making a claim about a structure this is not.
492
+ *
493
+ * @internal
494
+ */
495
+ function algorithmProtectionReport(attrs, digestAlgorithm, armName, armIdentifier) {
496
+ var out = { present: false, oid: null, compared: [], mismatch: null };
497
+ var found = null;
498
+ for (var i = 0; i < attrs.length; i++) {
499
+ if (found === null && _isAlgProtection(attrs[i].type)) found = attrs[i];
500
+ }
501
+ if (found === null) return out;
502
+ out.present = true;
503
+ out.oid = found.type;
504
+ var value;
505
+ try { value = parseAlgorithmProtection(found.values[0]); }
506
+ catch (e) {
507
+ out.mismatch = { code: (e && e.code) || "cms/bad-algorithm-protection-attr", message: e && e.message };
508
+ return out;
509
+ }
510
+ if (!sameDigestAlgorithm(digestAlgorithm, value.digestAlgorithm)) {
511
+ out.mismatch = { code: "cms/algorithm-protection-mismatch",
512
+ message: "the algorithm protection attribute names the digest algorithm " +
513
+ (value.digestAlgorithm.name || value.digestAlgorithm.oid) + " and the structure it protects " +
514
+ "names " + ((digestAlgorithm && (digestAlgorithm.name || digestAlgorithm.oid)) || "none") +
515
+ " (RFC 6211 sec. 3)" };
516
+ return out;
517
+ }
518
+ out.compared.push("digestAlgorithm");
519
+ var wantsMacArm = armName === "macAlgorithm";
520
+ var arm = wantsMacArm ? value.macAlgorithm : value.signatureAlgorithm;
521
+ var other = wantsMacArm ? value.signatureAlgorithm : value.macAlgorithm;
522
+ if (arm === null || other !== null) {
523
+ out.mismatch = { code: "cms/algorithm-protection-mismatch",
524
+ message: "the algorithm protection attribute carries " + (arm === null
525
+ ? "no " + armName + " to compare against the one this structure names"
526
+ : "the arm belonging to the other placement") + ", and RFC 6211 sec. 2 populates each arm " +
527
+ "only in the placement it belongs to" };
528
+ return out;
529
+ }
530
+ if (!sameAlgorithmModuloDefaults(armIdentifier, arm)) {
531
+ out.mismatch = { code: "cms/algorithm-protection-mismatch",
532
+ message: "the algorithm protection attribute names the " + armName + " " +
533
+ (arm.name || arm.oid) + " and the structure it protects names " +
534
+ ((armIdentifier && (armIdentifier.name || armIdentifier.oid)) || "none") +
535
+ ", or their parameters differ (RFC 6211 sec. 3)" };
536
+ return out;
537
+ }
538
+ out.compared.push(armName);
539
+ return out;
540
+ }
541
+
542
+ /** @internal The producer door for the RFC 6211 attribute, shared by every verb that emits one. A
543
+ * caller-supplied copy cannot contradict the identifiers being emitted, and cannot be a second
544
+ * instance beside the one a verb emitted: caller attributes reach the wire without being read, so
545
+ * without this a message could carry a protection attribute naming algorithms its own structure
546
+ * does not use, which every verifier that reads the attribute then refuses. The comparison is
547
+ * against the very value the emitter would build, so it is the emitter's own answer rather than a
548
+ * second derivation of it.
549
+ *
550
+ * `attrs` are `{ type, values }` records. The signing verbs hold their attributes in that shape
551
+ * already; the authenticate verb holds DER TLVs and adapts them, so the POLICY lives here once
552
+ * rather than in a loop per verb.
553
+ *
554
+ * @internal
555
+ */
556
+ function assertAlgorithmProtectionEmission(attrs, wantValue, E) {
557
+ var found = 0;
558
+ for (var i = 0; i < attrs.length; i++) {
559
+ if (!_isAlgProtection(attrs[i].type)) continue;
560
+ found += 1;
561
+ if (found > 1) {
562
+ throw E("cms/bad-input", "only one algorithm protection attribute may be emitted (RFC 6211 " +
563
+ "sec. 2), counting both identifiers it is assigned and any copy supplied by the caller");
564
+ }
565
+ if (attrs[i].values.length !== 1) {
566
+ throw E("cms/bad-input", "an algorithm protection attribute carries exactly one value " +
567
+ "(RFC 6211 sec. 2), got " + attrs[i].values.length);
568
+ }
569
+ if (guard.bytes.compare(attrs[i].values[0], wantValue) !== 0) {
570
+ throw E("cms/bad-input", "the algorithm protection attribute supplied does not copy the " +
571
+ "algorithm identifiers this structure uses (RFC 6211 sec. 2), so every verifier that reads " +
572
+ "it would refuse the result; omit it and pass algorithmProtection: true to have it built");
573
+ }
574
+ }
575
+ }
576
+
339
577
  function makeSignerInfo(mode) {
340
578
  return schema.seq([
341
579
  schema.field("version", SIGNER_VERSION),
@@ -780,7 +1018,7 @@ var AUTHENTICATED_DATA = schema.seq([
780
1018
  _assertDerEncodedAttrs(m.fields.authAttrs.node, "cms/bad-auth-attrs", ctx);
781
1019
  authAttrs = m.fields.authAttrs.value.items.map(function (it) { return it.value.result; });
782
1020
  _checkAttrPlacement(authAttrs, "auth");
783
- _checkContentBindingAttrs(authAttrs, "content", ctx);
1021
+ _checkContentBindingAttrs(authAttrs, "auth", ctx);
784
1022
  _assertContentTypeMatchesAttrs(authAttrs, encapContentInfo.eContentType, ctx);
785
1023
  authAttrsBytes = m.fields.authAttrs.node.bytes;
786
1024
  }
@@ -827,7 +1065,12 @@ var AUTH_ENVELOPED_DATA = schema.seq([
827
1065
  if (hasAuthAttrs) {
828
1066
  _assertDerEncodedAttrs(m.fields.authAttrs.node, "cms/bad-auth-attrs", ctx);
829
1067
  authAttrs = m.fields.authAttrs.value.items.map(function (it) { return it.value.result; });
830
- _checkAttrPlacement(authAttrs, "auth");
1068
+ /** @internal Both names: this is an authenticated-attribute set, and it is also this content
1069
+ * type's own set, which refuses one attribute more. The algorithm protection attribute
1070
+ * belongs in an `AuthenticatedData` and in no other, and the verbs that emit attributes
1071
+ * refuse it here, so the parser refuses it here too rather than reading a message this
1072
+ * package would not write. */
1073
+ _checkAttrPlacement(authAttrs, ["auth", "authEnveloped"]);
831
1074
  _checkNoDuplicateAttrs(authAttrs);
832
1075
  _assertContentTypeMatchesAttrs(authAttrs, encryptedContentInfo.contentType, ctx);
833
1076
  authAttrsBytes = m.fields.authAttrs.node.bytes;
@@ -968,6 +1211,40 @@ var CONTENT_INFO = schema.seq([
968
1211
  */
969
1212
  var parse = pkix.makeRecordingParser({ pemLabel: "CMS", PemError: PemError, ErrorClass: CmsError, prefix: "cms", what: "CMS ContentInfo", topSchema: CONTENT_INFO, ns: NS }, "cms");
970
1213
 
1214
+ /**
1215
+ * @primitive pki.schema.cms.parseAlgorithmProtection
1216
+ * @signature pki.schema.cms.parseAlgorithmProtection(der) -> { digestAlgorithm, signatureAlgorithm, macAlgorithm }
1217
+ * @since 0.8.53
1218
+ * @status stable
1219
+ * @spec RFC 6211
1220
+ * @related pki.cms.verify, pki.cms.sign, pki.schema.cms.parse
1221
+ *
1222
+ * Decode a `CMSAlgorithmProtection` attribute value (RFC 6211 sec. 2): a copy of the digest
1223
+ * algorithm identifier the structure names, plus exactly one of the signature algorithm identifier
1224
+ * (the `[1]` arm, for a `SignerInfo`) or the message authentication code algorithm identifier (the
1225
+ * `[2]` arm, for an `AuthenticatedData`). The arm that is absent reads as `null`. Each identifier is
1226
+ * surfaced whole, `{ oid, name, parameters }`, because the parameters are what the attribute exists
1227
+ * to pin: a signature algorithm's parameters are not covered by the signature.
1228
+ *
1229
+ * A value carrying both arms or neither is refused `cms/bad-algorithm-protection-attr`, which is the
1230
+ * sec. 2 rule "Exactly one of signatureAlgorithm or macAlgorithm SHALL be present". `pki.cms.parse`
1231
+ * decodes the attribute during its own walk, so a message carrying a malformed one is refused there;
1232
+ * this verb is for reading a value held on its own.
1233
+ *
1234
+ * @example
1235
+ * var pair = await pki.key.generate("Ed25519");
1236
+ * var key = await pki.key.export(pair.privateKey);
1237
+ * var cert = await pki.x509.sign({ subject: "Signer", subjectPublicKey: await pki.key.export(pair.publicKey),
1238
+ * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: key });
1239
+ * var signedDer = await pki.cms.sign(Buffer.from("hello"), { cert: cert, key: key }, { algorithmProtection: true });
1240
+ * var si = pki.schema.cms.parse(signedDer).signerInfos[0];
1241
+ * var attr = si.signedAttrs.filter(function (a) { return a.name === "cmsAlgorithmProtection"; })[0];
1242
+ * var protection = pki.schema.cms.parseAlgorithmProtection(attr.values[0]);
1243
+ * protection.digestAlgorithm.name; // "sha512", the digest Ed25519 signs under here
1244
+ * protection.macAlgorithm; // null, this being a SignerInfo
1245
+ */
1246
+ var parseAlgorithmProtection = pkix.makeParser({ pemLabel: null, PemError: PemError, ErrorClass: CmsError, prefix: "cms", what: "CMSAlgorithmProtection", topSchema: ALG_PROTECTION, ns: NS });
1247
+
971
1248
  /**
972
1249
  * @primitive pki.schema.cms.pemDecode
973
1250
  * @signature pki.schema.cms.pemDecode(text, label?) -> Buffer
@@ -1101,9 +1378,103 @@ function sameDigestAlgorithm(a, c) {
1101
1378
  a.parameters.equals(c.parameters);
1102
1379
  }
1103
1380
 
1381
+ /** @internal The parameter fields that carry an ASN.1 DEFAULT, keyed by the algorithm that owns
1382
+ * them, because RFC 6211 sec. 3 says a binary compare is not enough: "A field with a default value
1383
+ * MUST compare as identical, independently of whether the value is defaulted or is explicitly
1384
+ * provided. This implies that a binary compare of the encoded bytes is insufficient." RSASSA-PSS
1385
+ * is the only identifier this attribute carries whose parameters have any, and RFC 4055 sec. 3.1
1386
+ * gives each of its four a default: `hashAlgorithm` sha1Identifier, `maskGenAlgorithm`
1387
+ * mgf1SHA1Identifier, `saltLength` 20 and `trailerField` 1. A row rather than a branch, so an
1388
+ * algorithm that arrives later with defaulted parameters is data. */
1389
+ var SHA1_IDENTIFIER = asn1.build.sequence([asn1.build.oid(oid.byName("sha1")), asn1.build.nullValue()]);
1390
+ var PARAM_MAX_TAG = 3;
1391
+ var PARAM_DEFAULTS = Object.create(null);
1392
+ PARAM_DEFAULTS[oid.byName("rsassaPss")] = [
1393
+ { tag: 0, value: SHA1_IDENTIFIER },
1394
+ { tag: 1, value: asn1.build.sequence([asn1.build.oid(oid.byName("mgf1")), SHA1_IDENTIFIER]) },
1395
+ { tag: 2, value: asn1.build.integer(20) },
1396
+ { tag: 3, value: asn1.build.integer(1) },
1397
+ ];
1398
+
1399
+ /** @internal The tagged fields of a parameters SEQUENCE, as `[{ tag, node, bytes }]` in the order
1400
+ * they appear, or `null` when the bytes are not that shape. The DER rule for a SEQUENCE whose
1401
+ * fields are OPTIONAL or DEFAULT is one context-tagged element each, in ascending tag order with
1402
+ * no repeats, holding exactly one value; a producer that repeats a tag or writes them out of order
1403
+ * has not encoded this structure. One home for the walk, so the verifier that resolves RSASSA-PSS
1404
+ * parameters and the comparison that reads their defaults agree on what a field is.
1405
+ *
1406
+ * `maxTag` bounds the tags a caller will act on, so a field it has no meaning for is refused here
1407
+ * rather than ignored below.
1408
+ *
1409
+ * @internal
1410
+ */
1411
+ function taggedParameterFields(params, maxTag) {
1412
+ if (!Buffer.isBuffer(params) || params.length === 0) return null;
1413
+ var node;
1414
+ // allow:bare-decode parameters of an identifier a parse door already accepted, read for a comparison outside any walk, so no parse context carries caps here.
1415
+ try { node = asn1.decode(params); } catch (_e) { return null; } // allow:swallow-unverified measured unreachable from the verbs: an attribute whose parameters do not decode is refused as cms/bad-der by the parse door, so no message carrying one reaches a comparison; bytes that do not decode are not the defaulted shape and fall back to the exact comparison
1416
+ if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE ||
1417
+ !Array.isArray(node.children)) return null;
1418
+ var out = [], last = -1;
1419
+ for (var i = 0; i < node.children.length; i++) {
1420
+ var f = node.children[i];
1421
+ if (f.tagClass !== "context" || f.tagNumber <= last || f.tagNumber > maxTag ||
1422
+ !Array.isArray(f.children) || f.children.length !== 1) return null;
1423
+ last = f.tagNumber;
1424
+ if (!Buffer.isBuffer(f.children[0].bytes)) return null;
1425
+ out.push({ tag: f.tagNumber, node: f.children[0], bytes: f.children[0].bytes });
1426
+ }
1427
+ return out;
1428
+ }
1429
+
1430
+ /** @internal The parameters as a comparison key with every defaulted field dropped, or `null` when
1431
+ * they are not the shape the defaults belong to. A field whose encoding equals its default reads
1432
+ * the same as one the producer omitted, which is what the clause above requires; a field holding
1433
+ * anything else is kept and compared, since that is where RSASSA-PSS states the hash, the mask
1434
+ * generator, the salt length and the trailer field. */
1435
+ function _paramKeyWithoutDefaults(params, rows) {
1436
+ var fields = taggedParameterFields(params, PARAM_MAX_TAG);
1437
+ if (fields === null) return null;
1438
+ var parts = [];
1439
+ for (var i = 0; i < fields.length; i++) {
1440
+ var defaulted = false;
1441
+ for (var r = 0; r < rows.length; r++) {
1442
+ if (rows[r].tag === fields[i].tag) { defaulted = rows[r].value.equals(fields[i].bytes); break; }
1443
+ }
1444
+ if (defaulted) continue;
1445
+ parts.push(String(fields[i].tag) + ":" + fields[i].bytes.toString("hex"));
1446
+ }
1447
+ return parts.join(",");
1448
+ }
1449
+
1450
+ /** @internal Whether two AlgorithmIdentifiers name one algorithm under RFC 6211 sec. 3, which is
1451
+ * byte equality for every identifier whose parameters hold no defaulted field and field equality
1452
+ * for one that does. Parameters that do not decode as the defaulted shape fall back to the exact
1453
+ * comparison, so an unreadable value is equal only to the identical bytes. */
1454
+ function sameAlgorithmModuloDefaults(a, c) {
1455
+ if (!a || !c || a.oid !== c.oid) return false;
1456
+ var rows = PARAM_DEFAULTS[a.oid];
1457
+ if (rows === undefined) return guard.der.sameAlgorithmExactly(a, c);
1458
+ /** @internal Two copies of one identifier normally carry the same bytes, and that answer needs no
1459
+ * decode: the fields are read only when the copies disagree, which is the case the clause is
1460
+ * about and the only one worth the walk. */
1461
+ if (guard.der.sameAlgorithmExactly(a, c)) return true;
1462
+ var ak = _paramKeyWithoutDefaults(a.parameters, rows);
1463
+ var ck = _paramKeyWithoutDefaults(c.parameters, rows);
1464
+ if (ak === null || ck === null) return guard.der.sameAlgorithmExactly(a, c);
1465
+ return ak === ck;
1466
+ }
1467
+
1104
1468
  module.exports = {
1105
1469
  parse: parse,
1470
+ parseAlgorithmProtection: parseAlgorithmProtection,
1471
+ algorithmProtectionReport: algorithmProtectionReport, // @internal
1472
+ assertAlgorithmProtectionEmission: assertAlgorithmProtectionEmission, // @internal
1473
+ isAlgorithmProtectionType: isAlgorithmProtectionType, // @internal
1474
+ algorithmProtectionOid: algorithmProtectionOid, // @internal
1106
1475
  sameDigestAlgorithm: sameDigestAlgorithm, // @internal
1476
+ sameAlgorithmModuloDefaults: sameAlgorithmModuloDefaults, // @internal
1477
+ taggedParameterFields: taggedParameterFields, // @internal
1107
1478
  pemDecode: pemDecode,
1108
1479
  pemEncode: pemEncode,
1109
1480
  matches: matches,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.8.52",
3
+ "version": "0.8.53",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:4d4aadb5-0f2b-4564-b30c-638343b637e8",
5
+ "serialNumber": "urn:uuid:f287baca-f745-4ecd-8265-d6953251f0b6",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-10-05T18:37:07.808Z",
8
+ "timestamp": "2026-10-06T00:56:34.786Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/pki@0.8.52",
22
+ "bom-ref": "@blamejs/pki@0.8.53",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.8.52",
25
+ "version": "0.8.53",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
29
- "purl": "pkg:npm/%40blamejs/pki@0.8.52",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.8.53",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/pki@0.8.52",
57
+ "ref": "@blamejs/pki@0.8.53",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]