@blamejs/pki 0.7.4 → 0.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,28 @@ All notable changes to `@blamejs/pki` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## v0.7.6 — 2026-09-10
8
+
9
+ A Sigstore bundle is held to the shape its specification states, in signature count and in entry count.
10
+
11
+ ### Changed
12
+
13
+ - A bundle offering more than 32 transparency-log entries is refused with sigstore/bad-bundle, naming the count it offered. Each entry a bundle offers can cost a certification-path validation, since an entry is tried until one passes every check that depends on which entry it is, and the validation instant comes from the entry itself unless the caller pins opts.time. Nothing bounded the count: the 1 MiB bundle budget leaves room for roughly 155 entries, and every bundle in the Sigstore conformance suite carries exactly one. The ceiling is a local resource bound rather than a rule the specification states, and it is checked before any entry is read. It is available as pki.C.LIMITS.TLOG_MAX_COUNT.
14
+
15
+ ### Fixed
16
+
17
+ - A DSSE envelope carrying more than one signature is refused with sigstore/bad-dsse, naming the count it carried. The Sigstore bundle specification states the rule on the producer, that an envelope in a bundle has exactly one signature, and again on the verifier, that a client rejects an envelope whose signature count is not one. Only the first signature was ever read, so an envelope with a second was accepted and returned a verdict that said nothing about it. An envelope with exactly one is unaffected, and an empty signature list keeps the refusal it already had.
18
+
19
+ ## v0.7.5 — 2026-09-10
20
+
21
+ Certificate linting now covers the ML-DSA and SLH-DSA certificate profiles.
22
+
23
+ ### Added
24
+
25
+ - pki.lint checks an ML-DSA certificate's key usage against RFC 9881 section 5, as the finding id lint/rfc9881/mldsa-key-usage under the new profile name rfc9881. Where the keyUsage extension is present, at least one of digitalSignature, nonRepudiation, keyCertSign or cRLSign must be set and none of keyEncipherment, dataEncipherment, keyAgreement, encipherOnly or decipherOnly may be. The finding carries which key-establishment bits were set and whether a signing bit was present. A certificate carrying no keyUsage extension is unconstrained. All three parameter sets are covered.
26
+ - pki.lint checks an SLH-DSA certificate's key usage against RFC 9909 section 6, as the finding id lint/rfc9909/slhdsa-key-usage under the new profile name rfc9909. The requirement is the same pair. RFC 9909 section 5 admits both the pure id-slh-dsa-* and the prehash id-hash-slh-dsa-*-with-* identifiers as a subject public key algorithm, and the rule reaches both, across all twelve parameter sets of each.
27
+ - pki.lint.profiles() returns rfc5280, rfc9881, rfc9909, rfc9935 and cabf-tls. Passing rfc9881 or rfc9909 as opts.profile runs that profile on its own, and pki.lint.rules(name) enumerates its rows.
28
+
7
29
  ## v0.7.4 — 2026-09-10
8
30
 
9
31
  Two authoring inputs that were accepted quietly are now refused by name.
package/README.md CHANGED
@@ -252,7 +252,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
252
252
  | `pki.sigstore` | Offline verifier for a Sigstore bundle, the artifact `npm publish --provenance` produces and the registry serves. `verifyBundle` composes five fail-closed legs against caller-pinned trust (Fulcio CA roots and Rekor log keys, never trusted from the bundle): the DSSE signature over its PAE preimage under the Fulcio leaf key, the ephemeral Fulcio certificate chain validated as of the Rekor log time, the RFC 9162 inclusion proof folded to a Rekor-signed tree root, the log entry bound to this exact signature, and the in-toto SLSA subject digest the caller confirms against the published artifact. It reuses the X.509 parser, the RFC 5280 path validator, and the Merkle verifier; the net-new codecs are the DSSE PAE byte-builder and a fail-closed JSON reader. `verified: true` says the artifact was signed and logged, not that a party you trust signed it — Fulcio issues to anyone who completes an OIDC flow, so who signed is decided only by `opts.identity`, and `identityChecked` reports which of its fields were compared. An identity policy naming no field, or a field name that is not one of the three, is refused rather than satisfied — either would accept every signer while reading as a policy in force. A sixth leg is opt-in: pinning the certificate-transparency logs in `opts.ctLogs` checks the receipt Fulcio embeds in the certificate it issues (RFC 6962 §3.2), verified over the certificate as it stood before the receipt was added and under the key of the log that issued it, so the verdict says the signing certificate was public when it was issued rather than handed out quietly. At least one receipt must verify against a pinned log, the verdict reports `sctChecked` and `validScts`, and an empty log set is refused rather than read as a policy that checks nothing. Both content arms are read: a DSSE-wrapped in-toto attestation, and a message signature over an artifact's own bytes, which `cosign sign-blob` produces. For that arm `opts.artifact` supplies the bytes, and they are held to the hash the Rekor `hashedrekord` entry records before the signature is checked over them; the `messageDigest` the bundle carries is covered by no signature, so it is compared against the computed digest and never used in its place. `contentType` names the arm and the verdict has one shape for both — `pae`, `parseBundle`, `verifyBundle` |
