@blamejs/pki 0.8.51 → 0.8.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,6 +22,9 @@
22
22
  * (raw), the hash algorithm (decoded for v2, or the implied SHA-1 for v1), and
23
23
  * the optional `issuerSerial` (issuer `GeneralNames` validated + surfaced raw,
24
24
  * serial as a BigInt + hex)) plus the optional certificate policies.
25
+ * `buildSigningCertificateV2` encodes the v2 attribute value a signer attaches:
26
+ * one `ESSCertIDv2` per certificate, the SHA-256 form with the DEFAULT algorithm
27
+ * omitted and SHA-384 / SHA-512 with an absent parameters field.
25
28
  * `parseSmimeCapabilities` decodes the ordered `SMIMECapabilities` list (each a
26
29
  * capability OID + raw parameters). `decodeAttribute` takes a CMS-shaped
27
30
  * `{ type, values }` attribute, enforces the single-`AttributeValue` rule
@@ -46,9 +49,14 @@ var schema = require("./schema-engine");
46
49
  var pkix = require("./schema-pkix");
47
50
  var oid = require("./oid");
48
51
  var frameworkError = require("./framework-error");
52
+ var guard = require("./guard-all");
53
+ var intrinsic = require("./guard-intrinsic");
54
+ var constants = require("./constants");
55
+ var x509 = require("./schema-x509");
49
56
 
50
57
  var SmimeError = frameworkError.SmimeError;
51
58
  var PemError = frameworkError.PemError;
59
+ var b = asn1.build;
52
60
 
53
61
  var NS = pkix.makeNS("smime", SmimeError, oid);
54
62
  var TAGS = asn1.TAGS;
@@ -61,6 +69,12 @@ var OID_SIGNING_CERTIFICATE = oid.byName("signingCertificate");
61
69
  var OID_SIGNING_CERTIFICATE_V2 = oid.byName("signingCertificateV2");
62
70
  var OID_SMIME_CAPABILITIES = oid.byName("smimeCapabilities");
63
71
 
72
+ /** @internal The digests this module will BIND a certificate with. RFC 5035 sec. 4 leaves
73
+ * `hashAlgorithm` open, and the two readers that consume the field disagree on the set they admit,
74
+ * so the builder emits only what the strictest consumer accepts: SHA-1 is excluded because a
75
+ * binding is an identity claim, and an identifier a verifier refuses is a token nobody can use. */
76
+ var BIND_DIGESTS = intrinsic.assign(intrinsic.create(null), { sha256: 1, sha384: 1, sha512: 1 });
77
+
64
78
  var GENERAL_NAME_DIRECTORY = 4;
