@blamejs/pki 0.2.2 → 0.2.4

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,7 +4,24 @@ 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.2.2 — 2026-07-12
7
+ ## v0.2.4 — 2026-07-12
8
+
9
+ Human-readable certificate inspection joins the toolkit as pki.inspect.
10
+
11
+ ### Added
12
+
13
+ - pki.inspect.certificate(input) renders a certificate (a PEM string, a DER Buffer, or a pki.schema.x509.parse result) as a human-readable OpenSSL-x509-text-style report, composed over the strict X.509 parser, the shared RFC 5280 extension decoders, and the two-way OID registry. Standard extensions (basic constraints, key usage, extended key usage with purpose names, subject/issuer alternative names, subject and authority key identifiers, and more) decode to their content; an extension with no decoder is named from the registry and shown as its string or a hex dump. A value that is not a certificate, DER, or PEM throws inspect/bad-input; malformed certificate bytes throw inspect/bad-certificate; a malformed single extension never sinks the report. The error taxonomy gains InspectError (inspect/*). RFC 5280.
14
+
15
+ ## v0.2.3 — 2026-07-12
16
+
17
+ Offline Sigstore bundle verification joins the toolkit as pki.sigstore.
18
+
19
+ ### Added
20
+
21
+ - pki.sigstore.verifyBundle(bundle, opts) verifies a Sigstore bundle offline against caller-supplied trust material (opts.fulcioRoots, the Fulcio CA certificates; opts.rekorKeys, the Rekor log public keys, each with an optional validFor window honored against the log time so a rotated-out key is not used; optional opts.identity policy and opts.time). It returns { verified: true, payload, statement, subjects, predicateType, predicate, identity, integratedTime } on success -- payload being the raw verified envelope bytes, never a re-serialization -- and throws a typed sigstore/* error on any leg's failure. The transparency-log entry is bound to both the bundle signature and its leaf certificate, and only the v0.1-v0.3 bundle versions this release verifies are accepted (a newer version is recognized and deferred). pki.sigstore.parseBundle(input) decodes and structurally validates a bundle (object, JSON string, or Buffer) fail-closed. pki.sigstore.pae(payloadType, payloadBytes) builds the DSSE Pre-Authentication Encoding a signature covers. DSSE / Sigstore bundle v0.3 / RFC 9162 / SLSA provenance v1.
22
+ - The OID registry gains the Fulcio (Sigstore) certificate-extension arc 1.3.6.1.4.1.57264.1.* (the OIDC issuer, build-signer and source-repository identity claims), honoring the raw-string-vs-DER-UTF8String encoding split by member. The error taxonomy gains SigstoreError (sigstore/*): a malformed or oversize bundle (sigstore/bad-bundle), an unknown media type (sigstore/bad-bundle-version), an unsupported content arm (sigstore/unsupported-content), a DSSE signature that does not verify under the Fulcio leaf key (sigstore/dsse-verify-failed), an inclusion proof that does not reconstruct the tree root (sigstore/inclusion-proof-mismatch) or is malformed (sigstore/bad-inclusion-proof), a tree root not attested by the Rekor key (sigstore/unsigned-root), a log time not attested by the Rekor SET that signs it (sigstore/unattested-time, so an attacker cannot backdate the ephemeral Fulcio certificate into validity), a Fulcio chain that does not terminate at a caller-supplied trust anchor (sigstore/chain-incomplete) or fails validation as of the log time (sigstore/chain-invalid), an undecodable certificate identity (sigstore/bad-certificate), a malformed transparency-log entry (sigstore/bad-tlog-entry), a log entry that does not bind this signature (sigstore/entry-mismatch), an identity that fails the caller policy (sigstore/identity-mismatch), a payload that is not the expected in-toto statement (sigstore/bad-statement), and a predicateType that does not match a caller-pinned one (sigstore/predicate-mismatch). Fulcio CA anchors and Rekor keys honor their trusted-root validity windows so a rotated-out key or CA is not used, and every anchor sharing a subject DN is tried.
23
+
24
+ ## v0.2.2 — 2026-07-11
8
25
 
9
26
  Hybrid Public Key Encryption (RFC 9180) joins the toolkit as pki.hpke.
10
27
 
package/README.md CHANGED
@@ -226,6 +226,8 @@ is callable today; nothing below is a stub.
226
226
  | `pki.trust` | Mozilla / CCADB trust-store ingestion — `parseCertdata` reads the NSS `certdata.txt` object stream and `parseCcadbCsv` the CCADB CSV export into one identical constraint-carrying anchor shape: the per-purpose trust bits (only `CKT_NSS_TRUSTED_DELEGATOR` grants) and the per-purpose distrust-after dates the bare root list omits. Certificate and trust objects pair by byte-exact issuer + serial (never adjacency) and are cross-checked against the parsed DER, so metadata can never attach to the wrong root; `anchor()` hands an entry to `pki.path.validate({ trustAnchor, checkPurpose })`. Offline, fail-closed, bounded — `parseCertdata`, `parseCcadbCsv`, `anchor` |
227
227
  | `pki.shbs` | Stateful hash-based signature **verification** — HSS/LMS (RFC 8554), carried in X.509 by RFC 9802 and CMS by RFC 9708, profiled by NIST SP 800-208 (CNSA 2.0 firmware signing). `verify` checks an HSS signature (every level must pass) and `verifyLms` a single-tree LMS, over the raw public-key / signature blobs the parsers already surface. Pure public-input SHA-256 / SHAKE256 hashing, a data-driven typecode registry, bounds-before-slice reads; a malformed blob throws a typed `ShbsError`, a well-formed-but-wrong signature returns `false`. **Verify only by design** — stateful signing needs atomic one-time-key state that belongs in an HSM — `verify`, `verifyLms` |
228
228
  | `pki.hpke` | Hybrid Public Key Encryption (RFC 9180) — the encrypt-to-a-public-key primitive behind TLS ECH, MLS, and OHTTP. `setupS`/`setupR` establish a sender/recipient context (KEM encapsulation + HKDF key schedule); the context's `seal`/`open` AEAD-encrypt with a sequence-counter nonce and `export` derives further secrets; `seal`/`open` are single-shot wrappers. DHKEM (P-256, P-521, X25519, X448) × HKDF-SHA256/SHA512 × AES-GCM/ChaCha20Poly1305/export-only × all four modes, proven against the RFC 9180 Appendix A vectors. DHKEM(P-384) and HKDF-SHA384 are RFC-registered but Appendix A ships no vector for them, so they fail closed until an authoritative KAT exists. Pure composition over `node:crypto`; ML-KEM / X-Wing are a registry data-row extension pending stable drafts — `suites`, `setupS`, `setupR`, `seal`, `open` |
229
+ | `pki.sigstore` | Offline verifier for a Sigstore bundle — the exact artifact `npm publish --provenance` produces and the registry serves. `verifyBundle` composes five fail-closed legs against caller-pinned trust (Fulcio CA roots + 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. Zero runtime deps — reuses the X.509 parser, RFC 5280 path validator, and Merkle verifier; the net-new codecs are the DSSE PAE byte-builder and a fail-closed JSON reader. `pae`, `parseBundle`, `verifyBundle` |
230
+ | `pki.inspect` | Human-readable certificate inspection — the pure-JS equivalent of `openssl x509 -text`. `certificate(pem | der | parsed)` renders a familiar OpenSSL-style report: version, serial, signature algorithm, issuer/subject distinguished names, validity, public-key details (curve or modulus size + the raw point/modulus), every decoded extension with its critical flag, and the signature. Built over the strict parser and the two-way OID registry, so it names extension and algorithm OIDs an OpenSSL build shows only as raw bytes. No OpenSSL dependency; the format is stable and OpenSSL-familiar rather than pinned to one OpenSSL version; a malformed extension falls back to a hex dump rather than throwing — `certificate` |
229
231
  | `pki.C` / `pki.constants` | Version-stable constants — functional scale helpers (`C.TIME.*`, `C.BYTES.*`), codec `LIMITS`, `version` |
230
232
  | `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`, each carrying a stable `code` in `domain/reason` form |
231
233
  | `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>` |
@@ -261,34 +263,42 @@ reintroduce the bug class it prevents. Adding a format is a schema declaration
261
263
  plus a documentation comment block, not new parse logic.
262
264
 
263
265
  ```
264
- ┌─ Detect + route ────────────────────────────────────────────────────────┐
265
- pki.schema.parse — inspect the DER root, route to the matching sibling
266
- └─────────────────────────────────────────────────────────────────────────┘
266
+ ┌─ Detect + route ─────────────────────────────────────────────────────────┐
267
+ pki.schema.parse — inspect the DER root, route to the matching sibling
268
+ └──────────────────────────────────────────────────────────────────────────┘
267
269
 
268
- ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ Format
269
- │ x509 │ │ crl │ │ csr │ │ pkcs8 │ parsers
270
- └────────┘ └────────┘ └────────┘ └────────┘ (siblings)
271
270
  ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
272
- cms │ │ ocsp │ │ tsp │ │attrcert
271
+ x509 │ │ crl │ │ csr │ │ pkcs8
273
272
  └────────┘ └────────┘ └────────┘ └────────┘
274
- │ every sibling composes
275
- ┌─ Shared structure ──────────────────────────────────────────────────────┐
276
- │ pki.schema.engine walk + combinators (positional reads, tag order, │
277
- │ SET-OF uniqueness, arity, typed errors) · the PKIX sub-schemas — │
278
- AlgorithmIdentifier · Name · Extension · a bounded version reader —
279
- │ and the one coerce → decode → walk parse-entry (PEM cap + DER wrap). │
280
- └─────────────────────────────────────────────────────────────────────────┘
273
+ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
274
+ │ cms │ │ ocsp │ │ tsp │ │ crmf │
275
+ └────────┘ └────────┘ └────────┘ └────────┘
276
+ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
277
+ pkcs12 │ cmp │ smime │attrcert│
278
+ └────────┘ └────────┘ └────────┘ └────────┘
279
+ │ routed DER format parsers (siblings)
280
+ ┌─ Protocols · trust · supply chain (reached by explicit call) ───────────┐
281
+ │ pki.path RFC 5280 · pki.trust anchors · pki.ct SCTs · pki.hpke RFC 9180 │
282
+ │ pki.shbs HSS/LMS · pki.merkle RFC 9162 · pki.sigstore npm provenance │
283
+ │ pki.jose · pki.acme · pki.est — compose the layers below directly. │
284
+ └──────────────────────────────────────────────────────────────────────────┘
285
+ │ every module composes ↓
286
+ ┌─ Shared structure ───────────────────────────────────────────────────────┐
287
+ │ pki.schema.engine — walk + combinators (positional reads, tag order, │
288
+ │ SET-OF uniqueness, typed errors) · the PKIX sub-schemas · and the │
289
+ │ guard family (guard-*) — one fail-closed choke point per CVE class. │
290
+ └──────────────────────────────────────────────────────────────────────────┘
281
291
  │ built on ↓
282
- ┌─ Foundation ────────────────────────────────────────────────────────────┐
283
- pki.asn1 — strict, bounded DER codec · pki.oidtwo-way, PQC-seeded
284
- OID registry · pki.errors — the PkiError domain/reason taxonomy ·
285
- pki.C — version-stable LIMITS + scale constants. │
286
- └─────────────────────────────────────────────────────────────────────────┘
287
-
288
- ┌─ Crypto ────────────────────────────────────────────────────────────────┐
289
- pki.webcrypto ──▶ node:crypto — a W3C SubtleCrypto engine: the
290
- classical set + post-quantum ML-DSA / SLH-DSA + ML-KEM key generation.
291
- └─────────────────────────────────────────────────────────────────────────┘
292
+ ┌─ Foundation ─────────────────────────────────────────────────────────────┐
293
+ pki.asn1 — strict bounded DER codec · pki.cbordeterministic CBOR ·
294
+ pki.oid — two-way PQC-seeded registry · pki.errors — PkiError taxonomy
295
+ · pki.C — version-stable LIMITS + scale constants. │
296
+ └──────────────────────────────────────────────────────────────────────────┘
297
+
298
+ ┌─ Crypto ─────────────────────────────────────────────────────────────────┐
299
+ pki.webcrypto ─▶ node:crypto — a W3C SubtleCrypto engine: the
300
+ classical set + post-quantum ML-DSA / SLH-DSA + ML-KEM key generation.
301
+ └──────────────────────────────────────────────────────────────────────────┘
292
302
  ```
293
303
 
294
304
  **Foundation.** The strict, bounded DER codec (`pki.asn1`), the two-way OID
@@ -302,12 +312,24 @@ bounded version reader, and the single coerce → decode → walk parse-entry th
302
312
  every format's `parse` is bound to. Because input coercion, the PEM size cap, and
303
313
  the DER-decode wrapping live here once, a format cannot diverge on a guard.
304
314
 
305
- **Format parsers.** `x509`, `crl`, `csr`, `pkcs8`, `cms`, `ocsp`, `tsp`, and `attrcert` are siblings:
306
- each is a schema declaration composed from the shared pieces, emitting its own
307
- typed `domain/reason` error codes. `pki.schema.parse` inspects a decoded root and
315
+ **Format parsers.** `x509`, `crl`, `csr`, `pkcs8`, `cms`, `ocsp`, `tsp`, `attrcert`,
316
+ `crmf`, `pkcs12`, `cmp`, `smime`, and `csrattrs` are siblings: each is a schema
317
+ declaration composed from the shared pieces, emitting its own typed
318
+ `domain/reason` error codes. `pki.schema.parse` inspects a decoded root and
308
319
  detect-and-routes to the matching sibling; the detectors are mutually exclusive by
309
320
  construction, so routing is unambiguous regardless of registration order.
310
321
 
322
+ **Protocols, trust, and supply chain.** Above the format parsers sit the domain
323
+ modules reached by explicit call rather than DER routing: `pki.path` (RFC 5280
324
+ path validation), `pki.trust` (trust anchors), `pki.ct` (Certificate Transparency
325
+ SCTs), `pki.hpke` (RFC 9180), `pki.shbs` (HSS/LMS stateful hash signatures),
326
+ `pki.merkle` (RFC 9162 transparency proofs), `pki.sigstore` (offline npm-provenance
327
+ verification), and the `jose` / `acme` / `est` enrollment surfaces. Each composes
328
+ the shared structure, foundation, and crypto layers directly. Alongside the schema
329
+ engine, the fail-closed **guard family** (`guard-*`) centralizes each CVE-class
330
+ defense — detached-buffer re-view, resource caps, constant-time compares,
331
+ canonical-DN comparison — as one choke point a format cannot re-inline.
332
+
311
333
  **Crypto.** `pki.webcrypto` is a W3C `SubtleCrypto` engine over `node:crypto`,
312
334
  carrying the classical suite plus post-quantum ML-DSA / SLH-DSA signatures and
313
335
  ML-KEM key generation. Sign/verify resolves algorithms through the same OID
package/index.js CHANGED
@@ -39,10 +39,12 @@ var ct = require("./lib/ct");
39
39
  var merkle = require("./lib/merkle");
40
40
  var shbs = require("./lib/shbs");
41
41
  var hpke = require("./lib/hpke");
42
+ var sigstore = require("./lib/sigstore");
42
43
  var est = require("./lib/est");
43
44
  var jose = require("./lib/jose");
44
45
  var acme = require("./lib/acme");
45
46
  var trust = require("./lib/trust");
47
+ var inspect = require("./lib/inspect");
46
48
 
47
49
  module.exports = {
48
50
  version: constants.version,
@@ -84,6 +86,10 @@ module.exports = {
84
86
  // schedule + AEAD context construction behind TLS ECH / MLS / OHTTP. Pure
85
87
  // composition over node:crypto; the classical DHKEM suites, all four modes.
86
88
  hpke: hpke,
89
+ // `sigstore` verifies a Sigstore bundle (the npm --provenance artifact): a
90
+ // keyless Fulcio signature over a DSSE-wrapped in-toto SLSA attestation with a
91
+ // Rekor inclusion proof -- offline, zero-dep, against caller-pinned trust.
92
+ sigstore: sigstore,
87
93
  // `est` is RFC 7030 / 8951 / 9908 Enrollment over Secure Transport -- the
88
94
  // transport-agnostic client codecs (base64 transfer, multipart splitter),
89
95
  // certs-only + serverkeygen validators over CMS, the enroll-attribute builders,
@@ -105,6 +111,11 @@ module.exports = {
105
111
  // root list omits); pki.trust.anchor hands one to pki.path.validate. Offline:
106
112
  // the operator supplies the text; no fetch.
107
113
  trust: trust,
114
+ // `inspect` is human-readable inspection -- pki.inspect.certificate(pem|der|parsed)
115
+ // renders an OpenSSL-familiar `x509 -text`-style report from the strict parser +
116
+ // OID registry, naming extension/algorithm OIDs OpenSSL shows only as raw bytes.
117
+ // Pure, no OpenSSL dependency; best-effort (a bad extension falls back to hex).
118
+ inspect: inspect,
108
119
  // A ready W3C Crypto instance (globalThis.crypto shape) with the classes for
109
120
  // constructing more attached under the same namespace (pki.webcrypto.CryptoKey,
110
121
  // .SubtleCrypto, .Crypto, .WebCryptoError). PQC-first, classical-capable, zero-dep.
package/lib/constants.js CHANGED
@@ -257,6 +257,101 @@ var LIMITS = {
257
257
  TRUST_MAX_CSV_FIELD_BYTES: BYTES.kib(64),
258
258
  };
259
259
 
260
+ // NAMES -- display-naming conventions shared by the human-readable renderers
261
+ // (pki.inspect today; a future lint / CLI report tomorrow). Centralized so every
262
+ // consumer speaks ONE convention -- the OpenSSL-familiar labels an operator expects
263
+ // -- rather than each renderer copying a table that silently drifts. These are
264
+ // DISPLAY strings, not wire identifiers (those resolve through pki.oid): a value here
265
+ // never affects parse or verify, only how a decoded field is titled in a report.
266
+ var NAMES = {
267
+ // ==== Distinguished-name attribute short names (RFC 4514 sec. 3 + RFC 4519 / CABF) ==
268
+ // The single source of truth: this table was previously copied in both
269
+ // lib/inspect.js and lib/schema-pkix.js and had already drifted (STREET vs street,
270
+ // SERIALNUMBER vs serialNumber). An entry whose type the OID registry does not yet
271
+ // resolve is simply never hit -- forward entries (jurisdiction*, organizationIdentifier)
272
+ // are harmless until their arc is registered.
273
+ DN_SHORT: {
274
+ commonName: "CN", surname: "SN", serialNumber: "SERIALNUMBER", countryName: "C",
275
+ localityName: "L", stateOrProvinceName: "ST", streetAddress: "STREET",
276
+ organizationName: "O", organizationalUnitName: "OU", title: "title", givenName: "GN",
277
+ initials: "initials", generationQualifier: "generationQualifier", distinguishedNameQualifier: "dnQualifier",
278
+ pseudonym: "pseudonym", domainComponent: "DC", userId: "UID", emailAddress: "emailAddress",
279
+ businessCategory: "businessCategory", postalCode: "postalCode", name: "name",
280
+ jurisdictionCountryName: "jurisdictionC", jurisdictionStateOrProvinceName: "jurisdictionST",
281
+ jurisdictionLocalityName: "jurisdictionL", organizationIdentifier: "organizationIdentifier",
282
+ },
283
+ // ==== X.509v3 extension section titles (OpenSSL-familiar); an extension absent
284
+ // here is titled from the OID registry name / dotted OID ====
285
+ EXTENSION: {
286
+ basicConstraints: "X509v3 Basic Constraints", keyUsage: "X509v3 Key Usage",
287
+ extKeyUsage: "X509v3 Extended Key Usage", subjectAltName: "X509v3 Subject Alternative Name",
288
+ issuerAltName: "X509v3 Issuer Alternative Name", subjectKeyIdentifier: "X509v3 Subject Key Identifier",
289
+ authorityKeyIdentifier: "X509v3 Authority Key Identifier", certificatePolicies: "X509v3 Certificate Policies",
290
+ policyMappings: "X509v3 Policy Mappings", policyConstraints: "X509v3 Policy Constraints",
291
+ inhibitAnyPolicy: "X509v3 Inhibit Any Policy", nameConstraints: "X509v3 Name Constraints",
292
+ cRLDistributionPoints: "X509v3 CRL Distribution Points", freshestCRL: "X509v3 Freshest CRL",
293
+ subjectInfoAccess: "Subject Information Access", authorityInfoAccess: "Authority Information Access",
294
+ cRLNumber: "X509v3 CRL Number", deltaCRLIndicator: "X509v3 Delta CRL Indicator",
295
+ issuingDistributionPoint: "X509v3 Issuing Distribution Point", cRLReason: "X509v3 CRL Reason Code",
296
+ invalidityDate: "Invalidity Date", certificateIssuer: "X509v3 Certificate Issuer",
297
+ signedCertificateTimestampList: "CT Precertificate SCTs", precertificatePoison: "CT Precertificate Poison",
298
+ },
299
+ // ==== KeyUsage bits (RFC 5280 sec. 4.2.1.3, bit order) ====
300
+ KEY_USAGE: {
301
+ digitalSignature: "Digital Signature", nonRepudiation: "Non Repudiation", keyEncipherment: "Key Encipherment",
302
+ dataEncipherment: "Data Encipherment", keyAgreement: "Key Agreement", keyCertSign: "Certificate Sign",
303
+ cRLSign: "CRL Sign", encipherOnly: "Encipher Only", decipherOnly: "Decipher Only",
304
+ },
305
+ // ==== ExtendedKeyUsage purposes (RFC 5280 sec. 4.2.1.12 + common); an unlisted
306
+ // purpose shows its registry name / OID ====
307
+ EXT_KEY_USAGE: {
308
+ anyExtendedKeyUsage: "Any Extended Key Usage", serverAuth: "TLS Web Server Authentication",
309
+ clientAuth: "TLS Web Client Authentication", codeSigning: "Code Signing",
310
+ emailProtection: "E-mail Protection", timeStamping: "Time Stamping", ocspSigning: "OCSP Signing",
311
+ },
312
+ // ==== GeneralName CHOICE labels, by context tag number (RFC 5280 sec. 4.2.1.6) ====
313
+ GENERAL_NAME: { 0: "othername", 1: "email", 2: "DNS", 3: "X400Name", 4: "DirName", 5: "EdiPartyName", 6: "URI", 7: "IP Address", 8: "Registered ID" },
314
+ // ==== Named-curve -> NIST alias, for the EC key line ====
315
+ NIST_CURVE: { prime256v1: "P-256", secp384r1: "P-384", secp521r1: "P-521" },
316
+ // ==== ReasonFlags BIT STRING bit -> name (RFC 5280 sec. 4.2.1.13, a CRL
317
+ // distribution point's revocation scope; bit 0 is "unused"). Distinct from the
318
+ // CRLReason ENUMERATED below -- a BIT STRING of scopes, not a single code. ====
319
+ REASON_FLAGS: {
320
+ 1: "Key Compromise", 2: "CA Compromise", 3: "Affiliation Changed", 4: "Superseded",
321
+ 5: "Cessation Of Operation", 6: "Certificate Hold", 7: "Privilege Withdrawn", 8: "AA Compromise",
322
+ },
323
+ // ==== CRLReason ENUMERATED value names (RFC 5280 sec. 5.3.1; value 7 is unused) ====
324
+ CRL_REASON: {
325
+ "0": "unspecified", "1": "keyCompromise", "2": "cACompromise", "3": "affiliationChanged",
326
+ "4": "superseded", "5": "cessationOfOperation", "6": "certificateHold",
327
+ "8": "removeFromCRL", "9": "privilegeWithdrawn", "10": "aACompromise",
328
+ },
329
+ // ==== OCSPResponseStatus value names (RFC 6960 sec. 4.2.1; value 4 is unassigned) ====
330
+ OCSP_STATUS: { "0": "successful", "1": "malformedRequest", "2": "internalError", "3": "tryLater", "5": "sigRequired", "6": "unauthorized" },
331
+ // ==== Attribute-cert objectDigestInfo digested-object types (RFC 5755 sec. 4.4.2) ====
332
+ OBJECT_DIGEST_TYPE: { "0": "publicKey", "1": "publicKeyCert", "2": "otherObjectTypes" },
333
+ // ==== Public-key / signature algorithm display names, classical + PQC. Forward-
334
+ // looking: the toolkit is PQC-first, so the FIPS 203/204/205 names render as-is;
335
+ // a consumer that finds no entry shows the OID registry name / dotted OID ====
336
+ ALGORITHM: {
337
+ rsaEncryption: "rsaEncryption", rsassaPss: "rsassaPss", rsaesOaep: "rsaesOaep", ecPublicKey: "id-ecPublicKey",
338
+ ed25519: "ED25519", ed448: "ED448", x25519: "X25519", x448: "X448",
339
+ "ML-DSA-44": "ML-DSA-44", "ML-DSA-65": "ML-DSA-65", "ML-DSA-87": "ML-DSA-87",
340
+ "ML-KEM-512": "ML-KEM-512", "ML-KEM-768": "ML-KEM-768", "ML-KEM-1024": "ML-KEM-1024",
341
+ "SLH-DSA-SHA2-128s": "SLH-DSA-SHA2-128s", "SLH-DSA-SHA2-128f": "SLH-DSA-SHA2-128f",
342
+ "SLH-DSA-SHA2-192s": "SLH-DSA-SHA2-192s", "SLH-DSA-SHA2-256s": "SLH-DSA-SHA2-256s",
343
+ },
344
+ };
345
+
346
+ // NAMES is BOTH a display table (pki.inspect) and the strict parsers' legal-value
347
+ // allow-list: the CRL reasonCode, OCSP response-status, and attribute-cert object-
348
+ // digest decoders check membership against CRL_REASON / OCSP_STATUS / OBJECT_DIGEST_TYPE
349
+ // to fail closed on an RFC-reserved / undefined value. Deep-freeze it so a caller can
350
+ // NEVER widen what those decoders accept by mutating the "display-only" table (e.g.
351
+ // adding CRLReason 7, RFC-reserved, which would otherwise make the decoders fail open).
352
+ Object.keys(NAMES).forEach(function (k) { Object.freeze(NAMES[k]); });
353
+ Object.freeze(NAMES);
354
+
260
355
  // Single-sourced from the package manifest so the reported version can
261
356
  // never drift from the published package (package.json is always present
262
357
  // in the installed tarball). A hand-maintained literal here silently
@@ -267,5 +362,6 @@ module.exports = {
267
362
  TIME: TIME,
268
363
  BYTES: BYTES,
269
364
  LIMITS: LIMITS,
365
+ NAMES: NAMES,
270
366
  version: VERSION,
271
367
  };
@@ -283,6 +283,21 @@ var AcmeError = defineClass("AcmeError", { withCause: true });
283
283
  var TrustError = defineClass("TrustError", { withCause: true });
284
284
  var ShbsError = defineClass("ShbsError", { withCause: true });
285
285
  var HpkeError = defineClass("HpkeError", { withCause: true });
286
+ // SigstoreError -- a Sigstore bundle (npm --provenance artifact) that is not a
287
+ // well-formed / verifiable keyless signature: a malformed or oversize JSON
288
+ // bundle, an unknown media type or content arm, a DSSE envelope missing a
289
+ // required field or whose signature does not verify under the Fulcio leaf key, a
290
+ // Rekor inclusion proof that does not reconstruct the signed tree root, an
291
+ // unsigned/attacker-supplied root, an entry that does not bind this signature, a
292
+ // Fulcio identity that fails the caller policy, or a payload that is not the
293
+ // expected in-toto Statement. Carries the underlying leaf fault (an x509/*,
294
+ // merkle/*, or asn1/* error) as `.cause`.
295
+ var SigstoreError = defineClass("SigstoreError", { withCause: true });
296
+ // InspectError -- a bad input to the human-readable inspector (pki.inspect): a
297
+ // value that is neither a parsed structure, DER Buffer, nor PEM string, or a PEM
298
+ // whose label does not decode. Rendering itself is best-effort and never throws on
299
+ // a malformed field -- an undecodable extension falls back to a hex dump.
300
+ var InspectError = defineClass("InspectError", { withCause: true });
286
301
 
287
302
  module.exports = {
288
303
  PkiError: PkiError,
@@ -308,6 +323,8 @@ module.exports = {
308
323
  CtError: CtError,
309
324
  ShbsError: ShbsError,
310
325
  HpkeError: HpkeError,
326
+ SigstoreError: SigstoreError,
327
+ InspectError: InspectError,
311
328
  MerkleError: MerkleError,
312
329
  SmimeError: SmimeError,
313
330
  CsrattrsError: CsrattrsError,
package/lib/guard-all.js CHANGED
@@ -23,6 +23,8 @@
23
23
  // to Number (silent-narrowing defence)
24
24
  // guard.name.dnEqual / .rdnEqual / .assertNoControlBytes / .assertPrintableIa5
25
25
  // -- canonical DN identity + name-string integrity
26
+ // guard.name.escapeControlBytes / .escapeDnValue
27
+ // -- render-side name safety: control-byte + RFC 4514 escaping
26
28
  // (CVE-2009-2408 truncation / identity defence)
27
29
  // guard.encoding.base64url / .base64 / .hex
28
30
  // -- strict textual-encoding decode, canonical
package/lib/guard-name.js CHANGED
@@ -108,4 +108,53 @@ function dnEqual(rdnsA, rdnsB, E, code, label) {
108
108
  return true;
109
109
  }
110
110
 
111
- module.exports = { assertNoControlBytes: assertNoControlBytes, assertPrintableIa5: assertPrintableIa5, dnEqual: dnEqual, rdnEqual: rdnEqual };
111
+ // escapeControlBytes(str) -> str. The render-side sibling of assertNoControlBytes:
112
+ // where a name string must still be DISPLAYED best-effort (a human-readable report,
113
+ // a log line) rather than rejected, neutralize every C0 control byte and DEL by
114
+ // rendering it as \xHH. A bare CR / LF / NUL in a decoded dNSName or DN value would
115
+ // otherwise forge or overwrite report lines in a terminal or log (CWE-117 output-log
116
+ // injection / CWE-116 improper output encoding). Non-control bytes pass through.
117
+ // Written as a charCodeAt scan, not a /[\x00-\x1f]/ regex: eslint's no-control-regex
118
+ // (correctly) refuses control characters in a regex literal, and the control-byte
119
+ // range test is the rename-proof shape the detector keys on regardless.
120
+ // @enforced-by guard-shape-reinlined
121
+ // @guard-shape < 0x20 \|\| \w+ === 0x7f
122
+ function escapeControlBytes(str) {
123
+ var s = String(str), out = "";
124
+ for (var i = 0; i < s.length; i++) {
125
+ var c = s.charCodeAt(i);
126
+ out += (c < 0x20 || c === 0x7f) ? "\\x" + (c < 16 ? "0" : "") + c.toString(16).toUpperCase() : s.charAt(i);
127
+ }
128
+ return out;
129
+ }
130
+
131
+ // escapeDnValue(v) -> str. RFC 4514 sec. 2.4 string-representation escaping of a DN
132
+ // attribute value (the input is the already-unwrapped semantic string): backslash-
133
+ // escape the always-specials , + " \ < > ; and a NUL (as \00), then the positional
134
+ // rules -- a trailing space as '\ ' and a leading '#' or space as '\#' / '\ '. Without
135
+ // this a value like `foo, CN=admin` renders as two RDNs, and a literal `#05` collides
136
+ // with the hexstring form, so the report misstates a subject/issuer name (CWE-116).
137
+ // The one place a DN attribute value is made display-safe; pki.schema.pkix's DN
138
+ // rendering composes it, and pki.inspect reuses that parser output (name.dn).
139
+ // @enforced-by behavioral -- the RFC 4514 separator class carries a quote inside a
140
+ // regex literal, which the codebase-patterns literal-stripper mis-tokenizes, so no
141
+ // rename-proof shape is detectable; the guard-name RED vectors + the schema-pkix DN
142
+ // round-trip vectors (a comma / plus / leading '#' renders backslash-escaped) are the guard.
143
+ function escapeDnValue(v) {
144
+ var s = String(v).replace(/([,+"\\<>;])/g, "\\$1"), out = "";
145
+ // RFC 4514 sec. 2.4: a NUL / control octet -> '\' + two hex digits, so an embedded
146
+ // CR / LF / NUL in a decoded DN value can never forge a report line when displayed.
147
+ for (var i = 0; i < s.length; i++) {
148
+ var c = s.charCodeAt(i);
149
+ out += (c < 0x20 || c === 0x7f) ? "\\" + (c < 16 ? "0" : "") + c.toString(16).toUpperCase() : s.charAt(i);
150
+ }
151
+ if (out.length && out.charAt(out.length - 1) === " ") out = out.slice(0, -1) + "\\ ";
152
+ if (out.charAt(0) === "#" || out.charAt(0) === " ") out = "\\" + out;
153
+ return out;
154
+ }
155
+
156
+ module.exports = {
157
+ assertNoControlBytes: assertNoControlBytes, assertPrintableIa5: assertPrintableIa5,
158
+ dnEqual: dnEqual, rdnEqual: rdnEqual,
159
+ escapeControlBytes: escapeControlBytes, escapeDnValue: escapeDnValue,
160
+ };