253
253
  | `pki.inspect` | Human-readable inspection, the pure-JS equivalent of `openssl x509/crl/req/cms -text`. `certificate(pem \| der \| parsed)` renders an OpenSSL-style report: version, serial, signature algorithm, issuer and subject distinguished names, validity, public-key details (curve or modulus size plus the raw point or modulus), every decoded extension with its critical flag, and the signature. `crl`, `csr`, and `cms` render the other formats the same way — a CRL like `openssl crl -text`, a CSR like `openssl req -text`, and a CMS message like `openssl cms -cmsout -print`, with a stable summary for a non-SignedData ContentInfo — and `any(input)` detects the format and routes to the right report. Built over the strict parsers and the two-way OID registry with one set of field renderers, it names extension and algorithm OIDs an OpenSSL build shows only as raw bytes. No OpenSSL dependency, and the format is stable and OpenSSL-familiar rather than pinned to one OpenSSL version. A certificate policy's user notice renders as text, both its explicit text and a notice reference with the notice numbers that identify it, rather than hex, and a malformed part falls back to a hex dump rather than throwing — `certificate`, `crl`, `csr`, `cms`, `any` |
254
254
  | `pki.webauthn` | WebAuthn and passkey verification, both halves: offline trust evaluation of a W3C WebAuthn (Level 3) registration, and signature verification of the assertion every login returns. `parseAttestationObject(bytes)` decodes the CBOR attestation object, authenticatorData, and COSE credential key over the strict `pki.cbor` codec; `parseAuthenticatorData(bytes)` reads the bare form an assertion carries through the same parser; `parseClientData(bytes, opts)` decodes the `clientDataJSON` no signature check looks inside, through the shared JSON guard since these are attacker-chosen bytes, returning the challenge decoded so a caller compares bytes rather than spellings, and checking the ceremony type, challenge, and origin when the relying party supplies what it issued. `verifyAssertion(input)` verifies an assertion signature over `authenticatorData \|\| SHA-256(clientDataJSON)` — raw bytes, no COSE_Sign1, an ES256 signature in ASN.1 DER — and applies the §7.2 step 21 counter rule when a stored `previousSignCount` is given, so a counter that fails to advance is refused as a cloned authenticator. `verify(attestationObject, clientDataHash, opts)` checks the attestation-statement signature and each format's structural bindings for packed, tpm, android-key, apple, fido-u2f, and none: the x5c leaf key, the apple nonce, the tpm `certInfo` Name and `extraData` over the `pubArea`, the android `KeyDescription`, and the fido-u2f `verificationData`. It binds the credential public key to each attestation, through the signed authenticatorData for packed and fido-u2f or a cert or `pubArea`-key equality check for android-key, apple, and tpm, and enforces each leaf's certificate requirements. The credential-key check covers the full WebAuthn COSE algorithm set — ES256/384/512, RS256/384/512, PS256/384/512, EdDSA (Ed25519), and the RFC 9864 fully-specified identifiers ESP256/384/512, Ed25519, and Ed448 — validating the public-key point on its curve, rejecting the compressed EC point form, and enforcing a minimally encoded DER ECDSA signature. The verdict field is `attestationVerified`, and `signatureVerified` for an assertion, rather than a bare `verified`, because a sound statement is a different claim from an acceptable ceremony: an attestation naming another origin's RP ID with user presence clear is perfectly sound and must not be registered. Pass `expectedRpId`, `requireUserPresence`, `requireUserVerification`, or `allowedAlgorithms` and those are checked, with `bindingChecked` reporting which ran, so a check that passed can be told from one that never happened. Pass `opts.clientDataJSON` instead of the digest and the client data is read here too: the ceremony type is checked unconditionally, since a login response replayed into a registration is what that rule stops, and the challenge, origin, and top-level origin are checked against what the relying party issued, with `clientData.checked` reporting which comparisons ran. A registration verdict also carries the `credentialId`, `credentialPublicKey`, and initial `signCount` a later login needs. A credential key declaring COSE algorithm `-65535` (RSASSA-PKCS1-v1_5 with SHA-1) is refused unless `allowedAlgorithms` names it, since every signature that credential ever makes would use SHA-1. Anchoring the trust path has two routes: `opts.metadata` resolves the roots the authenticator's own model registered, and `opts.rootCertificates` pins roots directly, which is what anchors the formats FIDO MDS does not cover, Apple's authenticators and the Google hardware-attestation roots among them. `metadata` governs when both are given, and `anchoredTo` names every route that anchored the path, joined with `+` when more than one did: `"metadata"`, `"rootCertificates"`, and `"safetyNetRoots"` for the android-safetynet chain, which anchors through the roots that format requires whether or not either other route was asked for. It is `null` only when nothing anchored the path. `verifyMetadataBlob(blob, opts)` reads a FIDO Metadata Service (MDS v3) BLOB, the signed catalogue of registered authenticator models, verifying its JWS and chaining its signer to an operator-supplied FIDO root before the payload is parsed, with sequence-number rollback and `nextUpdate` freshness checks. Passing the result as `opts.metadata` to `verify` resolves the authenticator's registered attestation roots from its identifier and requires the trust path to fully validate to one of them, refusing an unlisted or revoked model. `metadataAnchors(entry, opts)` applies the same status gate for a caller anchoring the path themselves: a model the catalogue has disqualified registers no anchors. Both of the catalogue's key spaces are covered: an aaguid, and the attestation-certificate key identifiers a U2F authenticator is listed under instead. No FIDO root is bundled, there is no trust-on-first-use, and retrieving the BLOB is out of scope. Fail-closed with typed `webauthn/*` errors — `parseAttestationObject`, `verify`, `verifyMetadataBlob`, `metadataFor`, `metadataAnchors` |