65
79
  var ISSUER_SERIAL = schema.seq([
66
80
  schema.field("issuer", pkix.generalNames(NS, { code: "smime/bad-general-names" })),
@@ -71,7 +85,7 @@ var ISSUER_SERIAL = schema.seq([
71
85
  return {
72
86
  issuer: m.fields.issuer.value.result,
73
87
  serialNumber: m.fields.serialNumber.value,
74
- serialNumberHex: m.fields.serialNumber.node.content.toString("hex"),
88
+ serialNumberHex: intrinsic.bufToString(m.fields.serialNumber.node.content, "hex"),
75
89
  };
76
90
  },
77
91
  });
@@ -105,7 +119,7 @@ var POLICY_INFORMATION = schema.seq([
105
119
  });
106
120
  var POLICIES = schema.seqOf(POLICY_INFORMATION, {
107
121
  assert: "sequence", min: 0, code: "smime/bad-policies", what: "policies",
108
- build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
122
+ build: function (m) { return intrinsic.map(m.items, function (it) { return it.value.result; }); },
109
123
  });
110
124
 
111
125
  var ESS_CERT_ID = schema.seq([
@@ -155,7 +169,7 @@ function signingCertificateSchema(essCertId, code, what) {
155
169
  ], {
156
170
  assert: "sequence", code: code, what: what,
157
171
  build: function (m, ctx) {
158
- var certs = m.fields.certs.value.items.map(function (it) { return it.value.result; });
172
+ var certs = intrinsic.map(m.fields.certs.value.items, function (it) { return it.value.result; });
159
173
  assertSignerIssuerIsDirectoryName(certs, function (msg) { throw ctx.E("smime/bad-issuer-serial", msg); });
160
174
  return {
161
175
  certs: certs,
@@ -182,7 +196,7 @@ var SMIME_CAPABILITY = schema.seq([
182
196
  });
183
197
  var SMIME_CAPABILITIES = schema.seqOf(SMIME_CAPABILITY, {
184
198
  assert: "sequence", min: 0, code: "smime/bad-capabilities", what: "SMIMECapabilities",
185
- build: function (m) { return { capabilities: m.items.map(function (it) { return it.value.result; }) }; },
199
+ build: function (m) { return { capabilities: intrinsic.map(m.items, function (it) { return it.value.result; }) }; },
186
200
  });
187
201
 
188
202
  /**
@@ -266,6 +280,197 @@ var parseSigningCertificateV2 = pkix.makeParser({ pemLabel: null, PemError: PemE
266
280
  */
267
281
  var parseSmimeCapabilities = pkix.makeParser({ pemLabel: null, PemError: PemError, ErrorClass: SmimeError, prefix: "smime", what: "SMIMECapabilities", topSchema: SMIME_CAPABILITIES, ns: NS });
268
282
 
283
+ function _err(code, message) { return new SmimeError(code, message); }
284
+
285
+ /** @internal The digests a signing-certificate identifier may BIND a certificate with, on the
286
+ * reading side. It is the set the builder emits, and it is narrower than the set the ESS decoder
287
+ * will decode: an identifier is an identity claim, so admitting a digest nobody should bind with
288
+ * would let the claim be satisfied by a certificate other than the one that signed. SHA-1 is not
289
+ * here and is reported as weak by its own flag, because the v1 attribute has no algorithm field
290
+ * and is SHA-1 by definition (RFC 2634), so a reader must be able to tell "weak" from "unknown". */
291
+ var BIND_VERIFY_DIGESTS = intrinsic.assign(intrinsic.create(null), { sha256: 1, sha384: 1, sha512: 1 });
292
+ var GENERAL_NAME = pkix.generalName(NS, { decodeValue: true, code: "smime/bad-general-names" });
293
+
294
+ /** @internal What ONE signing-certificate attribute claims about `certDer`, as facts rather than a
295
+ * verdict. Each claim is read and compared here so there is one implementation of "does this
296
+ * identifier name that certificate", and the two consumers apply their own policy to the result:
297
+ * `pki.tsp.verify` refuses a token, and `pki.cms.verify` reports a CAdES conformance finding.
298
+ *
299
+ * An ESSCertID states the signer in two independent ways, and both are compared: the hash under
300
+ * the identifier's own digest, and the issuer and serial where it carries them (RFC 5035 clause 5,
301
+ * RFC 2634 clause 5.4). A hash that agrees beside an issuer and serial naming another certificate
302
+ * is a contradiction, not a match. Identifiers after the first are not read: RFC 2634 clause 5.4
303
+ * makes the first the signing certificate and leaves the rest as the chain. */
304
+ function _bindingClaim(attr) {
305
+ var out = { form: attr.type === OID_SIGNING_CERTIFICATE_V2 ? "v2" : "v1", readable: false,
306
+ digestName: null, supported: false, weak: false, certHash: null, issuerSerial: null };
307
+ var decoded;
308
+ try { decoded = decodeAttribute(attr); }
309
+ catch (_e) { return out; }
310
+ var ess = decoded.certs && decoded.certs[0];
311
+ if (!ess) return out;
312
+ /** @internal The digest identifier is held to its own encoding before its NAME is read. A SHA-2
313
+ * identifier carries absent or NULL parameters and nothing else (RFC 5754 clause 2, "MUST accept
314
+ * SHA2 AlgorithmIdentifiers with absent parameters ... with NULL parameters"), and the same two
315
+ * forms are what RFC 3370 clause 2.1 states for SHA-1. The ANY parameters field is carried
316
+ * through the parse unvalidated, so an identifier naming a digest this build computes while
317
+ * carrying arbitrary bytes beside it would otherwise be hashed as a well-formed binding. */
318
+ if (!guard.der.paramsAbsentOrNull(ess.hashAlgorithm == null ? null : ess.hashAlgorithm.parameters)) return out;
319
+ out.readable = true;
320
+ out.digestName = ess.hashAlgorithm == null ? "sha1" : ess.hashAlgorithm.name;
321
+ out.weak = intrinsic.hasOwn(constants.NAMES.WEAK_DIGESTS, out.digestName);
322
+ out.supported = out.weak || intrinsic.hasOwn(BIND_VERIFY_DIGESTS, out.digestName);
323
+ out.certHash = ess.certHash;
324
+ out.issuerSerial = ess.issuerSerial || null;
325
+ return out;
326
+ }
327
+
328
+ /** @internal One claim compared against one certificate. The claim is read ONCE, by
329
+ * `certBindingClaims`, and this is what runs per certificate: a signer identifier can match many
330
+ * candidates, and re-reading the attribute for each would multiply the attribute's size by the
331
+ * candidate count before any signature is checked. Only the first identifier is ever compared, so
332
+ * only it is kept.
333
+ *
334
+ * @internal
335
+ */
336
+ function certBindingAgainst(claim, certDer) {
337
+ var out = { form: claim.form, readable: claim.readable, digestName: claim.digestName,
338
+ supported: claim.supported, weak: claim.weak, matches: null, certUnreadable: false,
339
+ issuerSerialPresent: claim.issuerSerial !== null, issuerSerialMatches: null };
340
+ if (!claim.readable || !claim.supported) return out;
341
+ /** @internal Through the captured digest and the constant-time comparison: this IS the binding
342
+ * between a signature and the certificate its attribute names, so neither what the digest covers
343
+ * nor how the result is compared is the caller's to choose. */
344
+ out.matches = guard.crypto.constantTimeEqual(guard.crypto.digest(claim.digestName, certDer), claim.certHash);
345
+ if (claim.issuerSerial === null) return out;
346
+ /** @internal A certificate that does not parse is its own fact, kept apart from an attribute that
347
+ * does not decode: one says the identifier is malformed and the other says the thing it would be
348
+ * compared against is, and a consumer orders the two itself. Folding them into one flag moved a
349
+ * refusal from "this identifier names another certificate" to "this attribute is unreadable". */
350
+ var cert;
351
+ try { cert = x509.parse(certDer); }
352
+ catch (_e2) { out.certUnreadable = true; return out; }
353
+ if (claim.issuerSerial.serialNumber !== cert.serialNumber) { out.issuerSerialMatches = false; return out; }
354
+ var names = claim.issuerSerial.issuer.names || [];
355
+ var gn = null;
356
+ try {
357
+ if (names.length === 1) {
358
+ gn = schema.embeddedDer(GENERAL_NAME, names[0].bytes, NS,
359
+ { code: "smime/bad-general-names", what: "GeneralName" });
360
+ }
361
+ } catch (_e3) { gn = null; }
362
+ out.issuerSerialMatches = !!gn && gn.tagNumber === GENERAL_NAME_DIRECTORY && !!gn.value &&
363
+ guard.name.dnEqual(gn.value.rdns, cert.issuer.rdns, _err, "smime/bad-issuer-serial",
364
+ "the ESSCertID issuer");
365
+ return out;
366
+ }
367
+
368
+ /** @internal Every signing-certificate attribute in `signedAttrs`, read against `certDer`.
369
+ * `chosen` is the v2 attribute where there is one, which supersedes v1 (RFC 5035); `superseded`
370
+ * holds the rest, which a consumer still holds to the same claim, because one structure making two
371
+ * incompatible statements about one fact is a contradiction rather than something to resolve in
372
+ * favor of whichever attribute a reader prefers. EVERY attribute is collected rather than the first
373
+ * of each type: `signedAttrs` is a DER SET OF, so which of two comes first is a function of their
374
+ * encodings and a hostile producer picks it.
375
+ *
376
+ * @internal
377
+ */
378
+ function certBindingClaims(signedAttrs) {
379
+ var found = [];
380
+ intrinsic.forEach(signedAttrs || [], function (a) {
381
+ if (a && (a.type === OID_SIGNING_CERTIFICATE || a.type === OID_SIGNING_CERTIFICATE_V2)) {
382
+ guard.list.append(found, a);
383
+ }
384
+ });
385
+ if (!found.length) return { present: false, chosen: null, superseded: [] };
386
+ var chosenAt = 0;
387
+ for (var i = 0; i < found.length; i++) {
388
+ if (found[i].type === OID_SIGNING_CERTIFICATE_V2) { chosenAt = i; break; }
389
+ }
390
+ var superseded = [];
391
+ for (var j = 0; j < found.length; j++) {
392
+ if (j !== chosenAt) guard.list.append(superseded, _bindingClaim(found[j]));
393
+ }
394
+ return { present: true, chosen: _bindingClaim(found[chosenAt]), superseded: superseded };
395
+ }
396
+
397
+ /** @internal The claims read and compared in one call, for a consumer holding a single certificate.
398
+ * A consumer with several reads the claims once and calls `certBindingAgainst` per certificate.
399
+ *
400
+ * @internal
401
+ */
402
+ function certBindingFacts(signedAttrs, certDer) {
403
+ var claims = certBindingClaims(signedAttrs);
404
+ if (!claims.present) return { present: false, chosen: null, superseded: [] };
405
+ return {
406
+ present: true,
407
+ chosen: certBindingAgainst(claims.chosen, certDer),
408
+ superseded: intrinsic.map(claims.superseded, function (c) { return certBindingAgainst(c, certDer); }),
409
+ };
410
+ }
411
+
412
+ var _BUILD_V2_OPTS = intrinsic.assign(intrinsic.create(null), { hashAlgorithm: 1 });
413
+
414
+ /**
415
+ * @primitive pki.schema.smime.buildSigningCertificateV2
416
+ * @signature pki.schema.smime.buildSigningCertificateV2(certs, opts?) -> Buffer
417
+ * @since 0.8.52
418
+ * @status stable
419
+ * @spec RFC 5035, RFC 5754
420
+ * @related pki.schema.smime.parseSigningCertificateV2, pki.cms.sign, pki.tsp.sign
421
+ *
422
+ * Encode an ESS `SigningCertificateV2` attribute value (RFC 5035 sec. 5.4.1), the signed attribute
423
+ * that binds a signature to the certificate that made it. `certs` is the signer's certificate DER,
424
+ * or a list of certificate DERs whose first entry is the signer's (RFC 5035 sec. 5.4); each entry
425
+ * becomes an `ESSCertIDv2` carrying that certificate's hash. The result is the single
426
+ * `AttributeValue` of a `signingCertificateV2` signed attribute, which is what CAdES requires a
427
+ * signature to carry (ETSI EN 319 122-1 clauses 5.2.2.2 and 5.2.2.3).
428
+ *
429
+ * `opts.hashAlgorithm` names the binding digest: `sha256` (the default), `sha384` or `sha512`. The
430
+ * SHA-256 form omits the `hashAlgorithm` field, which is the RFC 5035 sec. 4 DEFAULT and so must be
431
+ * absent (X.690 sec. 11.5); the other two emit an `AlgorithmIdentifier` with the parameters field
432
+ * absent, which RFC 5754 sec. 2 requires an implementation to generate. SHA-1 is refused
433
+ * `smime/unsupported-algorithm`: a binding is an identity claim, and `pki.tsp.verify` refuses a
434
+ * SHA-1 one as a weak binding.
435
+ *
436
+ * The OPTIONAL `issuerSerial` and `policies` fields are not emitted; CAdES has the issuer serial
437
+ * omitted (ETSI EN 319 122-1 clause 6.3 requirement g). A caller that needs either field builds the
438
+ * value with `pki.asn1.build`, and `parseSigningCertificateV2` reads both back.
439
+ *
440
+ * @example
441
+ * var pair = await pki.key.generate("Ed25519");
442
+ * var key = await pki.key.export(pair.privateKey);
443
+ * var signerCertDer = await pki.x509.sign({ subject: "Signer", subjectPublicKey: await pki.key.export(pair.publicKey),
444
+ * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: key });
445
+ * var av = pki.schema.smime.buildSigningCertificateV2(signerCertDer);
446
+ * var msg = await pki.cms.sign(Buffer.from("hello"), { cert: signerCertDer, key: key },
447
+ * { additionalSignedAttributes: [{ type: "signingCertificateV2", values: [av] }] });
448
+ * (await pki.cms.verify(msg, { certs: [signerCertDer] })).signers[0].cadesBaseline.conformant; // true
449
+ */
450
+ function buildSigningCertificateV2(certs, opts) {
451
+ guard.identifier.assertKnownKeys(opts, _BUILD_V2_OPTS, _err, "smime/bad-input",
452
+ "pki.schema.smime.buildSigningCertificateV2 has an unknown option ");
453
+ var settings = guard.identifier.pickSettings(opts, ["hashAlgorithm"]);
454
+ var hashName = settings.hashAlgorithm === undefined || settings.hashAlgorithm === null ? "sha256" : settings.hashAlgorithm;
455
+ if (typeof hashName !== "string" || !BIND_DIGESTS[guard.text.keyOf(hashName)]) {
456
+ throw _err("smime/unsupported-algorithm", "the ESSCertIDv2 binding digest must be sha256, sha384 or sha512, got " +
457
+ guard.text.showValue(hashName));
458
+ }
459
+ var list = guard.list.snapshot(intrinsic.isArray(certs) ? certs : [certs]);
460
+ if (!list.length) {
461
+ throw _err("smime/bad-input", "a SigningCertificateV2 needs at least one certificate, the signer's first (RFC 5035 sec. 5.4)");
462
+ }
463
+ var ids = [];
464
+ for (var i = 0; i < list.length; i++) {
465
+ var der = guard.bytes.view(list[i], SmimeError, "smime/bad-input", "the certificate at certs[" + i + "]");
466
+ var certHash = b.octetString(guard.crypto.digest(hashName, der));
467
+ guard.list.append(ids, hashName === "sha256"
468
+ ? b.sequence([certHash])
469
+ : b.sequence([b.sequence([b.oid(oid.byName(hashName))]), certHash]));
470
+ }
471
+ return b.sequence([b.sequence(ids)]);
472
+ }
473
+
269
474
  /**
270
475
  * @primitive pki.schema.smime.decodeAttribute
271
476
  * @signature pki.schema.smime.decodeAttribute(attr) -> { kind, ... }
@@ -292,7 +497,7 @@ var parseSmimeCapabilities = pkix.makeParser({ pemLabel: null, PemError: PemErro
292
497
  * got.kind; // "signingCertificateV2"
293
498
  */
294
499
  function decodeAttribute(attr) {
295
- if (!attr || typeof attr !== "object" || typeof attr.type !== "string" || !Array.isArray(attr.values)) {
500
+ if (!attr || typeof attr !== "object" || typeof attr.type !== "string" || !intrinsic.isArray(attr.values)) {
296
501
  throw new SmimeError("smime/bad-input", "decodeAttribute expects a CMS attribute { type, values }");
297
502
  }
298
503
  if (attr.type === OID_SIGNING_CERTIFICATE || attr.type === OID_SIGNING_CERTIFICATE_V2 || attr.type === OID_SMIME_CAPABILITIES) {
@@ -313,6 +518,11 @@ function decodeAttribute(attr) {
313
518
  }
314
519
 
315
520
  module.exports = {
521
+ buildSigningCertificateV2: buildSigningCertificateV2,
522
+ certBindingFacts: certBindingFacts, // @internal
523
+ certBindingClaims: certBindingClaims, // @internal
524
+ certBindingAgainst: certBindingAgainst, // @internal
525
+
316
526
  parseSigningCertificate: parseSigningCertificate,
317
527
  parseSigningCertificateV2: parseSigningCertificateV2,
318
528
  parseSmimeCapabilities: parseSmimeCapabilities,
package/lib/tsp-sign.js CHANGED
@@ -25,7 +25,6 @@ var smime = require("./schema-smime");
25
25
  var schemaTsp = require("./schema-tsp");
26
26
  var guard = require("./guard-all");
27
27
  var intrinsic = require("./guard-intrinsic");
28
- var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
29
28
  var _compare = intrinsic.compare;
30
29
  var _pop = intrinsic.pop;
31
30
  var _reverse = intrinsic.reverse;
@@ -44,10 +43,6 @@ var _EXT_DECODERS = pkix.certExtensionDecoders(_NS).byOid;
44
43
  var b = asn1.build;
45
44
  function _err(code, message, cause) { return new TspError(code, message, cause); }
46
45
 
47
- function _hasNonZeroDigit(s) {
48
- for (var i = 0; i < s.length; i++) { var c = _charCodeAt(s, i); if (c >= 49 && c <= 57) return true; }
49
- return false;
50
- }
51
46
  function O(name) { return oid.byName(name); }
52
47
 
53
48
  var _b = pkiBuild.makeBuilder({
@@ -67,9 +62,15 @@ function _moreEstablished(candidate, current) {
67
62
  return _rankRevocation(candidate) > _rankRevocation(current);
68
63
  }
69
64
 
65
+ /** @internal The message imprint's algorithm identifier, emitted with the parameters field ABSENT.
66
+ * RFC 5754 sec. 2: "Implementations MUST generate SHA2 AlgorithmIdentifiers with absent
67
+ * parameters", and it is a SHA-2 digest here because that is the whole set this verb signs under.
68
+ * Both forms are still read: a request or token from elsewhere carrying NULL parameters verifies,
69
+ * and nothing compares the identifier's bytes, since an imprint is matched by the algorithm it
70
+ * names and the digest it carries. */
70
71
  function _hashAlgId(name) {
71
72
  if (!NODE_DIGEST[guard.text.keyOf(name)]) throw _err("tsp/unsupported-algorithm", "unsupported hash algorithm " + guard.text.showValue(name));
72
- return b.sequence([b.oid(O(name)), b.nullValue()]);
73
+ return b.sequence([b.oid(O(name))]);
73
74
  }
74
75
  function _policy(p) {
75
76
  if (typeof p !== "string") throw _err("tsp/bad-input", "the timestamp policy must be an OID name or dotted string");
@@ -88,12 +89,12 @@ function _certDer(c) {
88
89
  return der;
89
90
  }
90
91
 
92
+ /** @internal The sec. 2.4.2 binding, encoded by the one builder that owns the ESS domain. A second
93
+ * encoder here would be a second answer to "which bytes bind this certificate", and the two can
94
+ * drift: the SHA-384 and SHA-512 algorithm identifiers are generated with the parameters field
95
+ * absent (RFC 5754 sec. 2), and the SHA-256 field omitted as the RFC 5035 sec. 4 DEFAULT. */
91
96
  function _signingCertV2(certDer, hashName) {
92
- var certHash = guard.crypto.digest(NODE_DIGEST[hashName], certDer);
93
- var essCertId = hashName === "sha256"
94
- ? b.sequence([b.octetString(certHash)])
95
- : b.sequence([_hashAlgId(hashName), b.octetString(certHash)]);
96
- return b.sequence([b.sequence([essCertId])]);
97
+ return smime.buildSigningCertificateV2(certDer, { hashAlgorithm: hashName });
97
98
  }
98
99
 
99
100
  /**
@@ -432,17 +433,21 @@ function response(token, opts) {
432
433
  function parseResponse(input) { return schemaTsp.parse(input); }
433
434
 
434
435
 
435
- var _VERIFY_DIGEST = intrinsic.assign(intrinsic.create(null), { sha256: "sha256", sha384: "sha384", sha512: "sha512", sha1: "sha1" });
436
+ var _VERIFY_DIGEST = constants.NAMES.TIMESTAMP_IMPRINT_DIGESTS;
436
437
  var _VERIFY_DIGEST_LEN = constants.NAMES.DIGEST_OCTETS;
437
- var _BIND_DIGEST = intrinsic.assign(intrinsic.create(null), { sha256: "sha256", sha384: "sha384", sha512: "sha512" });
438
438
  /** @internal The digests admitted for an imprint that are not collision resistant, so a token under one
439
439
  * binds nothing an attacker with a collision cannot move. They verify only under `allowWeakDigests`. */
440
- var _WEAK_IMPRINT_DIGEST = intrinsic.assign(intrinsic.create(null), { sha1: 1 });
440
+ var _WEAK_IMPRINT_DIGEST = constants.NAMES.WEAK_DIGESTS;
441
441
 
442
442
  function _imprintMatches(mi, data, allowWeakDigests) {
443
443
  var name = mi.hashAlgorithm && mi.hashAlgorithm.name;
444
444
  var nodeAlg = (name && intrinsic.hasOwn(_VERIFY_DIGEST, name)) ? _VERIFY_DIGEST[name] : null;
445
445
  if (!nodeAlg) return "tsp/unsupported-algorithm";
446
+ /** @internal The identifier is held to its own encoding as well as to its name. A digest
447
+ * AlgorithmIdentifier carries absent or DER NULL parameters (RFC 5754 clause 2), and the ANY
448
+ * field reaches here unvalidated from the generic PKIX decoder, so an imprint naming sha256
449
+ * beside arbitrary bytes would otherwise be hashed as a well-formed one. */
450
+ if (!guard.der.paramsAbsentOrNull(mi.hashAlgorithm.parameters)) return "tsp/unsupported-algorithm";
446
451
  /** @internal The imprint is the ONLY thing binding a token to the data it timestamps, so the digest
447
452
  * that computes it carries the whole binding and a collision in it substitutes the data under a
448
453
  * genuine token. `allowWeakDigests` used to reach only the token's own signature digest, through
@@ -452,7 +457,11 @@ function _imprintMatches(mi, data, allowWeakDigests) {
452
457
  * already does with a signature digest, and the opt-in is the same option an archived token needs. */
453
458
  if (!allowWeakDigests && intrinsic.hasOwn(_WEAK_IMPRINT_DIGEST, name)) return "tsp/weak-digest";
454
459
  var wantLen = _VERIFY_DIGEST_LEN[name];
455
- if (mi.hashedMessage.length !== wantLen) return "tsp/imprint-mismatch";
460
+ /** @internal An imprint whose length is not the output length of the algorithm it names cannot be
461
+ * a digest under that algorithm, which is a different fault from a digest that covers other
462
+ * bytes, and it gets a code of its own: the first says the token is malformed, the second that it
463
+ * is well formed and stamps something else (RFC 3161 sec. 2.4.2). */
464
+ if (mi.hashedMessage.length !== wantLen) return "tsp/imprint-length";
456
465
  var actual;
457
466
  if (guard.bytes.isByteSource(data)) {
458
467
  var dataBytes = guard.bytes.source(data, TspError, "tsp/bad-input", "data");
@@ -468,104 +477,57 @@ function _imprintMatches(mi, data, allowWeakDigests) {
468
477
  return _compare(actual, _bufferFrom(mi.hashedMessage)) === 0 ? true : "tsp/imprint-mismatch";
469
478
  }
470
479
 
471
- /** @internal The certificate one binding attribute names, and the digest under which it names it.
472
- * An ESSCertID v1 carries NO hashAlgorithm field: it is SHA-1 by definition (RFC 2634 sec. 5.4.1),
473
- * and EN 319 122-1 clause 5.2.2.2 says the v1 attribute "shall be used if the SHA-1 hash algorithm is
474
- * used", so these tokens exist. An absent algorithm therefore means SHA-1 rather than nothing, and
475
- * reporting `tsp/unsupported-algorithm` for one named the wrong fault: the algorithm is known and the
476
- * binding is deliberately weak, which is a different refusal from one this build cannot compute.
477
- * ABSENT IS NOT THE SAME AS UNRECOGNIZED: a v1 ESSCertID omits the field, so absent carries the type's
478
- * default of SHA-1; an ESSCertIDv2 naming an OID this build does not know carries an algorithm that
479
- * genuinely cannot be computed, and that one is `tsp/unsupported-algorithm`.
480
- * Returns a refusal code, or the ESSCertID with the digest resolved. SHA-1 resolves rather than
481
- * refusing, because whether a weak binding is a refusal depends on what the caller is doing with it:
482
- * the attribute the verdict rests on is held to it, and an attribute read only for agreement is not. */
483
- function _readBinding(attr) {
484
- var decoded;
485
- try { decoded = smime.decodeAttribute(attr); }
486
- catch (_e) { return "tsp/bad-signing-certificate"; }
487
- var essCert = decoded.certs && decoded.certs[0];
488
- if (!essCert) return "tsp/bad-signing-certificate";
489
- var name = essCert.hashAlgorithm == null ? "sha1" : essCert.hashAlgorithm.name;
490
- if (name === "sha1") return { essCert: essCert, nodeDigest: "sha1", weak: true };
491
- if (!name || !intrinsic.hasOwn(_BIND_DIGEST, name)) return "tsp/unsupported-algorithm";
492
- return { essCert: essCert, nodeDigest: _BIND_DIGEST[name], weak: false };
493
- }
494
-
495
- /** @internal Whether one ESSCertID names the certificate that signed the token, by every claim it
496
- * carries: the hash under its own digest, and the issuer and serial where it states them. Returns
497
- * true or the refusal code. An ESSCertID states the signer in two independent ways and both are
498
- * compared, because a hash that agrees beside an issuer and serial that name another certificate is
499
- * the same contradiction as two attributes disagreeing. Certificate identifiers after the first are
500
- * not read: RFC 2634 sec. 5.4 makes the first the signing certificate and leaves the rest as the
501
- * chain, so they say nothing about the signer. */
502
- function _bindingNamesCert(read, tsaCertDer) {
503
- /** @internal Through the captured digest: this comparison IS the binding between a token and the
504
- * certificate its signing-certificate attribute names, so what the digest covers is not the
505
- * caller's to choose. */
506
- if (_compare(guard.crypto.digest(read.nodeDigest, tsaCertDer),
507
- _bufferFrom(read.essCert.certHash)) !== 0) return "tsp/cert-binding-mismatch";
508
- var issuerSerial = read.essCert.issuerSerial;
509
- if (!issuerSerial) return true;
510
- var signerCert;
511
- try { signerCert = pkiX509.parse(tsaCertDer); }
512
- catch (_e) { return "tsp/bad-signing-certificate"; }
513
- if (issuerSerial.serialNumber !== signerCert.serialNumber) return "tsp/cert-binding-mismatch";
514
- var names = issuerSerial.issuer.names || [];
515
- var gn = null;
516
- try { if (names.length === 1) gn = schemaTsp.decodeGeneralName(names[0].bytes); }
517
- catch (_e2) { return "tsp/cert-binding-mismatch"; }
518
- if (!gn || gn.tagNumber !== 4 || !gn.value || !guard.name.dnEqual(gn.value.rdns, signerCert.issuer.rdns, _err, "tsp/cert-binding-mismatch", "the ESSCertID issuer")) {
519
- return "tsp/cert-binding-mismatch";
520
- }
521
- return true;
522
- }
480
+ /** @internal An ESSCertID v1 carries NO hashAlgorithm field: it is SHA-1 by definition (RFC 2634
481
+ * sec. 5.4.1), and EN 319 122-1 clause 5.2.2.2 says the v1 attribute "shall be used if the SHA-1 hash
482
+ * algorithm is used", so these tokens exist. The reader therefore reports SHA-1 as WEAK rather than as
483
+ * unsupported, which is a different refusal: the algorithm is known and the binding is deliberately
484
+ * weak, where an ESSCertIDv2 naming an OID this build does not know carries one that genuinely cannot
485
+ * be computed. Which of the two a caller refuses on is the caller's policy, applied below. */
523
486
 
524
487
  function _checkCertBinding(signerInfo, tsaCertDer) {
525
- var attrs = (signerInfo && signerInfo.signedAttrs) || [];
526
- var binding = [];
527
- for (var i = 0; i < attrs.length; i++) {
528
- if (attrs[i].type === O("signingCertificateV2") || attrs[i].type === O("signingCertificate")) {
529
- intrinsic.push(binding, attrs[i]);
530
- }
531
- }
532
- if (!binding.length) return "tsp/missing-signing-certificate";
488
+ var facts = smime.certBindingFacts((signerInfo && signerInfo.signedAttrs) || [], tsaCertDer);
489
+ if (!facts.present) return "tsp/missing-signing-certificate";
533
490
  /** @internal A v2 binding supersedes a v1 one (RFC 5035), and that is why the verdict rests on the v2
534
491
  * where there is one. But the v1 attribute does not stop existing: a token carrying both used to be
535
492
  * checked against the v2 alone, so one naming the real signer in v2 and a DIFFERENT certificate in v1
536
493
  * verified, and a legacy verifier reading only v1 resolves a certificate this one never checked. Both
537
494
  * sit in signedAttrs, so the TSA signed the contradiction. One structure making two incompatible
538
495
  * claims about one fact is refused rather than resolved in favor of whichever attribute a reader
539
- * prefers.
540
- * EVERY BINDING ATTRIBUTE IS COLLECTED, not the first of each type. signedAttrs is a DER SET OF,
541
- * sorted by encoded value, so which of the two attributes comes first is a function of their lengths
542
- * and the producer of a hostile token chooses it: a scan that stopped at the v2 caught the
543
- * contradiction in one ordering and missed it in the other, and an ordering-dependent check is no
544
- * check. The same applies to a repeated instance of either type, which `pki.cms.verify` refuses
545
- * before this runs and which is not relied on here.
496
+ * prefers. Which attribute is `chosen` and which is `superseded` is decided over EVERY binding
497
+ * attribute rather than the first of each type, because signedAttrs is a DER SET OF and a hostile
498
+ * producer picks the order through the encodings.
546
499
  * THE SHA-1 ON A SUPERSEDED ATTRIBUTE PINS NOTHING. It detects a disagreement, while the binding the
547
500
  * verdict rests on is the v2 one. A collision would let a disagreeing v1 pass for an agreeing one,
548
501
  * which costs an attacker the refusal and gains them nothing: the certificate the token is held to is
549
502
  * the one v2 names, under its own digest. */
550
- var chosen = 0;
551
- for (var j = 0; j < binding.length; j++) {
552
- if (binding[j].type === O("signingCertificateV2")) { chosen = j; break; }
553
- }
554
- for (var k = 0; k < binding.length; k++) {
555
- if (k === chosen) continue;
556
- var alt = _readBinding(binding[k]);
557
- if (typeof alt === "string") return alt;
558
- var altCode = _bindingNamesCert(alt, tsaCertDer);
503
+ for (var k = 0; k < facts.superseded.length; k++) {
504
+ var altCode = _bindingClaimCode(facts.superseded[k]);
559
505
  if (altCode !== true) return altCode;
560
506
  }
561
- var read = _readBinding(binding[chosen]);
562
- if (typeof read === "string") return read;
507
+ var read = facts.chosen;
508
+ if (!read.readable) return "tsp/bad-signing-certificate";
509
+ if (!read.supported) return "tsp/unsupported-algorithm";
563
510
  /** @internal The refusal of a weak binding stands whatever `allowWeakDigests` says, because a binding
564
511
  * is not the archived-signature case the option exists for. Whether it should follow the imprint and
565
512
  * become an opt-in is a posture question, and the refusal was pinned deliberately, so it is left as
566
513
  * it was. */
567
514
  if (read.weak) return "tsp/weak-digest";
568
- return _bindingNamesCert(read, tsaCertDer);
515
+ return _bindingClaimCode(read);
516
+ }
517
+
518
+ /** @internal The refusal one binding attribute's claims earn, in the order the claims are decided:
519
+ * an attribute that does not decode, then a digest this build does not bind with, then the hash,
520
+ * then the certificate the issuer and serial would be read off, then those two. The hash is
521
+ * compared before the certificate is parsed, because a hash that disagrees is a mismatch whatever
522
+ * the certificate turns out to be. The weak-digest refusal is the caller's, since it applies to the
523
+ * attribute the verdict rests on and not to one read only for agreement. */
524
+ function _bindingClaimCode(e) {
525
+ if (!e.readable) return "tsp/bad-signing-certificate";
526
+ if (!e.supported) return "tsp/unsupported-algorithm";
527
+ if (e.matches !== true) return "tsp/cert-binding-mismatch";
528
+ if (e.certUnreadable) return "tsp/bad-signing-certificate";
529
+ if (e.issuerSerialMatches === false) return "tsp/cert-binding-mismatch";
530
+ return true;
569
531
  }
570
532
 
571
533
  function _checkTsaCertUsage(tsaCertDer) {
@@ -755,7 +717,8 @@ async function verify(token, data, opts) {
755
717
  if (opts.trustAnchors) {
756
718
  var pathRes = null;
757
719
  var floorT = tst.genTime;
758
- var ceilT = (tst.genTimeFraction != null && _hasNonZeroDigit(tst.genTimeFraction.slice(3))) ? new Date(guard.time.instantOf(floorT) + 1) : floorT;
720
+ var ceilAt = guard.time.ceilInstantOf(floorT, tst.genTimeFraction);
721
+ var ceilT = ceilAt === guard.time.instantOf(floorT) ? floorT : new Date(ceilAt);
759
722
  try {
760
723
  var pool = (parsed.certificates || []).filter(function (c) { return c.tagClass === "universal"; }).map(function (c) { return pkiX509.parse(c.bytes); });
761
724
  (opts.certs || []).forEach(function (c) { guard.list.append(pool, pkiX509.parse(c)); });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.8.51",
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:3a1ddfe7-c71b-4cb4-8bd0-256c3b6075ba",
5
+ "serialNumber": "urn:uuid:f287baca-f745-4ecd-8265-d6953251f0b6",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-10-05T13:17:14.754Z",
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.51",
22
+ "bom-ref": "@blamejs/pki@0.8.53",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.8.51",
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.51",
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.51",
57
+ "ref": "@blamejs/pki@0.8.53",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]