255
- | `pki.lint` | Certificate linting, the zlint or pkilint of JavaScript. `certificate(pem \| der \| parsed, opts)` walks a parsed certificate and emits graded advisory findings, each with a stable id, a severity (`fatal`, `error`, `warn`, `notice`), a source, a spec-clause citation, and a message, against the RFC 5280 profile plus a representative CA/Browser Forum TLS BR subset: serial sign and size, validity ordering and the SC081v3 reducing validity schedule, keyCertSign coherence, extension criticality (basicConstraints, nameConstraints, policyConstraints and inhibitAnyPolicy must be critical, and keyUsage should be), nameConstraints CA-scope, unknown critical extensions, empty-subject SAN, SKI and AKI presence including the end-entity subjectKeyIdentifier, SAN required and CN-in-SAN, dNSName syntax, serverAuth EKU, weak keys, and the §4.2.1.4 certificate-policy user-notice rules (a VisibleString or BMPString explicitText, a notice past 200 characters, an empty notice, control characters, and a non-NFC UTF8String notice, each at the strength the clause states). Alone among these entries the data path never throws: hostile bytes return a `fatal` `lint/unparseable` finding carrying the strict parser's code, so a whole directory lints without a try/catch, and only config-time misuse throws a typed `LintError` — `certificate`, `rules`, `profiles` |
255
+ | `pki.lint` | Certificate linting, the zlint or pkilint of JavaScript. `certificate(pem \| der \| parsed, opts)` walks a parsed certificate and emits graded advisory findings, each with a stable id, a severity (`fatal`, `error`, `warn`, `notice`), a source, a spec-clause citation, and a message, against the RFC 5280 profile, the post-quantum certificate profiles, and a representative CA/Browser Forum TLS BR subset: serial sign and size, validity ordering and the SC081v3 reducing validity schedule, keyCertSign coherence, extension criticality (basicConstraints, nameConstraints, policyConstraints and inhibitAnyPolicy must be critical, and keyUsage should be), nameConstraints CA-scope, unknown critical extensions, empty-subject SAN, SKI and AKI presence including the end-entity subjectKeyIdentifier, SAN required and CN-in-SAN, dNSName syntax, serverAuth EKU, weak keys, the post-quantum key-usage rules binding a certificate's keyUsage bits to its subject key type (ML-KEM under RFC 9935 §5, ML-DSA under RFC 9881 §5, and SLH-DSA under RFC 9909 §6 across both its pure and prehash identifiers) together with the ML-KEM encapsulation-key encoding and size, and the §4.2.1.4 certificate-policy user-notice rules (a VisibleString or BMPString explicitText, a notice past 200 characters, an empty notice, control characters, and a non-NFC UTF8String notice, each at the strength the clause states). Alone among these entries the data path never throws: hostile bytes return a `fatal` `lint/unparseable` finding carrying the strict parser's code, so a whole directory lints without a try/catch, and only config-time misuse throws a typed `LintError` — `certificate`, `rules`, `profiles` |
256
256
  | `pki.C` / `pki.constants` | Version-stable constants: the functional scale helpers `C.TIME.*` and `C.BYTES.*`, the codec `LIMITS`, and `version` |
257
257
  | `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError`, `Asn1Error`, `OidError`, `PemError`, `CertificateError`, `CrlError`, `CsrError`, `Pkcs8Error`, `CmsError`, `OcspError`, `TspError`, `AttrCertError`, `CrmfError`, `Pkcs12Error`, `CmpError`, `PathError`, `CtError`, `JoseError`, `AcmeError`, `WebauthnError`, and `LintError`, each carrying a stable `code` in `domain/reason` form |
258
258
  | `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>`, `pki inspect <cert>`, `pki lint <cert>`, `pki convert <file> --to der\|pem`, `pki verify <cert>... --anchor <cert>`, `pki sign <file> --cert <c> --key <k>` |
package/lib/constants.js CHANGED
@@ -128,6 +128,7 @@ var LIMITS = Object.assign(Object.create(null), {
128
128
  CBOR_MAX_ITEMS: 1000000,
129
129
  SCT_MAX_BYTES: BYTES.kib(64) + 1,
130
130
  SCT_MAX_COUNT: 256,
131
+ TLOG_MAX_COUNT: 32,
131
132
  CMS_MAX_KARI_ENCRYPTED_KEYS: 64,
132
133
  CMP_KEM_MAX_KEY_BYTES: 1024,
133
134
  CMP_KEM_MIN_KEY_BYTES: 20,
package/lib/lint.js CHANGED
@@ -10,8 +10,9 @@
10
10
  * ALREADY-PARSED certificate (from `pki.schema.x509.parse`, whose extension values it
11
11
  * decodes with the shared RFC 5280 decoders) and emits graded, advisory FINDINGS: each
12
12
  * with a stable id, a severity (`fatal` > `error` > `warn` > `notice` > `pass`), a source,
13
- * a spec-clause citation, and a human message. It ships the RFC 5280 certificate profile
14
- * plus a representative CA/Browser Forum TLS Baseline Requirements subset.
13
+ * a spec-clause citation, and a human message. It ships the RFC 5280 certificate profile,
14
+ * the post-quantum certificate profiles for ML-KEM, ML-DSA and SLH-DSA, and a
15
+ * representative CA/Browser Forum TLS Baseline Requirements subset.
15
16
  *
16
17
  * Unlike every other toolkit entry, the lint data path never throws. A linter surveys a
17
18
  * corpus that includes malformed members, so `pki.lint.certificate(hostileBytes)` returns
@@ -20,7 +21,7 @@
20
21
  * unknown profile, an out-of-range severity threshold, or a wrong-type input), which
21
22
  * raises a typed `LintError`. This deliberate inversion of the toolkit's fail-closed-throw
22
23
  * posture is what lets an operator lint a whole directory without a try/catch per file.
23
- * @spec RFC 5280, CA/Browser Forum TLS Baseline Requirements
24
+ * @spec RFC 5280, RFC 9881, RFC 9909, RFC 9935, CA/Browser Forum TLS Baseline Requirements
24
25
  * @card Lint a certificate against RFC 5280 + CABF TLS BR, in pure JS.
25
26
  */
26
27
 
@@ -511,12 +512,61 @@ var RFC9935_RULES = [
511
512
  },
512
513
  ];
513
514
 
515
+ /** @internal RFC 9881 sec. 5 and RFC 9909 sec. 6 state one pair of requirements for a post-quantum
516
+ * signature key, both conditioned on keyUsage being present: at least one of digitalSignature,
517
+ * nonRepudiation, keyCertSign or cRLSign is set, and none of keyEncipherment, dataEncipherment,
518
+ * keyAgreement, encipherOnly or decipherOnly is. The nine RFC 5280 bits are exhausted by those two
519
+ * lists, so the only input that fails the signing-bit requirement without also setting a
520
+ * key-establishment bit is one whose sole bit is reserved. A keyUsage with no bits set never
521
+ * reaches either rule: the extension decoder refuses it first. */
522
+ function _spkiAlgName(cert) {
523
+ var spki = cert.subjectPublicKeyInfo;
524
+ var n = spki && spki.algorithm && spki.algorithm.name;
525
+ return typeof n === "string" ? n : "";
526
+ }
527
+ function _isMlDsa(cert) { return _spkiAlgName(cert).indexOf("id-ml-dsa-") === 0; }
528
+ /** @internal RFC 9909 sec. 5 admits both the pure and the prehash identifier as a subject public
529
+ * key algorithm, and sec. 6 binds the key usage of both. */
530
+ function _isSlhDsa(cert) {
531
+ var n = _spkiAlgName(cert);
532
+ return n.indexOf("id-slh-dsa-") === 0 || n.indexOf("id-hash-slh-dsa-") === 0;
533
+ }
534
+ var PQC_SIG_ESTABLISHMENT_BITS = ["keyEncipherment", "dataEncipherment", "keyAgreement", "encipherOnly", "decipherOnly"];
535
+ var PQC_SIG_SIGNING_BITS = ["digitalSignature", "nonRepudiation", "keyCertSign", "cRLSign"];
536
+ function _pqcSigKeyUsage(cert, ctx) {
537
+ var ku = ctx.decode("keyUsage");
538
+ if (!ku || !ku.value) return null;
539
+ var v = ku.value;
540
+ var establishment = PQC_SIG_ESTABLISHMENT_BITS.filter(function (bit) { return v[bit] === true; });
541
+ var signing = PQC_SIG_SIGNING_BITS.some(function (bit) { return v[bit] === true; });
542
+ if (establishment.length === 0 && signing) return null;
543
+ return { context: { keyEstablishmentBitsSet: establishment, signingBitSet: signing } };
544
+ }
545
+ function _pqcSigKeyUsageRule(id, source, citation, family, appliesTo) {
546
+ return {
547
+ id: id, severity: "error", source: source, citation: citation,
548
+ message: "an " + family + " certificate's keyUsage, if present, must set at least one signing bit "
549
+ + "(digitalSignature, nonRepudiation, keyCertSign, cRLSign) and no key-establishment bit "
550
+ + "(keyEncipherment, dataEncipherment, keyAgreement, encipherOnly, decipherOnly)",
551
+ appliesTo: appliesTo,
552
+ check: _pqcSigKeyUsage,
553
+ };
554
+ }
555
+ var RFC9881_RULES = [
556
+ _pqcSigKeyUsageRule("lint/rfc9881/mldsa-key-usage", "rfc9881", "RFC 9881 5", "ML-DSA", _isMlDsa),
557
+ ];
558
+ var RFC9909_RULES = [
559
+ _pqcSigKeyUsageRule("lint/rfc9909/slhdsa-key-usage", "rfc9909", "RFC 9909 6", "SLH-DSA", _isSlhDsa),
560
+ ];
561
+
514
562
  var PROFILES = Object.assign(Object.create(null), {
515
563
  "rfc5280": RFC5280_RULES,
564
+ "rfc9881": RFC9881_RULES,
565
+ "rfc9909": RFC9909_RULES,
516
566
  "rfc9935": RFC9935_RULES,
517
567
  "cabf-tls": CABF_TLS_RULES,
518
568
  });
519
- var ALL_RULES = RFC5280_RULES.concat(RFC9935_RULES).concat(CABF_TLS_RULES);
569
+ var ALL_RULES = RFC5280_RULES.concat(RFC9881_RULES).concat(RFC9909_RULES).concat(RFC9935_RULES).concat(CABF_TLS_RULES);
520
570
 
521
571
  function _selectRules(profile) {
522
572
  if (profile == null || profile === "all" || profile === "default") return ALL_RULES;
@@ -535,11 +585,12 @@ function _applyThreshold(report, severity) {
535
585
  * @signature pki.lint.certificate(input, opts?) -> LintReport
536
586
  * @since 0.2.10
537
587
  * @status stable
538
- * @spec RFC 5280, CA/Browser Forum TLS BR
588
+ * @spec RFC 5280, RFC 9881, RFC 9909, RFC 9935, CA/Browser Forum TLS BR
539
589
  * @related pki.schema.x509.parse, pki.inspect.certificate, pki.path.validate
540
590
  *
541
- * Lint a certificate against the RFC 5280 profile plus a representative CABF TLS BR
542
- * subset. `input` is a PEM string, a DER `Buffer`, or an already-parsed
591
+ * Lint a certificate against the RFC 5280 profile, the post-quantum certificate profiles,
592
+ * and a representative CABF TLS BR subset. `input` is a PEM string, a DER `Buffer`, or an
593
+ * already-parsed
543
594
  * `pki.schema.x509` object. Returns a `LintReport`
544
595
  * `{ findings: [{id, severity, source, citation, message, context?}], counts, worst, ran }`.
545
596
  *
@@ -608,11 +659,11 @@ function rules(profile) {
608
659
  * @signature pki.lint.profiles() -> [string]
609
660
  * @since 0.2.10
610
661
  * @status stable
611
- * @spec RFC 5280, CA/Browser Forum TLS BR
662
+ * @spec RFC 5280, RFC 9881, RFC 9909, RFC 9935, CA/Browser Forum TLS BR
612
663
  *
613
664
  * List the known lint-profile names.
614
665
  * @example
615
- * pki.lint.profiles(); // -> ["rfc5280", "rfc9935", "cabf-tls"]
666
+ * pki.lint.profiles(); // -> ["rfc5280", "rfc9881", "rfc9909", "rfc9935", "cabf-tls"]
616
667
  */
617
668
  function profiles() { return Object.keys(PROFILES); }
618
669
 
package/lib/sigstore.js CHANGED
@@ -290,6 +290,13 @@ function parseBundle(input) {
290
290
  !d.signatures[0] || typeof d.signatures[0] !== "object" || typeof d.signatures[0].sig !== "string") {
291
291
  throw _err("sigstore/bad-dsse", "the DSSE envelope is missing a required field (payload / payloadType / signatures[].sig)");
292
292
  }
293
+ /** @internal The bundle specification states this on the producer and again on the verifier: an
294
+ * envelope in a bundle carries exactly one signature, and a verifier rejects one whose count is
295
+ * not one. Only `signatures[0]` is ever read, so admitting a second would return a verdict that
296
+ * says nothing about content the envelope carries. */
297
+ if (d.signatures.length !== 1) {
298
+ throw _err("sigstore/bad-dsse", "a bundle's DSSE envelope carries exactly one signature; this one carries " + d.signatures.length);
299
+ }
293
300
  return obj;
294
301
  }
295
302
 
@@ -1263,6 +1270,15 @@ async function verifyBundle(bundle, opts) {
1263
1270
 
1264
1271
  var tlogs = _isArray(vm.tlogEntries) ? vm.tlogEntries : [];
1265
1272
  if (!tlogs.length) throw _err("sigstore/bad-bundle", "a keyless bundle requires at least one transparency-log entry");
1273
+ /** @internal An entry is tried until one passes every check that depends on which entry it is,
1274
+ * and those checks include a certification-path validation whose instant comes from the entry
1275
+ * itself unless the caller pins one. The count is therefore a work multiplier, and the byte
1276
+ * budget alone leaves room for far more entries than any real bundle carries. This is a local
1277
+ * resource bound rather than a rule the bundle specification states. */
1278
+ if (tlogs.length > C.LIMITS.TLOG_MAX_COUNT) {
1279
+ throw _err("sigstore/bad-bundle", "a bundle offers at most " + C.LIMITS.TLOG_MAX_COUNT
1280
+ + " transparency-log entries; this one offers " + tlogs.length);
1281
+ }
1266
1282
  /** @internal Selecting an entry means finding one that passes EVERY check deciding WHICH entry it
1267
1283
  * is: that it binds to this signature and certificate, that it records this artifact, and that the
1268
1284
  * certificate chain and the embedded timestamps hold at the instant it attests. Any of those
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.7.4",
3
+ "version": "0.7.6",
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:f78d7d30-68ed-48a9-905f-3e46577f4005",
5
+ "serialNumber": "urn:uuid:8551b89b-6716-4eab-914a-0a3d1aafe57d",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-10T10:14:34.771Z",
8
+ "timestamp": "2026-09-10T12:38:33.413Z",
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.7.4",
22
+ "bom-ref": "@blamejs/pki@0.7.6",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.7.4",
25
+ "version": "0.7.6",
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.7.4",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.7.6",
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.7.4",
57
+ "ref": "@blamejs/pki@0.7.6",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]