@blamejs/pki 0.3.7 → 0.3.9

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,25 @@ 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.3.9 — 2026-07-23
8
+
9
+ pki.crl builds, signs, and verifies X.509 certificate revocation lists (RFC 5280 sec. 5) over any registry algorithm.
10
+
11
+ ### Added
12
+
13
+ - pki.crl.sign(spec, issuer, opts) builds and signs an X.509 CRL (RFC 5280 sec. 5): thisUpdate/nextUpdate, an optional crlNumber, a revoked list (each entry a serialNumber and revocationDate with an optional reason or invalidityDate), and an extensions object (authorityKeyIdentifier, issuingDistributionPoint, deltaCRLIndicator, freshestCRL, authorityInfoAccess) or an array of pre-encoded Extension DER. The issuer is a CA certificate + key, or an explicit name + public key + key. The signature algorithm is resolved from the issuer key, so RSA (PKCS#1 v1.5 / PSS via opts.pss), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. The version is derived from the extension set (v2 when any CRL or entry extension is present, else v1), an empty revocation list omits the field, the reason code is an ENUMERATED, an invalidity date is always a GeneralizedTime, and per-extension criticality is fixed by the profile. Returns DER or a PEM X509 CRL; the produced signature is verified under the issuer key before return, and every emitted CRL round-trips through pki.schema.crl.parse and is accepted by OpenSSL across the classical and post-quantum arms. RFC 5280 sec. 5.
14
+ - pki.crl.verify(crl, issuer) verifies a CRL's signature over its exact tbsCertList bytes against the issuer public key (a { cert }, a { publicKey } SPKI DER, or a raw SPKI Buffer), composing the one path-validation signature engine pki.path.crlChecker uses -- the RFC 9814 algorithm-confusion and Edwards low-order-point gates included -- and failing closed to false. It checks the signature only; issuer authorization, currency, and distribution-point scope remain pki.path.crlChecker.
15
+ - pki.crl.isRevoked(crl, serialNumber) returns the revoked-certificate entry a CRL lists for a serial number, or null when the serial is not listed.
16
+
17
+ ## v0.3.8 — 2026-07-18
18
+
19
+ Human-readable inspection extends to CRLs, CSRs, and CMS messages, with a pki.schema.detectFormat companion.
20
+
21
+ ### Added
22
+
23
+ - pki.inspect.crl / .csr / .cms render a certificate revocation list, a PKCS#10 certification request, and a CMS message as OpenSSL-familiar text reports (openssl crl -text / req -text / cms -cmsout -print), and pki.inspect.any detects the format of a DER/PEM input and routes it to the matching report. Each composes the certificate inspector's shipped field renderers, resolves every extension/attribute/algorithm/content-type OID through the registry (unknown -> dotted, undecodable value -> raw octets), and is best-effort: a malformed part hex-dumps rather than failing the report, and only entry-point coercion throws (inspect/bad-crl / inspect/bad-csr / inspect/bad-cms / inspect/unsupported-format). Cross-checked field-for-field against OpenSSL. RFC 5280 / RFC 2986 / RFC 5652.
24
+ - pki.schema.detectFormat(input) returns the registered PKI format name a DER Buffer or PEM string encodes -- one of pki.schema.all() -- without parsing it, or null when it matches no registered format. The detection half of pki.schema.parse, over the same authoritative format ordering.
25
+
7
26
  ## v0.3.7 — 2026-07-17
8
27
 
9
28
  Certification path building arrives as pki.path.build, and pki.lint gains seven RFC 5280 extension-criticality and CA-scope lints.
package/README.md CHANGED
@@ -227,6 +227,7 @@ is callable today; nothing below is a stub.
227
227
  | `pki.attrcert` | RFC 5755 attribute-certificate issuance — `sign(spec, issuer, opts)` builds and signs an `AttributeCertificate` as an Attribute Authority: a `spec` of `holder` (exactly one of an entity name, a `baseCertificateID` public-key-certificate reference, a `fromCertificate` binding derived from a certificate, or an object digest), the validity window (GeneralizedTime), an optional serial (positive, ≤ 20 octets; randomly generated when omitted), the `attributes` (the privilege syntaxes — role, clearance, group, chargingIdentity, accessIdentity, authenticationInfo — or pre-encoded Attribute DER), and optional `extensions` (auditIdentity, targetInformation, noRevAvail, aaControls, acProxying, authorityKeyIdentifier, or pre-encoded Extension DER, each with its RFC 5755 criticality). An attribute certificate is never self-signed — the `issuer` is the signing AA, supplied as `{ cert, key }` or `{ name, publicKey, key }`. The signature algorithm is resolved from the AA key, so RSA (PKCS#1 v1.5 / PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch, and the signature is verified under the AA public key before the certificate is returned. Returns DER, or a PEM `ATTRIBUTE CERTIFICATE` with `opts.pem`; malformed input throws a typed `AttrCertError`. Parsing stays at `pki.schema.attrcert.parse` — `sign` |
228
228
  | `pki.crmf` | RFC 4211 certificate-request-message issuance — `build(spec, key, opts)` assembles a `CertReqMessages`: a `spec` of `certReqId` (default 0; the RFC 9483 `-1` sentinel allowed), a `certTemplate` of the requested certificate fields (`subject`, `publicKey` — the SPKI DER of the key being certified — `validity`, requested `extensions`, an optional `version` 2), optional `controls` and `regInfo` (regToken / authenticator / utf8Pairs / oldCertID / protocolEncrKey, or pre-encoded `AttributeTypeAndValue` DER), and an optional `pop` selector. `key` (or `{ key }`) is the requester's private key — the message carries a `POPOSigningKey` proof of possession signed with the private half of `certTemplate.publicKey` (verified before the message is returned), exactly as a PKCS#10 CSR proves possession; a complete template signs the `CertRequest`, an incomplete one signs a `POPOSigningKeyInput`. The signature algorithm is resolved from the requested public key, so RSA (PKCS#1 v1.5 / PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. `key` is optional for a `raVerified` proof. Pass an array of specs for a batch; the CA-assigned template fields are never emitted. Returns DER, or a PEM block with `opts.pem`; malformed input throws a typed `CrmfError`. Parsing stays at `pki.schema.crmf.parse` — `build` |
229
229
  | `pki.cmp` | RFC 9810 Certificate Management Protocol message building — `build(message, opts)` assembles a protected `PKIMessage`. `message.header` carries the `sender` / `recipient` GeneralNames (including the anonymous NULL-DN) plus optional transaction metadata (`transactionID`, `senderNonce` / `recipNonce`, `messageTime` as a GeneralizedTime, `senderKID` / `recipKID`, `freeText`, `generalInfo`); `message.body` is a single-key object naming the arm. Request-side: `ir` / `cr` / `kur` (a `CertReqMessages` spec delegated to `pki.crmf.build`), `p10cr` (a PKCS#10 `CertificationRequest`), `certConf`, `pollReq`, `genm`, `rr`. CA/responder-side: `ip` / `cp` / `kup` / `ccp` (a `CertRepMessage` — `caPubs` and `response` entries carrying a `PKIStatusInfo` and, under a granting status, a `certifiedKeyPair`), `rp` (revocation response), `genp`, `error`, `pollRep`, `krp` (key-recovery response), `pkiconf`. Protection is exactly one of `opts.{ key, cert }` — a signature over the message under the sender key, the algorithm resolved from the signer certificate so RSA (PKCS#1 v1.5 / PSS), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch — or `opts.mac` — a PBMAC1 shared-secret HMAC (RFC 9481 / 9579, PBKDF2-derived). The protection covers the exact DER of the virtual `ProtectedPart` (the header and body) and is self-verified before the message is returned; the `protectionAlg` is derived, never caller-set, so the message the parser accepts is coherent by construction. Returns DER, or a PEM `CMP` block with `opts.pem`; malformed input throws a typed `CmpError`. Parsing stays at `pki.schema.cmp.parse` — `build` |
230
+ | `pki.crl` | RFC 5280 §5 certificate revocation list issuance — `sign(spec, issuer, opts)` builds and signs a `CertificateList`: a `spec` of `thisUpdate` / `nextUpdate`, an optional `crlNumber`, a `revoked` array (each entry a `serialNumber` + `revocationDate` with an optional `reason` or `invalidityDate`), and an optional `extensions` object (authority key identifier, issuing distribution point, delta-CRL indicator, freshest CRL, authority information access) or an array of pre-encoded Extension DER; an `issuer` of `{ cert, key }` or `{ name, publicKey, key }`. The signature algorithm is resolved from the issuer key, so RSA (PKCS#1 v1.5 / PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. The version is derived from the extension set (v2 when any CRL or entry extension is present, else v1), the outer `signatureAlgorithm` matches `tbsCertList.signature`, an empty revocation list omits the field rather than emitting an empty SEQUENCE, `reasonCode` is an ENUMERATED and `invalidityDate` is always GeneralizedTime, per-extension criticality is fixed by the RFC, and the produced signature is verified under the issuer key before return. `verify(crl, issuer)` checks a CRL signature through the one path-validation signature engine (algorithm-confusion and EdDSA low-order gates included), and `isRevoked(crl, serialNumber)` looks a serial up in the revocation list. Returns DER, or a PEM `X509 CRL` with `opts.pem`; malformed input throws a typed `CrlError`. Parsing stays at `pki.schema.crl.parse` — `sign` / `verify` / `isRevoked` |
230
231
  | `pki.cms` | RFC 5652 §5 CMS SignedData signing + signature verification — `sign(content, signers, opts)` produces a SignedData (attached or detached, one or many signers, RSA / RSASSA-PSS / ECDSA / EdDSA, the post-quantum ML-DSA-44/65/87 (RFC 9882) and SLH-DSA (all twelve FIPS 205 sets, RFC 9814), and composite ML-DSA (pairing ML-DSA with a traditional RSA / ECDSA / EdDSA — accepted only when **both** components verify — draft-ietf-lamps-cms-composite-sigs)); it builds the signed attributes (content-type, message-digest, signing-time) as canonical DER, signs the exact §5.4 preimage, and emits a DER `Buffer` or PEM. `verify(input, opts)` parses a SignedData over the strict `pki.schema.cms` codec, locates each SignerInfo's signer certificate by its issuerAndSerialNumber or subjectKeyIdentifier, and checks the signature over the exact §5.4 preimage: when signed attributes are present it confirms the message-digest attribute equals the content digest and verifies over the DER re-encoding of the SignedAttributes (the on-wire `[0]` tag replaced by a universal SET OF), otherwise directly over the content. It returns a per-signer verdict with the matched signer certificate; it does not chain that certificate to a trust anchor — that is the caller's step through `pki.path.validate`. **Content encryption** (RFC 5652/5083/5084/9629): `encrypt(content, recipients, opts)` produces an EnvelopedData, AuthEnvelopedData (AES-GCM, the authenticated default), or EncryptedData — recipients auto-dispatch off the certificate key to key-transport (RSAES-OAEP; v1.5 never emitted), key-agreement (ephemeral-static ECDH over P-256/384/521 with the X9.63 KDF, and X25519/X448 with HKDF), symmetric key-wrap, password (PBKDF2 + RFC 3211 PWRI-KEK), or the post-quantum ML-KEM KEMRecipientInfo (RFC 9629/9936) — one fresh content key wrapped for every recipient. `decrypt(input, keyMaterial, opts)` recovers the content through the matching arm and returns it with an `authenticated` flag; every secret-dependent failure collapses to one uniform `cms/decrypt-failed` verdict (Bleichenbacher / EFAIL / password-oracle freedom), and PKCS#1 v1.5 is decrypt-only under the RFC 3218 implicit-rejection countermeasure. **Compression** (RFC 3274): `compress(content, opts)` / `decompress(input, opts)` produce and consume a CompressedData (ZLIB, version 0, id-alg-zlibCompress); decompress bounds the uncompressed output at 16 MiB and stops before it is materialized, so a decompression bomb fails closed as `cms/decompress-too-large` — a size transform with no integrity/confidentiality (RFC 8551 §2.4.5). Fail-closed with typed `cms/*` errors — `sign`, `verify`, `encrypt`, `decrypt`, `compress`, `decompress` |
231
232
  | `pki.smime` | RFC 8551 S/MIME message assembly, verification, encryption, and compression over the CMS layer — `sign(content, signers, opts)` wraps a MIME entity as a signed S/MIME message in either form: `multipart/signed` (clear-signed — the content stays readable in any MUA, a detached CMS SignedData rides alongside as `application/pkcs7-signature` with a matching `micalg`) or `application/pkcs7-mime; smime-type=signed-data` (opaque — the whole entity is a base64 CMS SignedData). The signed bytes are the entity's RFC 8551 §3.1.1 canonical form (CRLF line endings); `verify(message, opts)` unwraps both forms and recomputes over the same canonicalizer, so a transport that re-wraps line endings still verifies and a tampered part fails. `encrypt(content, recipients, opts)` envelopes a MIME entity as an opaque `application/pkcs7-mime` message and `decrypt(message, keyMaterial, opts)` opens one — `smime-type=authEnveloped-data` (AES-GCM, confidentiality and integrity, the default) or `smime-type=enveloped-data` (AES-CBC, confidentiality only, so `decrypt` reports `authenticated: false`, the §3.3 no-integrity caveat); the `smime-type` is derived from the CMS body, not the header, and decryption is fail-closed and oracle-free. The crypto is entirely `pki.cms.sign` / `verify` / `encrypt` / `decrypt` — any RSA / RSASSA-PSS / ECDSA / EdDSA / ML-DSA / SLH-DSA signer and any RSA-OAEP / ECDH / X25519 / X448 / AES-KW / PBKDF2 / ML-KEM recipient carries through (algorithm-agnostic). Like `cms.verify`, `verify` returns the per-signer cryptographic verdict plus the recovered content; chaining a signer to a trust anchor is the caller's `pki.path.validate` step. `compress(content, opts)` / `decompress(message, opts)` add the opaque `application/pkcs7-mime; smime-type=compressed-data; name=smime.p7z` frame (RFC 8551 §3.6, RFC 3274) — a size transform with no integrity/confidentiality (§2.4.5), decompress bounded against a bomb; the recovered content, which may itself be signed or enveloped, is returned for the caller to re-verify. Bidirectionally interoperable with `openssl smime` / `openssl cms`. Fail-closed with typed `smime/*` errors — `sign`, `verify`, `encrypt`, `decrypt`, `compress`, `decompress` |
232
233
  | `pki.tsp` | RFC 3161 Time-Stamp Protocol — `sign(messageImprint, tsa, opts)` produces a TimeStampToken: a CMS SignedData (over `pki.cms.sign`) whose content is a `TSTInfo` carrying the timestamped message imprint, the TSA policy, a serial number, and `genTime` (with optional accuracy / nonce / ordering), plus the RFC 3161 §2.4.2 signing-certificate attribute binding the token to the TSA certificate (SHA-2 imprints, any `pki.cms.sign` TSA key). `request` / `parseRequest` build and parse the TimeStampReq a client sends (imprint, requested policy, nonce, certReq), `response` / `parseResponse` the TimeStampResp a TSA returns — a granted status wrapping a token, or a rejection with PKIStatus and failure info, the §2.4.2 status↔token coupling enforced in both directions. `verify(token, data, opts)` verifies a token fail-closed: the CMS signature over the exact signed bytes, the message imprint recomputed from the data, the TSTInfo content type, the ESSCertID(V2) binding to the TSA certificate, the §2.3 critical timeStamping-only extendedKeyUsage, the request nonce when used, and — with a trust anchor supplied — full certification-path validation of the TSA certificate at the token's `genTime`, returning `{ valid, genTime, serialNumber, tstInfo, … }` — `sign`, `request`, `parseRequest`, `response`, `parseResponse`, `verify` |
@@ -237,7 +238,7 @@ is callable today; nothing below is a stub.
237
238
  | `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` |
238
239
  | `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` |
239
240
  | `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` |
240
- | `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` |
241
+ | `pki.inspect` | Human-readable inspection — the pure-JS equivalent of `openssl x509/crl/req/cms -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. `crl` / `csr` / `cms` render the non-certificate formats the same way — a CRL like `openssl crl -text` (issuer, Last/Next Update, CRL extensions, each revoked entry with its serial, revocation date, and named reason), a CSR like `openssl req -text` (subject, key, requested extensions and attributes), and a CMS message like `openssl cms -cmsout -print` (a SignedData's content type, digest algorithms, embedded certificates, and each SignerInfo with its signer identifier, algorithms, attributes, and signature; a non-SignedData ContentInfo gets a stable summary) — and `any(input)` detects the format and routes to the right report. Built over the strict parsers and the two-way OID registry, reusing one set of field renderers, so it names extension/algorithm OIDs an OpenSSL build shows only as raw bytes and never drifts. No OpenSSL dependency; the format is stable and OpenSSL-familiar rather than pinned to one OpenSSL version; a malformed part falls back to a hex dump rather than throwing — `certificate`, `crl`, `csr`, `cms`, `any` |
241
242
  | `pki.webauthn` | WebAuthn / passkey attestation verification — offline trust evaluation of a W3C WebAuthn (Level 3) attestation. `parseAttestationObject(bytes)` decodes the CBOR attestation object + authenticatorData + COSE credential key over the strict `pki.cbor` codec; `verify(attestationObject, clientDataHash, opts)` checks the attestation-statement signature and each format's structural bindings for **packed / tpm / android-key / apple / fido-u2f / none** — the x5c leaf key, the apple nonce, the tpm `certInfo` Name/`extraData` over the `pubArea`, the android `KeyDescription`, the fido-u2f `verificationData` — binding the credential public key to each attestation (via the signed authenticatorData for packed/fido-u2f, or a cert/`pubArea`-key equality check for android-key/apple/tpm) and enforcing each leaf's certificate requirements. The credential-key check covers the full WebAuthn COSE algorithm set — ES256/384/512, RS256/384/512, PS256, 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. A verifier, not a ceremony client; fail-closed with typed `webauthn/*` errors. Chaining the returned trust path to a pinned root (and aaguid→root via FIDO MDS) is the caller's step through `pki.path.validate` — `parseAttestationObject`, `verify` |
242
243
  | `pki.lint` | Certificate linting — the zlint / 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/size, validity ordering + the SC081v3 reducing validity schedule, keyCertSign coherence, extension criticality — basicConstraints/nameConstraints/policyConstraints/inhibitAnyPolicy must be critical and keyUsage should be, nameConstraints CA-scope, unknown critical extensions, empty-subject SAN, SKI/AKI presence including the end-entity subjectKeyIdentifier, SAN required + CN-in-SAN, dNSName syntax, serverAuth EKU, weak keys). Unlike every other entry the DATA path never throws: hostile bytes return a `fatal` `lint/unparseable` finding (with the strict parser's code) so a whole directory lints without a try/catch; only config-time misuse throws a typed `LintError`. `certificate`, `rules`, `profiles` |
243
244
  | `pki.C` / `pki.constants` | Version-stable constants — functional scale helpers (`C.TIME.*`, `C.BYTES.*`), codec `LIMITS`, `version` |
package/index.js CHANGED
@@ -45,6 +45,7 @@ var csr = require("./lib/csr-sign");
45
45
  var attrcert = require("./lib/attrcert-sign");
46
46
  var crmf = require("./lib/crmf-sign");
47
47
  var cmp = require("./lib/cmp-build");
48
+ var crl = require("./lib/crl-sign");
48
49
  var merkle = require("./lib/merkle");
49
50
  var shbs = require("./lib/shbs");
50
51
  var hpke = require("./lib/hpke");
@@ -108,6 +109,10 @@ module.exports = {
108
109
  // protected PKIMessage (a certificate request / confirmation / revocation / general message).
109
110
  // Parsing lives at pki.schema.cmp.parse.
110
111
  cmp: cmp,
112
+ // `crl` is the RFC 5280 sec. 5 CRL producing side -- pki.crl.sign builds and signs a CertificateList
113
+ // over any registry algorithm, pki.crl.verify checks a CRL signature through the one path-validation
114
+ // signature engine, and pki.crl.isRevoked looks a serial up. Parsing lives at pki.schema.crl.parse.
115
+ crl: crl,
111
116
  // `merkle` is the RFC 6962 / RFC 9162 Merkle-tree proof-verification core --
112
117
  // pki.merkle.leafHash / nodeHash / emptyRootHash build the domain-separated
113
118
  // (0x00 leaf / 0x01 node) SHA-256 tree hashes; pki.merkle.verifyInclusion and
@@ -0,0 +1,574 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+
5
+ /**
6
+ * @module pki.crl
7
+ * @nav Signing
8
+ * @title CRLs
9
+ * @intro The X.509 CRL producing side. `pki.crl.sign` builds a `TBSCertList`, signs it, and emits a
10
+ * `CertificateList` (RFC 5280 sec. 5) that `pki.schema.crl.parse`, `pki.path.crlChecker`, and OpenSSL
11
+ * all accept -- over any signature algorithm the toolkit registry resolves: RSA (PKCS#1 v1.5 / PSS),
12
+ * ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite (hybrid) arms. `pki.crl.verify` checks a CRL
13
+ * signature through the one path-validation signature engine, and `pki.crl.isRevoked` looks a serial up
14
+ * in a parsed CRL. Parsing lives at `pki.schema.crl.parse`.
15
+ * @spec RFC 5280
16
+ * @card Build, sign, and verify an X.509 CRL (RFC 5280 sec. 5) over any registry algorithm.
17
+ */
18
+ //
19
+ // The signature matrix comes from the shared sign-scheme resolver (the same registry pki.x509.sign /
20
+ // pki.cms.sign drive), so a new algorithm is a registry row, never a branch here. The TBSCertList DER is
21
+ // assembled through the canonical asn1.build.* layer + the shared pki-build producing primitives; the
22
+ // strict schema-crl decoder round-trips it and OpenSSL cross-checks it. VERIFY composes the ONE
23
+ // path-validate.verifyCrlSignature engine pki.path.crlChecker uses -- there is no second, weaker verifier.
24
+
25
+ var asn1 = require("./asn1-der");
26
+ var oid = require("./oid");
27
+ var crlSchema = require("./schema-crl");
28
+ var x509Schema = require("./schema-x509");
29
+ var signScheme = require("./sign-scheme");
30
+ var guard = require("./guard-all");
31
+ var frameworkError = require("./framework-error");
32
+ var pkix = require("./schema-pkix");
33
+ var pkiBuild = require("./pki-build");
34
+ var constants = require("./constants");
35
+ require("./path-validate"); // side-effect: path-validate injects its signature engine into crl-verify at load
36
+ var crlVerify = require("./crl-verify");
37
+
38
+ var CrlError = frameworkError.CrlError;
39
+ var NS = pkix.makeNS("crl", CrlError, oid);
40
+ var NAME_SCHEMA = pkix.name(NS);
41
+ var SPKI_SCHEMA = pkix.spki(NS);
42
+ var b = asn1.build;
43
+ var TAGS = asn1.TAGS;
44
+
45
+ // Two error factories (x509-sign pattern): `_err` takes a full crl/* code; `_signE` prepends the domain so
46
+ // the shared sign-scheme resolver/signer faults keep crl/* codes. Both are FACTORIES (guard.* and
47
+ // resolveSignScheme invoke them as E(code, msg) with no `new`).
48
+ function _err(code, message, cause) { return new CrlError(code, message, cause); }
49
+ function _signE(kind, message, cause) { return new CrlError("crl/" + kind, message, cause); }
50
+ function O(n) { return oid.byName(n); }
51
+
52
+ // The shared PKIX producing primitives (lib/pki-build.js), bound to the crl namespace so they keep the
53
+ // frozen crl/* codes (Hard rule #5 -- one encode definition, no per-format hand-roll).
54
+ var _b = pkiBuild.makeBuilder({ ErrorClass: CrlError, prefix: "crl", O: O, NS: NS, NAME_SCHEMA: NAME_SCHEMA, SPKI_SCHEMA: SPKI_SCHEMA, EXT_DECODERS: {} });
55
+ var _encodeName = _b.encodeName, _isEmptyName = _b.isEmptyName, _reqDer = _b.reqDer,
56
+ _assertValidSpki = _b.assertValidSpki, _assertValidExtension = _b.assertValidExtension,
57
+ _timeDer = _b.timeDer, _ext = _b.ext, _extAki = _b.extAki, _spkiKeyId = _b.spkiKeyId,
58
+ _serialInteger = _b.serialInteger, _encodeGeneralName = _b.encodeGeneralName,
59
+ _certLikeFromSpki = _b.certLikeFromSpki, _assertSignatureVerifies = _b.assertSignatureVerifies;
60
+ // The RFC 5280 sec. 4.2.1 extension value decoders (shared with pki.schema.x509.parse), used to confirm an
61
+ // issuer certificate's keyUsage asserts cRLSign before a CRL is signed under it.
62
+ var EXT_DECODERS = pkix.certExtensionDecoders(NS).byOid;
63
+
64
+ var OID_SKI = O("subjectKeyIdentifier");
65
+
66
+ // CRLReason value<->name (RFC 5280 sec. 5.3.1). The value->name table is the frozen constants source;
67
+ // build the name->value reverse once at load. Value 7 is unused/reserved and absent from the table.
68
+ var CRL_REASON = constants.NAMES.CRL_REASON;
69
+ var REASON_BY_NAME = {};
70
+ Object.keys(CRL_REASON).forEach(function (v) { REASON_BY_NAME[CRL_REASON[v]] = Number(v); });
71
+
72
+ var KNOWN_CRL_EXT_KEYS = { authorityKeyIdentifier: 1, issuingDistributionPoint: 1, deltaCRLIndicator: 1, freshestCRL: 1, authorityInfoAccess: 1 };
73
+ var KNOWN_IDP_KEYS = { fullName: 1, onlyContainsUserCerts: 1, onlyContainsCACerts: 1, onlyContainsAttributeCerts: 1, indirectCRL: 1 };
74
+
75
+ // RFC 5280 sec. 5.2 -- the profile-fixed criticality of each recognized CRL extension. cRLNumber (5.2.3),
76
+ // authorityKeyIdentifier (5.2.1), freshestCRL (5.2.6), and authorityInfoAccess (5.2.7) MUST be non-critical;
77
+ // issuingDistributionPoint (5.2.5) and deltaCRLIndicator (5.2.4) MUST be critical. Enforced on the object
78
+ // form by construction AND on the pre-encoded escape hatch, so the hatch cannot bypass the profile.
79
+ var REQUIRED_CRITICALITY = {};
80
+ [["cRLNumber", false], ["authorityKeyIdentifier", false], ["freshestCRL", false], ["authorityInfoAccess", false],
81
+ ["issuingDistributionPoint", true], ["deltaCRLIndicator", true]].forEach(function (r) { REQUIRED_CRITICALITY[O(r[0])] = r[1]; });
82
+
83
+ // RFC 5280 sec. 5.3 -- the profile-fixed criticality of each recognized crlEntryExtension: reasonCode (5.3.1)
84
+ // and invalidityDate (5.3.2) MUST be non-critical; certificateIssuer (5.3.3) MUST be critical.
85
+ var REQUIRED_ENTRY_CRITICALITY = {};
86
+ [["reasonCode", false], ["invalidityDate", false]].forEach(function (r) { REQUIRED_ENTRY_CRITICALITY[O(r[0])] = r[1]; });
87
+
88
+ // certificateIssuer (indirect CRLs) is deferred: pki.path.crlChecker skips a CRL carrying any critical entry
89
+ // extension other than reasonCode (it does not track the per-entry issuer of an indirect CRL), so a CRL
90
+ // signed with certificateIssuer would verify but never be authoritative for revocation. Reject it -- on both
91
+ // the object form and the pre-encoded hatch -- until the checker processes indirect CRLs.
92
+ var _CERT_ISSUER_DEFERRED = "certificateIssuer (indirect CRLs) is not yet supported -- pki.path.crlChecker skips a CRL carrying any critical entry extension other than reasonCode, so an indirect CRL would never be authoritative for revocation. Re-enabled once the checker processes certificateIssuer / indirect CRLs.";
93
+ // The IDP indirectCRL flag marks the whole CRL as indirect, which crlChecker skips outright -- deferred for
94
+ // the same reason as certificateIssuer, on both the object form and the pre-encoded hatch.
95
+ var _INDIRECT_IDP_DEFERRED = "issuingDistributionPoint indirectCRL (indirect CRLs) is not yet supported -- pki.path.crlChecker skips a CRL whose IDP marks it indirect, so it would never be authoritative for revocation. Re-enabled once the checker processes indirect CRLs.";
96
+
97
+ // ---- shared numeric / reason / key-id helpers ------------------------------
98
+
99
+ // A non-negative INTEGER (0..MAX) of at most 20 content octets -- cRLNumber (sec. 5.2.3) and
100
+ // deltaCRLIndicator baseCRLNumber (sec. 5.2.4). b.integer has no size cap of its own (unlike serialInteger).
101
+ function _boundedInteger(v, label) {
102
+ var n;
103
+ if (typeof v === "bigint") n = v;
104
+ else if (typeof v === "number") { if (!Number.isSafeInteger(v)) throw _err("crl/bad-crl-number", label + " number must be a safe integer (pass a BigInt, hex string, or Buffer for a larger value)"); n = BigInt(v); }
105
+ else if (typeof v === "string") { try { n = BigInt(v); } catch (e) { throw _err("crl/bad-crl-number", label + " string must be a decimal or 0x-hex integer", e); } }
106
+ else if (Buffer.isBuffer(v)) { n = v.length ? BigInt("0x" + v.toString("hex")) : 0n; }
107
+ else throw _err("crl/bad-crl-number", label + " must be a BigInt, integer, hex string, or Buffer");
108
+ if (n < 0n) throw _err("crl/bad-crl-number", label + " must be non-negative (INTEGER 0..MAX, RFC 5280 sec. 5.2.3)");
109
+ var tlv = b.integer(n);
110
+ if (asn1.decode(tlv).content.length > 20) throw _err("crl/bad-crl-number", label + " must not exceed 20 octets (RFC 5280 sec. 5.2.3)");
111
+ return { tlv: tlv, value: n };
112
+ }
113
+
114
+ // Decode a pre-encoded Extension's extnValue (the trailing OCTET STRING's content) to one well-formed DER
115
+ // node, or reject -- so a recognized extension's value is validated on the escape hatch, not emitted opaque.
116
+ function _extInner(node, label) {
117
+ try { return asn1.decode(asn1.read.octetString(node.children[node.children.length - 1])); }
118
+ catch (e) { throw _err("crl/bad-input", "pre-encoded " + label + " extension value is not valid DER", e); }
119
+ }
120
+ // A pre-encoded cRLNumber / baseCRLNumber value: a non-negative INTEGER of at most 20 octets, or reject.
121
+ function _requireBoundedInt(inner, label) {
122
+ if (inner.tagClass !== "universal" || inner.tagNumber !== TAGS.INTEGER) throw _err("crl/bad-crl-number", "pre-encoded " + label + " value must be an INTEGER (RFC 5280 sec. 5.2.3 / 5.2.4)");
123
+ var v = asn1.read.integer(inner);
124
+ if (v < 0n) throw _err("crl/bad-crl-number", label + " must be non-negative (RFC 5280 sec. 5.2.3)");
125
+ if (inner.content.length > 20) throw _err("crl/bad-crl-number", label + " must not exceed 20 octets (RFC 5280 sec. 5.2.3)");
126
+ return v;
127
+ }
128
+
129
+ // Resolve a CRLReason to its numeric code: validate membership, reject 7/undefined, gate removeFromCRL(8)
130
+ // to a delta CRL. unspecified(0) is returned so the caller OMITS it (SHOULD be absent, sec. 5.3.1).
131
+ function _resolveReason(reason, isDelta) {
132
+ var codeNum;
133
+ if (typeof reason === "number") codeNum = reason;
134
+ else if (typeof reason === "string") {
135
+ if (!Object.prototype.hasOwnProperty.call(REASON_BY_NAME, reason)) throw _err("crl/bad-reason-code", "unknown CRLReason " + JSON.stringify(reason));
136
+ codeNum = REASON_BY_NAME[reason];
137
+ } else throw _err("crl/bad-reason-code", "reason must be a CRLReason name or number");
138
+ if (!Object.prototype.hasOwnProperty.call(CRL_REASON, String(codeNum))) throw _err("crl/bad-reason-code", "undefined or reserved CRLReason " + codeNum + " (RFC 5280 sec. 5.3.1)");
139
+ if (codeNum === 8 && !isDelta) throw _err("crl/bad-reason-code", "removeFromCRL(8) may appear only in a delta CRL (RFC 5280 sec. 5.3.1)");
140
+ return codeNum;
141
+ }
142
+
143
+ // The AKI keyIdentifier from the issuer: an explicit Buffer, or true -> the issuer cert's subjectKeyIdentifier,
144
+ // else the SHA-1 of the issuer SPKI (RFC 5280 sec. 5.2.1 key-identifier method).
145
+ function _akiKeyId(val, ctx) {
146
+ if (Buffer.isBuffer(val)) return val;
147
+ if (val === true) {
148
+ if (ctx.issuerCert) {
149
+ var ski = (ctx.issuerCert.extensions || []).filter(function (e) { return e.oid === OID_SKI; })[0];
150
+ if (ski) { try { return asn1.read.octetString(asn1.decode(ski.value)); } catch (_e) { /* fall through to re-derive from the issuer SPKI */ } }
151
+ }
152
+ return _spkiKeyId(ctx.issuerSpki);
153
+ }
154
+ throw _err("crl/bad-input", "authorityKeyIdentifier must be true (auto-derive from the issuer) or a Buffer key id");
155
+ }
156
+
157
+ // ---- CRL extension value encoders (sec. 5.2) -------------------------------
158
+
159
+ // IssuingDistributionPoint (sec. 5.2.5): critical; MUST NOT DER-encode to an empty SEQUENCE; at most one
160
+ // scope boolean TRUE; onlyContainsAttributeCerts MUST be FALSE; DEFAULT-FALSE booleans omitted.
161
+ function _idpValue(idp) {
162
+ if (!idp || typeof idp !== "object" || Array.isArray(idp) || Buffer.isBuffer(idp)) throw _err("crl/bad-idp", "issuingDistributionPoint must be an object");
163
+ Object.keys(idp).forEach(function (k) { if (!KNOWN_IDP_KEYS[k]) throw _err("crl/bad-idp", "unknown issuingDistributionPoint field " + JSON.stringify(k) + " (pass a pre-encoded Extension DER via the extensions array for an exotic field like onlySomeReasons)"); });
164
+ if (idp.onlyContainsAttributeCerts === true) throw _err("crl/bad-idp", "onlyContainsAttributeCerts=TRUE is not permitted for a conforming CRL issuer (RFC 5280 sec. 5.2.5)");
165
+ var children = [];
166
+ if (idp.fullName != null) {
167
+ var entries = Array.isArray(idp.fullName) ? idp.fullName : [idp.fullName];
168
+ if (!entries.length) throw _err("crl/bad-idp", "issuingDistributionPoint fullName must carry at least one GeneralName");
169
+ // distributionPoint [0] { fullName [0] IMPLICIT GeneralNames }
170
+ children.push(b.contextConstructed(0, b.contextConstructed(0, Buffer.concat(entries.map(_encodeGeneralName)))));
171
+ }
172
+ var scopeTrue = 0;
173
+ if (idp.onlyContainsUserCerts === true) { children.push(b.contextPrimitive(1, Buffer.from([0xff]))); scopeTrue++; }
174
+ if (idp.onlyContainsCACerts === true) { children.push(b.contextPrimitive(2, Buffer.from([0xff]))); scopeTrue++; }
175
+ if (idp.indirectCRL === true) throw _err("crl/bad-idp", _INDIRECT_IDP_DEFERRED);
176
+ if (scopeTrue > 1) throw _err("crl/bad-idp", "at most one of onlyContainsUserCerts / onlyContainsCACerts may be TRUE (RFC 5280 sec. 5.2.5)");
177
+ if (!children.length) throw _err("crl/bad-idp", "issuingDistributionPoint MUST NOT be empty (RFC 5280 sec. 5.2.5)");
178
+ return b.sequence(children);
179
+ }
180
+
181
+ // Validate a pre-encoded IssuingDistributionPoint value against the same RFC 5280 sec. 5.2.5 profile the
182
+ // object form enforces: MUST NOT be empty; at most one of onlyContainsUserCerts [1] / onlyContainsCACerts [2]
183
+ // TRUE; onlyContainsAttributeCerts [5] MUST be FALSE; indirectCRL [4] deferred (crlChecker skips indirect CRLs).
184
+ function _validatePreEncodedIdp(inner) {
185
+ if (!inner.children || !inner.children.length) throw _err("crl/bad-idp", "pre-encoded issuingDistributionPoint MUST NOT be empty (RFC 5280 sec. 5.2.5)");
186
+ var scopeTrue = 0;
187
+ inner.children.forEach(function (c) {
188
+ if (c.tagClass !== "context") throw _err("crl/bad-idp", "pre-encoded issuingDistributionPoint members must be context-tagged (RFC 5280 sec. 5.2.5)");
189
+ var isTrue = c.content != null && c.content.length > 0 && c.content[c.content.length - 1] !== 0x00; // a present DEFAULT-FALSE boolean is TRUE
190
+ if (c.tagNumber === 4) throw _err("crl/bad-idp", _INDIRECT_IDP_DEFERRED); // indirectCRL
191
+ if (c.tagNumber === 5 && isTrue) throw _err("crl/bad-idp", "onlyContainsAttributeCerts=TRUE is not permitted for a conforming CRL issuer (RFC 5280 sec. 5.2.5)");
192
+ if ((c.tagNumber === 1 || c.tagNumber === 2) && isTrue) scopeTrue++;
193
+ });
194
+ if (scopeTrue > 1) throw _err("crl/bad-idp", "at most one of onlyContainsUserCerts / onlyContainsCACerts may be TRUE (RFC 5280 sec. 5.2.5)");
195
+ }
196
+
197
+ // freshestCRL / CRLDistributionPoints (sec. 5.2.6): SEQUENCE OF DistributionPoint carrying only
198
+ // distributionPoint (reasons + cRLIssuer omitted). Accepts an array of GeneralName entries (one DP's
199
+ // fullName) or an array of { fullName } distribution points.
200
+ function _freshestValue(spec) {
201
+ if (!Array.isArray(spec) || !spec.length) throw _err("crl/bad-input", "freshestCRL must be a non-empty array of GeneralNames (or { fullName } distribution points)");
202
+ var dps = spec.every(function (e) { return e && typeof e === "object" && !Buffer.isBuffer(e) && Object.keys(e).length === 1 && e.fullName != null; })
203
+ ? spec.map(function (dp) { return dp.fullName; })
204
+ : [spec];
205
+ return b.sequence(dps.map(function (fullName) {
206
+ var entries = Array.isArray(fullName) ? fullName : [fullName];
207
+ if (!entries.length) throw _err("crl/bad-input", "a freshestCRL distribution point must carry at least one GeneralName (GeneralNames is SIZE(1..MAX), RFC 5280 sec. 4.2.1.13)");
208
+ return b.sequence([b.contextConstructed(0, b.contextConstructed(0, Buffer.concat(entries.map(_encodeGeneralName))))]);
209
+ }));
210
+ }
211
+
212
+ // authorityInfoAccess (sec. 5.2.7): SEQUENCE OF AccessDescription, caIssuers-only. Accepts an array of
213
+ // GeneralName entries, each a caIssuers accessLocation.
214
+ function _aiaValue(spec) {
215
+ if (!Array.isArray(spec) || !spec.length) throw _err("crl/bad-input", "authorityInfoAccess must be a non-empty array of caIssuers GeneralNames");
216
+ var caIssuers = O("caIssuers");
217
+ return b.sequence(spec.map(function (gn) { return b.sequence([b.oid(caIssuers), _encodeGeneralName(gn)]); }));
218
+ }
219
+
220
+ // The crlExtensions [0] set. Fixed per-extension criticality (sec. 5.2); at most one instance of each
221
+ // (sec. 5.2). `spec.extensions` is an object of the recognized keys OR an array of pre-encoded Extension
222
+ // DER (escape hatch for an extension the object form does not model). Returns { exts, isDelta }.
223
+ function _buildCrlExtensions(spec, ctx) {
224
+ var out = [], seen = {}, isDelta = false;
225
+ function push(oidName, critical, valueDer) {
226
+ var id = O(oidName);
227
+ if (seen[id]) throw _err("crl/bad-input", "duplicate CRL extension " + oidName + " (RFC 5280 sec. 5.2 -- at most one instance)");
228
+ seen[id] = true;
229
+ out.push(_ext(id, critical, valueDer));
230
+ }
231
+ var crlNumberVal = null;
232
+ if (spec.crlNumber != null) { var cn = _boundedInteger(spec.crlNumber, "cRLNumber"); crlNumberVal = cn.value; push("cRLNumber", false, cn.tlv); }
233
+ var ext = spec.extensions;
234
+ if (ext == null) return { exts: out, isDelta: isDelta };
235
+ if (Array.isArray(ext)) {
236
+ var arrBase = null, arrCrlNum = null;
237
+ ext.forEach(function (e, i) {
238
+ var der = _reqDer(e, "extension");
239
+ _assertValidExtension(der, i);
240
+ var node = asn1.decode(der);
241
+ var extnId = asn1.read.oid(node.children[0]);
242
+ if (seen[extnId]) throw _err("crl/bad-input", "duplicate extension " + extnId + " (RFC 5280 sec. 5.2)");
243
+ seen[extnId] = true;
244
+ // RFC 5280 sec. 5.2 -- a recognized CRL extension is held to the profile even via the escape hatch: its
245
+ // fixed criticality (assertValidExtension rejects an explicit critical=FALSE, so 3 children == critical,
246
+ // 2 == non-critical) AND a well-formed value of the right type (INTEGER for cRLNumber/deltaCRLIndicator,
247
+ // else a SEQUENCE), never emitted opaque.
248
+ if (Object.prototype.hasOwnProperty.call(REQUIRED_CRITICALITY, extnId)) {
249
+ if ((node.children.length === 3) !== REQUIRED_CRITICALITY[extnId]) {
250
+ throw _err("crl/bad-input", "pre-encoded " + (oid.name(extnId) || extnId) + " extension has the wrong criticality (RFC 5280 sec. 5.2 requires it " + (REQUIRED_CRITICALITY[extnId] ? "critical" : "non-critical") + ")");
251
+ }
252
+ var inner = _extInner(node, oid.name(extnId) || extnId);
253
+ if (extnId === O("cRLNumber")) arrCrlNum = _requireBoundedInt(inner, "cRLNumber");
254
+ else if (extnId === O("deltaCRLIndicator")) arrBase = _requireBoundedInt(inner, "deltaCRLIndicator baseCRLNumber");
255
+ else {
256
+ if (inner.tagClass !== "universal" || inner.tagNumber !== TAGS.SEQUENCE) throw _err("crl/bad-input", "pre-encoded " + (oid.name(extnId) || extnId) + " extension value must be a SEQUENCE (RFC 5280 sec. 5.2)");
257
+ // A pre-encoded IssuingDistributionPoint is held to the same sec. 5.2.5 profile as the object form.
258
+ if (extnId === O("issuingDistributionPoint")) _validatePreEncodedIdp(inner);
259
+ }
260
+ }
261
+ if (extnId === O("deltaCRLIndicator")) isDelta = true;
262
+ out.push(b.raw(der));
263
+ });
264
+ // RFC 5280 sec. 5.2.3 / 5.2.4 -- a delta CRL MUST carry a cRLNumber greater than its baseCRLNumber, whether
265
+ // that cRLNumber is pre-encoded here or supplied via spec.crlNumber; the escape hatch is held to the same rule.
266
+ if (isDelta) {
267
+ // RFC 5280 sec. 5.2.6 -- freshestCRL MUST NOT appear in a delta CRL, including via the pre-encoded hatch.
268
+ if (seen[O("freshestCRL")]) throw _err("crl/bad-input", "freshestCRL MUST NOT appear in a delta CRL (RFC 5280 sec. 5.2.6)");
269
+ var eff = crlNumberVal != null ? crlNumberVal : arrCrlNum;
270
+ if (eff == null) throw _err("crl/bad-input", "a delta CRL MUST include a cRLNumber (RFC 5280 sec. 5.2.3 / 5.2.4)");
271
+ if (arrBase != null && eff <= arrBase) throw _err("crl/bad-crl-number", "a delta CRL's cRLNumber MUST be greater than its baseCRLNumber (RFC 5280 sec. 5.2.4)");
272
+ }
273
+ return { exts: out, isDelta: isDelta };
274
+ }
275
+ if (typeof ext !== "object") throw _err("crl/bad-input", "extensions must be an object or an array of pre-encoded Extension DER");
276
+ Object.keys(ext).forEach(function (k) { if (!KNOWN_CRL_EXT_KEYS[k]) throw _err("crl/bad-input", "unknown CRL extension " + JSON.stringify(k) + " in the extensions spec; pass a pre-encoded Extension DER via the array form"); });
277
+ isDelta = ext.deltaCRLIndicator != null;
278
+ if (ext.authorityKeyIdentifier != null) push("authorityKeyIdentifier", false, _extAki(_akiKeyId(ext.authorityKeyIdentifier, ctx)));
279
+ if (ext.issuingDistributionPoint != null) push("issuingDistributionPoint", true, _idpValue(ext.issuingDistributionPoint));
280
+ if (ext.deltaCRLIndicator != null) {
281
+ var base = _boundedInteger(ext.deltaCRLIndicator, "deltaCRLIndicator baseCRLNumber");
282
+ // RFC 5280 sec. 5.2.3 / 5.2.4 -- a conforming delta CRL MUST carry a cRLNumber (sec. 5.2.3 requires it in
283
+ // every CRL), and that cRLNumber MUST be greater than the BaseCRLNumber it is built against (sec. 5.2.4).
284
+ if (crlNumberVal == null) throw _err("crl/bad-input", "a delta CRL MUST include a cRLNumber (set spec.crlNumber) (RFC 5280 sec. 5.2.3 / 5.2.4)");
285
+ if (crlNumberVal <= base.value) throw _err("crl/bad-crl-number", "a delta CRL's cRLNumber (" + crlNumberVal + ") MUST be greater than its baseCRLNumber (" + base.value + ") (RFC 5280 sec. 5.2.4)");
286
+ push("deltaCRLIndicator", true, base.tlv);
287
+ }
288
+ if (ext.freshestCRL != null) {
289
+ if (isDelta) throw _err("crl/bad-input", "freshestCRL MUST NOT appear in a delta CRL (RFC 5280 sec. 5.2.6)");
290
+ push("freshestCRL", false, _freshestValue(ext.freshestCRL));
291
+ }
292
+ if (ext.authorityInfoAccess != null) push("authorityInfoAccess", false, _aiaValue(ext.authorityInfoAccess));
293
+ return { exts: out, isDelta: isDelta };
294
+ }
295
+
296
+ // The revokedCertificates entries (sec. 5.1.2.6 / 5.3). Each entry: serial + revocationDate + optional
297
+ // crlEntryExtensions (reasonCode ENUMERATED, invalidityDate GeneralizedTime-only, certificateIssuer, or a
298
+ // pre-encoded escape hatch). Returns { entries, anyExt } -- anyExt forces v2 (sec. 5.1.2.1).
299
+ function _buildRevoked(entryList, isDelta) {
300
+ if (!Array.isArray(entryList)) throw _err("crl/bad-input", "revoked must be an array of revoked-certificate entries");
301
+ var anyExt = false, seenSerials = {};
302
+ var entries = entryList.map(function (e, idx) {
303
+ if (!e || typeof e !== "object" || Buffer.isBuffer(e)) throw _err("crl/bad-input", "each revoked entry must be an object");
304
+ if (e.serialNumber == null) throw _err("crl/bad-input", "revoked entry [" + idx + "] requires a serialNumber");
305
+ var serialTlv = _serialInteger(e.serialNumber);
306
+ // RFC 5280 sec. 5.1.2.6 -- a serial number identifies a certificate uniquely, so a CRL MUST NOT list the
307
+ // same serial twice; dedup on the DER content (the same value the parser surfaces as serialNumberHex).
308
+ var serialKey = asn1.decode(serialTlv).content.toString("hex");
309
+ if (seenSerials[serialKey]) throw _err("crl/bad-input", "revoked entry [" + idx + "] duplicates a serial number already listed in this CRL (RFC 5280 sec. 5.1.2.6)");
310
+ seenSerials[serialKey] = true;
311
+ var children = [serialTlv, _timeDer(e.revocationDate, "revocationDate")];
312
+ var entryExts = [], seen = {};
313
+ function pushE(oidName, critical, valueDer) {
314
+ var id = O(oidName);
315
+ if (seen[id]) throw _err("crl/bad-input", "duplicate entry extension " + oidName + " in revoked entry [" + idx + "]");
316
+ seen[id] = true;
317
+ entryExts.push(_ext(id, critical, valueDer));
318
+ }
319
+ if (e.reason != null) {
320
+ var codeNum = _resolveReason(e.reason, isDelta);
321
+ if (codeNum !== 0) pushE("reasonCode", false, b.enumerated(BigInt(codeNum))); // unspecified(0) omitted (sec. 5.3.1)
322
+ }
323
+ if (e.invalidityDate != null) {
324
+ guard.time.assertValid(e.invalidityDate, _err, "crl/bad-input", "invalidityDate");
325
+ // sec. 5.3.2 -- ALWAYS GeneralizedTime (never the UTCTime cutover), no fractional seconds.
326
+ pushE("invalidityDate", false, b.generalizedTime(e.invalidityDate));
327
+ }
328
+ if (e.certificateIssuer != null) throw _err("crl/bad-input", _CERT_ISSUER_DEFERRED);
329
+ if (e.extensions != null) {
330
+ if (!Array.isArray(e.extensions)) throw _err("crl/bad-input", "revoked entry [" + idx + "] extensions must be an array of pre-encoded Extension DER");
331
+ e.extensions.forEach(function (x, j) {
332
+ var der = _reqDer(x, "entry extension");
333
+ _assertValidExtension(der, j);
334
+ var xnode = asn1.decode(der);
335
+ var extnId = asn1.read.oid(xnode.children[0]);
336
+ if (seen[extnId]) throw _err("crl/bad-input", "duplicate entry extension " + extnId + " in revoked entry [" + idx + "]");
337
+ seen[extnId] = true;
338
+ // certificateIssuer (indirect CRLs) is not usable end-to-end yet (crlChecker skips such a CRL); reject it.
339
+ if (extnId === O("certificateIssuer")) throw _err("crl/bad-input", _CERT_ISSUER_DEFERRED);
340
+ // RFC 5280 sec. 5.3 -- a recognized entry extension is held to the profile even via the escape hatch:
341
+ // its fixed criticality AND a well-formed value of the right type (reasonCode an ENUMERATED in the
342
+ // legal set, invalidityDate a GeneralizedTime).
343
+ if (Object.prototype.hasOwnProperty.call(REQUIRED_ENTRY_CRITICALITY, extnId)) {
344
+ if ((xnode.children.length === 3) !== REQUIRED_ENTRY_CRITICALITY[extnId]) throw _err("crl/bad-input", "pre-encoded entry extension " + (oid.name(extnId) || extnId) + " has the wrong criticality (RFC 5280 sec. 5.3 requires it " + (REQUIRED_ENTRY_CRITICALITY[extnId] ? "critical" : "non-critical") + ")");
345
+ var einner = _extInner(xnode, oid.name(extnId) || extnId);
346
+ if (extnId === O("reasonCode")) {
347
+ if (einner.tagClass !== "universal" || einner.tagNumber !== TAGS.ENUMERATED) throw _err("crl/bad-reason-code", "pre-encoded reasonCode value must be an ENUMERATED (RFC 5280 sec. 5.3.1)");
348
+ var rc = asn1.read.enumerated(einner);
349
+ if (!Object.prototype.hasOwnProperty.call(CRL_REASON, rc.toString())) throw _err("crl/bad-reason-code", "pre-encoded reasonCode " + rc + " is undefined or reserved (RFC 5280 sec. 5.3.1)");
350
+ if (rc === 8n && !isDelta) throw _err("crl/bad-reason-code", "removeFromCRL(8) may appear only in a delta CRL (RFC 5280 sec. 5.3.1)");
351
+ } else if (extnId === O("invalidityDate")) {
352
+ if (einner.tagClass !== "universal" || einner.tagNumber !== TAGS.GENERALIZED_TIME) throw _err("crl/bad-input", "pre-encoded invalidityDate value must be a GeneralizedTime (RFC 5280 sec. 5.3.2)");
353
+ try { asn1.read.time(einner); } catch (e) { throw _err("crl/bad-input", "pre-encoded invalidityDate is not a well-formed GeneralizedTime (RFC 5280 sec. 5.3.2)", e); }
354
+ }
355
+ }
356
+ entryExts.push(b.raw(der));
357
+ });
358
+ }
359
+ if (entryExts.length) { anyExt = true; children.push(b.sequence(entryExts)); }
360
+ return b.sequence(children);
361
+ });
362
+ return { entries: entries, anyExt: anyExt };
363
+ }
364
+
365
+ // ---- the primitives --------------------------------------------------------
366
+
367
+ function _parseIssuerCert(cert) {
368
+ var parsed = (Buffer.isBuffer(cert) || typeof cert === "string") ? x509Schema.parse(cert) : cert;
369
+ if (!parsed || !parsed.tbsBytes || !parsed.subjectPublicKeyInfo) throw _err("crl/bad-input", "issuer.cert must be a certificate DER/PEM or a parsed certificate");
370
+ return parsed;
371
+ }
372
+
373
+ // RFC 5280 sec. 4.2.1.3 -- a certificate whose key signs CRLs asserts the cRLSign keyUsage bit. When the
374
+ // issuer certificate carries a keyUsage extension it MUST include cRLSign, or the CRL it signs is rejected
375
+ // by a conforming relying party (pki.path.crlChecker included). Mirrors x509-sign's keyCertSign gate; a
376
+ // certificate with no keyUsage extension is unrestricted.
377
+ function _assertIssuerCanSignCrl(issuerCert) {
378
+ var kuExt = (issuerCert.extensions || []).filter(function (e) { return e.oid === O("keyUsage"); })[0];
379
+ if (!kuExt) return;
380
+ var ku;
381
+ try { ku = EXT_DECODERS[O("keyUsage")](kuExt.value); }
382
+ catch (e) { if (e instanceof CrlError) throw e; throw _err("crl/bad-input", "the issuer certificate keyUsage is malformed", e); }
383
+ if (ku.cRLSign !== true) throw _err("crl/bad-input", "the issuer certificate keyUsage does not assert cRLSign -- it cannot sign CRLs (RFC 5280 sec. 4.2.1.3)");
384
+ }
385
+
386
+ function _sign(spec, issuer, opts) {
387
+ opts = opts || {};
388
+ if (!spec || typeof spec !== "object" || Buffer.isBuffer(spec)) throw _err("crl/bad-input", "the CRL spec must be an object");
389
+ issuer = issuer || {};
390
+ if (issuer.key == null) throw _err("crl/bad-input", "a signing key (issuer.key, a PKCS#8 private key) is required");
391
+
392
+ // Resolve the issuer name + signing-key SPKI. `{ cert }` supplies both; else `{ name|spec.issuer, publicKey }`.
393
+ var issuerDer, issuerSpki, issuerCert = null;
394
+ if (issuer.cert != null) {
395
+ issuerCert = _parseIssuerCert(issuer.cert);
396
+ _assertIssuerCanSignCrl(issuerCert);
397
+ issuerDer = pkiBuild.tbsNameField(issuerCert, "subject");
398
+ issuerSpki = issuerCert.subjectPublicKeyInfo.bytes;
399
+ } else {
400
+ issuerSpki = _reqDer(issuer.publicKey, "issuer.publicKey (the issuer SPKI DER)");
401
+ _assertValidSpki(issuerSpki, "issuer.publicKey");
402
+ var dnSource = issuer.name != null ? issuer.name : spec.issuer;
403
+ if (dnSource == null) throw _err("crl/bad-issuer", "an issuer distinguished name is required (issuer.name or spec.issuer) when no issuer.cert is given");
404
+ issuerDer = _encodeName(dnSource);
405
+ }
406
+ // RFC 5280 sec. 5.1.2.3 -- the issuer MUST be a non-empty distinguished name.
407
+ if (_isEmptyName(issuerDer)) throw _err("crl/bad-issuer", "issuer must be a non-empty distinguished name (RFC 5280 sec. 5.1.2.3)");
408
+
409
+ // thisUpdate (required) + nextUpdate (optional; MUST NOT precede thisUpdate). _timeDer validates each date
410
+ // and applies the RFC 5280 sec. 5.1.2.4/.5 UTCTime<=2049-else-GeneralizedTime cutover.
411
+ if (spec.thisUpdate == null) throw _err("crl/bad-input", "thisUpdate is required (RFC 5280 sec. 5.1.2.4)");
412
+ var thisU = _timeDer(spec.thisUpdate, "thisUpdate");
413
+ var nextU = null;
414
+ if (spec.nextUpdate != null) {
415
+ nextU = _timeDer(spec.nextUpdate, "nextUpdate");
416
+ // allow:nan-date-comparison-unguarded -- both operands are guard.time.assertValid'd via _timeDer on the
417
+ // two lines above (thisUpdate + nextUpdate), so an Invalid Date throws before this comparison.
418
+ if (spec.nextUpdate.getTime() < spec.thisUpdate.getTime()) throw _err("crl/bad-input", "nextUpdate must not be before thisUpdate (RFC 5280 sec. 5.1.2.5)");
419
+ }
420
+
421
+ var extResult = _buildCrlExtensions(spec, { issuerCert: issuerCert, issuerSpki: issuerSpki });
422
+ var crlExts = extResult.exts;
423
+ var revoked = spec.revoked != null ? _buildRevoked(spec.revoked, extResult.isDelta) : { entries: [], anyExt: false };
424
+ // RFC 5280 sec. 5.1.2.1 -- v2 iff any CRL or entry extension is present, else v1 (version omitted).
425
+ var version = (crlExts.length || revoked.anyExt) ? 2 : 1;
426
+
427
+ // The signature scheme resolves from the SIGNING key's SPKI algorithm (the whole registry, for free).
428
+ var scheme = signScheme.resolveSignScheme(_certLikeFromSpki(issuerSpki), { combinedRsaSig: true, pss: opts.pss, digestAlgorithm: opts.digestAlgorithm }, true, _signE);
429
+
430
+ var tbsChildren = [];
431
+ if (version === 2) tbsChildren.push(b.integer(1n)); // bare INTEGER(1), NOT a [0] EXPLICIT tag like a certificate
432
+ tbsChildren.push(scheme.sigAlgId); // signature == signatureAlgorithm (sec. 5.1.1.2), single source
433
+ tbsChildren.push(issuerDer);
434
+ tbsChildren.push(thisU);
435
+ if (nextU) tbsChildren.push(nextU);
436
+ if (revoked.entries.length) tbsChildren.push(b.sequence(revoked.entries)); // omit the field when empty (sec. 5.1.2.6)
437
+ if (crlExts.length) tbsChildren.push(b.explicit(0, b.sequence(crlExts))); // crlExtensions [0] EXPLICIT
438
+ var tbsDer = b.sequence(tbsChildren);
439
+
440
+ return signScheme.signOverTbs(scheme, issuer.key, tbsDer, _signE).then(function (sig) {
441
+ // The signature MUST verify under the issuer public key before returning, or the CRL would be rejected
442
+ // downstream (composite returns a promise; the classical/PQC path throws synchronously on a mismatch).
443
+ return Promise.resolve(_assertSignatureVerifies(tbsDer, sig, issuerSpki, scheme)).then(function () {
444
+ var crlDer = b.sequence([tbsDer, scheme.sigAlgId, b.bitString(sig, 0)]);
445
+ return opts.pem ? crlSchema.pemEncode(crlDer, "X509 CRL") : crlDer;
446
+ });
447
+ }, function (e) {
448
+ if (e instanceof CrlError) throw e;
449
+ throw _err("crl/bad-input", "signing the CRL failed -- the signing key does not match the resolved algorithm or is invalid", e);
450
+ });
451
+ }
452
+
453
+ /**
454
+ * @primitive pki.crl.sign
455
+ * @signature pki.crl.sign(spec, issuer, opts?) -> Promise<Buffer|string>
456
+ * @since 0.3.9
457
+ * @status experimental
458
+ * @spec RFC 5280 sec. 5, RFC 9882, RFC 9814
459
+ * @defends crl-forgery (CWE-347)
460
+ * @related pki.schema.crl.parse, pki.crl.verify, pki.path.crlChecker
461
+ *
462
+ * Build, sign, and DER-encode an X.509 certificate revocation list. `spec` describes the CRL --
463
+ * `thisUpdate` / `nextUpdate` (`Date`s), an optional `crlNumber`, a `revoked` array (each entry a
464
+ * `serialNumber` + `revocationDate` with an optional `reason` or `invalidityDate`),
465
+ * and an optional `extensions` object (`authorityKeyIdentifier`, `issuingDistributionPoint`,
466
+ * `deltaCRLIndicator`, `freshestCRL`, `authorityInfoAccess`) or an array of pre-encoded `Extension` DER.
467
+ * `issuer` is the signing side: `{ cert, key }` takes the issuer DN + SPKI from a CA certificate;
468
+ * `{ name, publicKey, key }` (or `spec.issuer` + `{ publicKey, key }`) supplies them explicitly. The
469
+ * signature algorithm is resolved from the signing key, so every algorithm the toolkit signs with (RSA
470
+ * PKCS#1 v1.5 / PSS, ECDSA, EdDSA, ML-DSA, SLH-DSA, composite) is available without a per-algorithm branch.
471
+ *
472
+ * The version is derived from the field set (v2 when any CRL or entry extension is present, else v1). The
473
+ * outer `signatureAlgorithm` is emitted from the same source as `tbsCertList.signature` (sec. 5.1.1.2); an
474
+ * empty revocation list omits `revokedCertificates` rather than emitting an empty SEQUENCE (sec. 5.1.2.6);
475
+ * `reasonCode` is an ENUMERATED and `invalidityDate` is always GeneralizedTime (sec. 5.3.1/5.3.2);
476
+ * per-extension criticality is fixed by the RFC; and the produced signature is verified under the issuer
477
+ * key before return. A violation throws a typed `CrlError`.
478
+ *
479
+ * @opts
480
+ * - `pem` (boolean) -- return a PEM `X509 CRL` string instead of DER.
481
+ * - `pss` (boolean) -- sign an RSA key with RSASSA-PSS rather than PKCS#1 v1.5.
482
+ * - `digestAlgorithm` (string) -- override the message digest where the algorithm permits a choice.
483
+ * @example
484
+ * var der = await pki.crl.sign({
485
+ * thisUpdate: new Date("2026-01-01T00:00:00Z"), nextUpdate: new Date("2026-02-01T00:00:00Z"),
486
+ * crlNumber: 7n,
487
+ * revoked: [{ serialNumber: 0x1234n, revocationDate: new Date("2026-01-15T00:00:00Z"), reason: "keyCompromise" }],
488
+ * extensions: { authorityKeyIdentifier: true },
489
+ * }, { cert: signerCertDer, key: signerKeyPkcs8 });
490
+ * pki.schema.crl.parse(der).revokedCertificates[0].serialNumberHex; // "1234"
491
+ */
492
+ function sign(spec, issuer, opts) { return Promise.resolve().then(function () { return _sign(spec, issuer, opts); }); }
493
+
494
+ function _coerceCrl(crl) {
495
+ if (Buffer.isBuffer(crl) || typeof crl === "string") return crlSchema.parse(crl);
496
+ if (crl && typeof crl === "object" && crl.tbsBytes && crl.signatureValue && crl.signatureAlgorithm) return crl;
497
+ throw _err("crl/bad-input", "crl must be a CRL DER Buffer, a PEM string, or a parsed CRL (from pki.schema.crl.parse)");
498
+ }
499
+
500
+ function _resolveIssuerSpki(issuer) {
501
+ if (issuer == null) throw _err("crl/bad-input", "an issuer is required to verify a CRL");
502
+ if (Buffer.isBuffer(issuer)) { _assertValidSpki(issuer, "issuer SPKI"); return issuer; }
503
+ if (issuer.cert != null) return _parseIssuerCert(issuer.cert).subjectPublicKeyInfo.bytes;
504
+ if (issuer.publicKey != null) { var spki = _reqDer(issuer.publicKey, "issuer.publicKey"); _assertValidSpki(spki, "issuer.publicKey"); return spki; }
505
+ if (issuer.subjectPublicKeyInfo && issuer.subjectPublicKeyInfo.bytes) return issuer.subjectPublicKeyInfo.bytes; // a parsed certificate
506
+ throw _err("crl/bad-input", "issuer must be { cert }, { publicKey } (SPKI DER), or a raw SPKI Buffer");
507
+ }
508
+
509
+ /**
510
+ * @primitive pki.crl.verify
511
+ * @signature pki.crl.verify(crl, issuer) -> Promise<boolean>
512
+ * @since 0.3.9
513
+ * @status experimental
514
+ * @spec RFC 5280 sec. 5.1.1.3, RFC 9814
515
+ * @defends crl-signature-bypass (CWE-347)
516
+ * @related pki.crl.sign, pki.path.crlChecker, pki.schema.crl.parse
517
+ *
518
+ * Verify a CRL's signature over its exact parsed `tbsCertList` bytes under the issuer public key. `crl`
519
+ * is a DER `Buffer`, a PEM string, or a parsed CRL; `issuer` is `{ cert }` (DER/PEM/parsed), `{ publicKey }`
520
+ * (SPKI DER), or a raw SPKI `Buffer`. Verification composes the one path-validation signature engine
521
+ * `pki.path.crlChecker` uses -- the same algorithm-confusion (RFC 9814 sec. 4 key-OID == sig-OID) and
522
+ * EdDSA low-order-point gates -- so there is no second, weaker CRL verifier. It fails closed to `false` on
523
+ * any resolution, import, or verification fault; malformed input throws a typed `CrlError`. This checks the
524
+ * signature only -- issuer authorization, currency, and distribution-point scope are `pki.path.crlChecker`.
525
+ *
526
+ * @example
527
+ * var ok = await pki.crl.verify(crlDer, { publicKey: signerSpki }); // true / false
528
+ */
529
+ function verify(crl, issuer) { return Promise.resolve().then(function () { return _verify(crl, issuer); }); }
530
+ function _verify(crl, issuer) {
531
+ var parsed = _coerceCrl(crl);
532
+ var spki = _resolveIssuerSpki(issuer);
533
+ return crlVerify.verifyCrlSignature(parsed, spki);
534
+ }
535
+
536
+ function _serialHexOf(serial) {
537
+ var v;
538
+ if (typeof serial === "bigint") v = serial;
539
+ else if (typeof serial === "number") { if (!Number.isSafeInteger(serial)) throw _err("crl/bad-input", "serialNumber number must be a safe integer (pass a BigInt, hex string, or Buffer)"); v = BigInt(serial); }
540
+ else if (typeof serial === "string") { try { v = BigInt(serial); } catch (e) { throw _err("crl/bad-input", "serialNumber string must be a decimal or 0x-hex integer", e); } }
541
+ else if (Buffer.isBuffer(serial)) { v = serial.length ? BigInt("0x" + serial.toString("hex")) : 0n; }
542
+ else throw _err("crl/bad-input", "serialNumber must be a BigInt, integer, hex string, or Buffer");
543
+ if (v <= 0n) throw _err("crl/bad-input", "serialNumber must be a positive integer");
544
+ // Match schema-crl's serialNumberHex: the DER INTEGER content octets (preserving sign padding).
545
+ return asn1.decode(b.integer(v)).content.toString("hex");
546
+ }
547
+
548
+ /**
549
+ * @primitive pki.crl.isRevoked
550
+ * @signature pki.crl.isRevoked(crl, serialNumber) -> entry | null
551
+ * @since 0.3.9
552
+ * @status experimental
553
+ * @spec RFC 5280 sec. 5.1.2.6
554
+ * @related pki.crl.verify, pki.schema.crl.parse
555
+ *
556
+ * Look a certificate serial number up in a CRL's `revokedCertificates` list. `crl` is a DER `Buffer`, a
557
+ * PEM string, or a parsed CRL; `serialNumber` is a `BigInt`, a safe integer, a decimal / `0x`-hex string,
558
+ * or a magnitude `Buffer`. Returns the matching revoked-certificate entry (`{ serialNumber, serialNumberHex,
559
+ * revocationDate, crlEntryExtensions }`) or `null` when the serial is not listed. A structural lookup only --
560
+ * it does NOT verify the CRL signature or its currency; call `pki.crl.verify` / `pki.path.crlChecker` for that.
561
+ *
562
+ * @example
563
+ * pki.crl.isRevoked(crlDer, 0x1234n) ? "revoked" : "not listed";
564
+ */
565
+ function isRevoked(crl, serialNumber) {
566
+ var parsed = _coerceCrl(crl);
567
+ var hex = _serialHexOf(serialNumber);
568
+ for (var i = 0; i < parsed.revokedCertificates.length; i++) {
569
+ if (parsed.revokedCertificates[i].serialNumberHex === hex) return parsed.revokedCertificates[i];
570
+ }
571
+ return null;
572
+ }
573
+
574
+ module.exports = { sign: sign, verify: verify, isRevoked: isRevoked };
@@ -0,0 +1,27 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal -- the ONE CRL signature-verify seam, shared by pki.path.crlChecker and pki.crl.verify so both
6
+ // route through the SAME signature engine (algorithm-confusion + EdDSA low-order + composite gates) and no
7
+ // second, weaker CRL verifier can drift. path-validate owns that engine (_verifyWithSpki) and injects it
8
+ // here at its module load (setEngine). Keeping the seam in this internal module -- rather than on
9
+ // path-validate's module.exports -- keeps it OFF the public pki.path surface (it takes the path-internal
10
+ // SubjectPublicKeyInfo bytes, not a documented issuer shape). NOT wired into index.js; reached only by require.
11
+
12
+ var guard = require("./guard-all");
13
+
14
+ var _engine = null; // path-validate's _verifyWithSpki, injected at path-validate's module load via setEngine.
15
+
16
+ function setEngine(verifyWithSpki) { _engine = verifyWithSpki; }
17
+
18
+ // Verify a parsed CRL's signature over its raw tbsCertList bytes under the issuer SubjectPublicKeyInfo DER.
19
+ // Fail-closed to false on a non-octet-aligned signature or any engine fault (the engine never throws out).
20
+ function verifyCrlSignature(crl, spkiBytes) {
21
+ // _engine is injected by path-validate at its module load (setEngine); pki.crl.sign requires path-validate
22
+ // for exactly that side-effect, so the engine is always set before a verify runs.
23
+ if (!guard.crypto.isOctetAligned(crl.signatureValue)) return Promise.resolve(false); // non-octet-aligned signature
24
+ return _engine(crl.signatureAlgorithm, crl.signatureValue.bytes, spkiBytes, crl.tbsBytes);
25
+ }
26
+
27
+ module.exports = { setEngine: setEngine, verifyCrlSignature: verifyCrlSignature };
package/lib/inspect.js CHANGED
@@ -27,6 +27,10 @@ var constants = require("./constants");
27
27
  var asn1 = require("./asn1-der");
28
28
  var oid = require("./oid");
29
29
  var x509 = require("./schema-x509");
30
+ var crl = require("./schema-crl");
31
+ var csr = require("./schema-csr");
32
+ var cms = require("./schema-cms");
33
+ var schemaAll = require("./schema-all");
30
34
  var pkix = require("./schema-pkix");
31
35
  var guard = require("./guard-all");
32
36
 
@@ -590,8 +594,395 @@ function certificate(input) {
590
594
  return L.join("\n") + "\n";
591
595
  }
592
596
 
597
+ // ---- CRL / CSR / CMS coercion (the _parse model, per format) -----------------
598
+
599
+ // Each fast-path completeness check mirrors _looksParsed: a bare/partial object
600
+ // with only the marker field is NOT a parsed result, so it throws the documented
601
+ // typed inspect/bad-input rather than dereferencing a missing field.
602
+ function _looksParsedCrl(o) {
603
+ return typeof o.version === "number" && o.issuer && typeof o.issuer === "object" &&
604
+ o.thisUpdate != null && Array.isArray(o.revokedCertificates) && Array.isArray(o.crlExtensions) &&
605
+ o.signatureAlgorithm && typeof o.signatureAlgorithm === "object";
606
+ }
607
+ function _looksParsedCsr(o) {
608
+ return typeof o.version === "number" && o.subject && typeof o.subject === "object" &&
609
+ o.subjectPublicKeyInfo && typeof o.subjectPublicKeyInfo === "object" &&
610
+ Array.isArray(o.attributes) && o.signatureAlgorithm && typeof o.signatureAlgorithm === "object";
611
+ }
612
+ function _looksParsedCms(o) {
613
+ if (typeof o.contentType !== "string" || typeof o.contentTypeName !== "string" || typeof o.version !== "number") return false;
614
+ // A real parse result is one of the six dispatched content types AND carries that type's
615
+ // required structural fields (_CMS_SHAPE). An unrecognized contentType, or a recognized one
616
+ // missing its fields, is not a parse result: fail closed as inspect/bad-input, not a partial render.
617
+ var shape = _CMS_SHAPE[o.contentType];
618
+ return shape ? shape(o) : false;
619
+ }
620
+ // The shared inspect coercion boundary, composed by every direct inspector AND any():
621
+ // pemLabel:null unwraps a PEM string OR a PEM-armored Buffer (a .pem read with
622
+ // fs.readFileSync) under ANY block label -- so an aliased label (PKCS7 for CMS,
623
+ // CRL for an X509 CRL) routes like any() instead of hitting a canonical-label
624
+ // mismatch -- while a raw DER Buffer / Uint8Array passes through the detached-store guard.
625
+ var _INSPECT_ENTRY = { pemLabel: null, PemError: InspectError, ErrorClass: InspectError, prefix: "inspect" };
626
+ // A parameterized _parse clone: marker field, fast-path completeness check, then
627
+ // the shared label-agnostic coercion to DER, then parse(der) inside try.
628
+ function _coerce(input, spec) {
629
+ if (input && typeof input === "object" && !Buffer.isBuffer(input) && !(input instanceof Uint8Array) && input[spec.marker] !== undefined) {
630
+ if (!spec.looksParsed(input)) throw _err("inspect/bad-input", "input has a " + spec.marker + " property but is not a complete " + spec.parsedName + " result");
631
+ return input;
632
+ }
633
+ var der;
634
+ // Compose the ONE coercion every format parser + any() use, so a PEM string and a
635
+ // PEM-armored Buffer unwrap under any label (not just the format's canonical one)
636
+ // and a detached-store Buffer fails closed here, not divergently per inspector.
637
+ try { der = pkix.coerceToDer(input, _INSPECT_ENTRY); }
638
+ catch (e) { throw _err("inspect/bad-input", "input must be a parsed " + spec.what + ", a DER Buffer, or a PEM block", e); }
639
+ try { return spec.parse(der); }
640
+ catch (e) { throw _err(spec.badCode, "input is not a well-formed " + spec.what, e); }
641
+ }
642
+ function _parseCrl(input) { return _coerce(input, { marker: "thisUpdate", looksParsed: _looksParsedCrl, parsedName: "pki.schema.crl.parse", parse: crl.parse, badCode: "inspect/bad-crl", what: "X.509 CRL" }); }
643
+ function _parseCsr(input) { return _coerce(input, { marker: "certificationRequestInfoBytes", looksParsed: _looksParsedCsr, parsedName: "pki.schema.csr.parse", parse: csr.parse, badCode: "inspect/bad-csr", what: "PKCS#10 certification request" }); }
644
+ function _parseCms(input) { return _coerce(input, { marker: "contentType", looksParsed: _looksParsedCms, parsedName: "pki.schema.cms.parse", parse: cms.parse, badCode: "inspect/bad-cms", what: "CMS message" }); }
645
+
646
+ // ---- shared attribute-value renderer (CSR attributes + CMS signed/unsigned) ---
647
+
648
+ // Attribute/extension dispatch keys on the STABLE OID, never the display name a
649
+ // pki.oid.register() override can change (the schema stores the overridden name).
650
+ var OID_EXTENSION_REQUEST = oid.byName("extensionRequest");
651
+ var OID_CONTENT_TYPE = oid.byName("contentType");
652
+ var OID_MESSAGE_DIGEST = oid.byName("messageDigest");
653
+ var OID_SIGNING_TIME = oid.byName("signingTime");
654
+
655
+ // Decode the known content-binding attribute types for display; anything else, or
656
+ // a decode failure, falls back to printable-or-hex. Never throws once parsed.
657
+ // Keyed on the attribute's OID (stable), not its display name (register-mutable).
658
+ function _attrValue(typeOid, rawDer, inner) {
659
+ try {
660
+ if (typeOid === OID_CONTENT_TYPE) { var ct = asn1.read.oid(asn1.decode(rawDer)); return inner + (oid.name(ct) || ct); }
661
+ if (typeOid === OID_MESSAGE_DIGEST) return _hexColon(asn1.read.octetString(asn1.decode(rawDer)), { wrap: 16, indent: inner.length });
662
+ if (typeOid === OID_SIGNING_TIME) return inner + _date(asn1.read.time(asn1.decode(rawDer)));
663
+ } catch (_e) { /* fall through to the raw fallback */ }
664
+ return _fallback(rawDer, inner);
665
+ }
666
+
667
+ // ---- CRL report --------------------------------------------------------------
668
+
669
+ // The CRL decoder PRE-DECODES three extension values to a JS type (not the raw
670
+ // Buffer the shared _extension consumes): cRLNumber -> BigInt, reasonCode ->
671
+ // Number, invalidityDate -> Date. Dispatch on the STABLE OID exactly as the decoder
672
+ // does (schema-crl decodeExt), NOT the display name -- pki.oid.register() can override
673
+ // a built-in name, which would skip these branches and hand _extension a non-Buffer
674
+ // value (rendering "(empty)" for the CRL number or reason). deltaCRLIndicator is a
675
+ // raw-Buffer extension the decoder does NOT pre-decode, but its value is a bare INTEGER
676
+ // (BaseCRLNumber) the operator needs in decimal to relate a delta CRL to its base -- so
677
+ // decode it here for display (best-effort: a malformed value falls through to hex).
678
+ // Delegate every other raw-value extension (AKI / IDP / freshestCRL / certificateIssuer)
679
+ // to the shared _extension verbatim.
680
+ var OID_CRL_NUMBER = oid.byName("cRLNumber");
681
+ var OID_REASON_CODE = oid.byName("reasonCode");
682
+ var OID_INVALIDITY_DATE = oid.byName("invalidityDate");
683
+ var OID_DELTA_CRL_INDICATOR = oid.byName("deltaCRLIndicator");
684
+ function _crlExtension(ext, pad) {
685
+ var label = EXT_LABEL[ext.name] || ext.name || ext.oid;
686
+ var header = pad + label + ":" + (ext.critical ? " critical" : "");
687
+ var inner = pad + " ";
688
+ if (ext.oid === OID_CRL_NUMBER && typeof ext.value === "bigint") return header + "\n" + inner + String(ext.value);
689
+ if (ext.oid === OID_REASON_CODE && typeof ext.value === "number") return header + "\n" + inner + (NAMES.CRL_REASON[ext.value] || String(ext.value));
690
+ if (ext.oid === OID_INVALIDITY_DATE && ext.value instanceof Date) return header + "\n" + inner + _date(ext.value);
691
+ if (ext.oid === OID_DELTA_CRL_INDICATOR && Buffer.isBuffer(ext.value)) {
692
+ try { return header + "\n" + inner + "BaseCRLNumber: " + String(asn1.read.integer(asn1.decode(ext.value))); }
693
+ catch (_e) { /* malformed BaseCRLNumber -> fall through to the shared _extension (hex) */ }
694
+ }
695
+ return _extension(ext, pad);
696
+ }
697
+
698
+ /**
699
+ * @primitive pki.inspect.crl
700
+ * @signature pki.inspect.crl(input) -> string
701
+ * @since 0.3.8
702
+ * @status experimental
703
+ * @spec RFC 5280
704
+ * @related pki.schema.crl.parse, pki.inspect.certificate
705
+ *
706
+ * Render a certificate revocation list as an `openssl crl -text`-familiar text
707
+ * report: issuer, Last/Next Update, the CRL extensions, each revoked entry (serial,
708
+ * revocation date, entry extensions), and the signature. `input` is a PEM string, a
709
+ * DER Buffer, or a `pki.schema.crl.parse` result; a non-CRL throws
710
+ * `inspect/bad-crl`, a wrong-type input `inspect/bad-input`. A malformed individual
711
+ * extension renders as hex rather than failing the report.
712
+ *
713
+ * @example
714
+ * pki.inspect.crl(crlDer).split("\n")[0]; // "Certificate Revocation List (CRL):"
715
+ */
716
+ function crlReport(input) {
717
+ var c = _parseCrl(input);
718
+ var L = ["Certificate Revocation List (CRL):"];
719
+ L.push(" Version " + c.version + " (0x" + (c.version - 1).toString(16) + ")");
720
+ L.push(" Signature Algorithm: " + _algName(c.signatureAlgorithm));
721
+ L.push(" Issuer: " + _dnString(c.issuer));
722
+ L.push(" Last Update: " + _date(c.thisUpdate));
723
+ L.push(" Next Update: " + (c.nextUpdate ? _date(c.nextUpdate) : "NONE"));
724
+ if (c.crlExtensions.length) {
725
+ L.push(" CRL extensions:");
726
+ c.crlExtensions.forEach(function (ext) { L.push(_crlExtension(ext, " ")); });
727
+ }
728
+ if (c.revokedCertificates.length) {
729
+ L.push("Revoked Certificates:");
730
+ c.revokedCertificates.forEach(function (e) {
731
+ L.push(" " + _serial(e, 8));
732
+ L.push(" Revocation Date: " + _date(e.revocationDate));
733
+ if ((e.crlEntryExtensions || []).length) {
734
+ L.push(" CRL entry extensions:");
735
+ e.crlEntryExtensions.forEach(function (ext) { L.push(_crlExtension(ext, " ")); });
736
+ }
737
+ });
738
+ } else {
739
+ L.push("No Revoked Certificates.");
740
+ }
741
+ L.push(" Signature Algorithm: " + _algName(c.signatureAlgorithm));
742
+ var sig = c.signatureValue && (c.signatureValue.bytes || c.signatureValue);
743
+ if (Buffer.isBuffer(sig)) { L.push(" Signature Value:"); L.push(_hexColon(sig, { wrap: 16, indent: 8 })); }
744
+ return L.join("\n") + "\n";
745
+ }
746
+
747
+ // ---- CSR report --------------------------------------------------------------
748
+
749
+ function _attribute(attr, pad) {
750
+ var inner = pad + " ";
751
+ // extensionRequest carries decoded RFC 5280 extensions (the cert-extension shape)
752
+ // -> render each through the shared _extension, identically to a certificate's.
753
+ if (attr.type === OID_EXTENSION_REQUEST && Array.isArray(attr.extensions)) {
754
+ var lines = [pad + "Requested Extensions:"];
755
+ attr.extensions.forEach(function (ext) { lines.push(_extension(ext, inner)); });
756
+ return lines.join("\n");
757
+ }
758
+ var header = pad + (attr.name || oid.name(attr.type) || attr.type) + ":";
759
+ var vals = (attr.values || []).map(function (v) { return _attrValue(attr.type, v, inner); });
760
+ return vals.length ? header + "\n" + vals.join("\n") : header;
761
+ }
762
+
763
+ /**
764
+ * @primitive pki.inspect.csr
765
+ * @signature pki.inspect.csr(input) -> string
766
+ * @since 0.3.8
767
+ * @status experimental
768
+ * @spec RFC 2986
769
+ * @related pki.schema.csr.parse, pki.inspect.certificate
770
+ *
771
+ * Render a PKCS#10 certification request as an `openssl req -text`-familiar text
772
+ * report: subject, the subject public key, the requested extensions and other
773
+ * attributes, and the signature. `input` is a PEM string, a DER Buffer, or a
774
+ * `pki.schema.csr.parse` result; a non-CSR throws `inspect/bad-csr`, a wrong-type
775
+ * input `inspect/bad-input`. Best-effort like `certificate`.
776
+ *
777
+ * @example
778
+ * pki.inspect.csr(csrDer).split("\n")[0]; // "Certificate Request:"
779
+ */
780
+ function csrReport(input) {
781
+ var c = _parseCsr(input);
782
+ var L = ["Certificate Request:", " Data:"];
783
+ L.push(" Version: " + c.version + " (0x" + (c.version - 1).toString(16) + ")");
784
+ L.push(" Subject: " + _dnString(c.subject));
785
+ L.push(" Subject Public Key Info:");
786
+ L.push(_keyBlock(c.subjectPublicKeyInfo, " "));
787
+ L.push(" Attributes:");
788
+ if (c.attributes.length) c.attributes.forEach(function (attr) { L.push(_attribute(attr, " ")); });
789
+ else L.push(" (none)");
790
+ L.push(" Signature Algorithm: " + _algName(c.signatureAlgorithm));
791
+ var sig = c.signatureValue && (c.signatureValue.bytes || c.signatureValue);
792
+ if (Buffer.isBuffer(sig)) { L.push(" Signature Value:"); L.push(_hexColon(sig, { wrap: 16, indent: 8 })); }
793
+ return L.join("\n") + "\n";
794
+ }
795
+
796
+ // ---- CMS report --------------------------------------------------------------
797
+
798
+ // Dispatch on the STABLE contentType OID, never the display name: pki.oid.register()
799
+ // lets an application override the built-in "signedData" name, and dispatching on the
800
+ // mutable name would then misroute a valid SignedData to the generic summary.
801
+ var OID_SIGNED_DATA = oid.byName("signedData");
802
+ // pki.schema.cms.parse dispatches ONLY these six RFC 5652 content types (every other
803
+ // OID throws cms/unsupported- or cms/unknown-content-type -- it never yields an object).
804
+ // Each maps to a predicate over the REQUIRED (non-OPTIONAL) fields of its top-level
805
+ // SEQUENCE, so the parsed-object fast path in _looksParsedCms accepts a genuine parse
806
+ // result but a partial/hand-built object (the type OID + name + version, none of the
807
+ // structural fields) fails closed as inspect/bad-input -- the documented boundary.
808
+ function _isObj(x) { return !!x && typeof x === "object"; }
809
+ var _CMS_SHAPE = {};
810
+ _CMS_SHAPE[OID_SIGNED_DATA] = function (o) { return Array.isArray(o.digestAlgorithms) && _isObj(o.encapContentInfo) && Array.isArray(o.signerInfos); }; // sec. 5.1
811
+ _CMS_SHAPE[oid.byName("envelopedData")] = function (o) { return Array.isArray(o.recipientInfos) && _isObj(o.encryptedContentInfo); }; // sec. 6.1
812
+ _CMS_SHAPE[oid.byName("encryptedData")] = function (o) { return _isObj(o.encryptedContentInfo); }; // sec. 8
813
+ _CMS_SHAPE[oid.byName("authData")] = function (o) { return Array.isArray(o.recipientInfos) && _isObj(o.macAlgorithm) && _isObj(o.encapContentInfo) && Buffer.isBuffer(o.mac); }; // sec. 9.1
814
+ _CMS_SHAPE[oid.byName("authEnvelopedData")] = function (o) { return Array.isArray(o.recipientInfos) && _isObj(o.encryptedContentInfo) && Buffer.isBuffer(o.mac); }; // RFC 5083 (build surfaces encryptedContentInfo)
815
+ _CMS_SHAPE[oid.byName("compressedData")] = function (o) { return _isObj(o.compressionAlgorithm) && _isObj(o.encapContentInfo); }; // RFC 3274
816
+
817
+ // Coverage residuals in the report assemblers -- verified-hard-to-reach, not gaps:
818
+ // * The `x.bytes || x` / `oid.name(o) || o` / `a.name || oid.name || a.type` fallbacks are
819
+ // belts for shapes the strict parsers never produce (a parsed CSR always carries
820
+ // signatureValue.bytes; a decoded algorithm always names its OID) -- they mirror
821
+ // certificate()'s own defensive fallbacks.
822
+ // * The embedded-CRL delegation and the CONTEXT-tagged embedded-element summary need a CMS
823
+ // carrying a crls element or an attribute-certificate CHOICE alternative, and the
824
+ // AuthenticatedData macAlgorithm / countersignature-unsignedAttrs arms need CMS shapes the
825
+ // toolkit's own producers (cms.sign / encrypt / compress) do not emit; every such arm is
826
+ // driven best-effort (never throws) and its structural sibling (embedded Certificate,
827
+ // envelopedData recipientInfo, CompressedData compressionAlgorithm, signed attributes) is
828
+ // covered.
829
+
830
+ // An embedded certificate/CRL element {bytes,tagClass,tagNumber}: a UNIVERSAL
831
+ // SEQUENCE is a real Certificate/CertificateList -> delegate to the full sub-report
832
+ // (guarded; a one-line summary on any failure); a CONTEXT-tagged CHOICE alternative
833
+ // (attribute certificate / other) renders a one-line tag+size summary, never parsed.
834
+ function _cmsEmbedded(kind, el, pad) {
835
+ if (el.tagClass === "universal") {
836
+ try {
837
+ var sub = (kind === "CRL") ? crlReport(el.bytes) : certificate(el.bytes);
838
+ return pad + kind + ":\n" + sub.replace(/\n$/, "").split("\n").map(function (l) { return pad + " " + l; }).join("\n");
839
+ } catch (_e) { /* fall through to the summary */ }
840
+ }
841
+ return pad + kind + " [" + el.tagClass + " " + el.tagNumber + "] (" + el.bytes.length + " bytes)";
842
+ }
843
+
844
+ function _signerInfoAttrs(title, attrs, pad) {
845
+ var lines = [pad + title + ":"];
846
+ var inner = pad + " ";
847
+ attrs.forEach(function (a) {
848
+ var vals = (a.values || []).map(function (v) { return _attrValue(a.type, v, inner + " "); });
849
+ lines.push(inner + (a.name || oid.name(a.type) || a.type) + ":");
850
+ vals.forEach(function (v) { lines.push(v); });
851
+ });
852
+ return lines.join("\n");
853
+ }
854
+
855
+ function _signerInfo(si, pad) {
856
+ var inner = pad + " ";
857
+ var L = [pad + "SignerInfo:", inner + "Version: " + si.version];
858
+ if (si.sid && si.sid.serialNumberHex !== undefined) {
859
+ L.push(inner + "Issuer: " + _dnString(si.sid.issuer));
860
+ L.push(inner + _serial(si.sid, pad.length + 8));
861
+ } else if (si.sid && Buffer.isBuffer(si.sid.subjectKeyIdentifier)) {
862
+ L.push(inner + "Subject Key Identifier: " + _hexColon(si.sid.subjectKeyIdentifier, {}));
863
+ }
864
+ L.push(inner + "Digest Algorithm: " + _algName(si.digestAlgorithm));
865
+ if (si.signedAttrs && si.signedAttrs.length) L.push(_signerInfoAttrs("Signed Attributes", si.signedAttrs, inner));
866
+ L.push(inner + "Signature Algorithm: " + _algName(si.signatureAlgorithm));
867
+ if (si.unsignedAttrs && si.unsignedAttrs.length) L.push(_signerInfoAttrs("Unsigned Attributes", si.unsignedAttrs, inner));
868
+ if (Buffer.isBuffer(si.signature)) { L.push(inner + "Signature Value:"); L.push(_hexColon(si.signature, { wrap: 16, indent: pad.length + 8 })); }
869
+ return L.join("\n");
870
+ }
871
+
872
+ /**
873
+ * @primitive pki.inspect.cms
874
+ * @signature pki.inspect.cms(input) -> string
875
+ * @since 0.3.8
876
+ * @status experimental
877
+ * @spec RFC 5652
878
+ * @related pki.schema.cms.parse, pki.inspect.certificate
879
+ *
880
+ * Render a CMS message as an `openssl cms -cmsout -print`-familiar text report. A
881
+ * SignedData shows the content type, digest algorithms, encapsulated content,
882
+ * embedded certificates/CRLs, and each SignerInfo (signer identifier, algorithms,
883
+ * signed/unsigned attributes, signature); a non-SignedData ContentInfo renders a
884
+ * stable top-field summary. `input` is a PEM string, a DER Buffer, or a
885
+ * `pki.schema.cms.parse` result; a non-CMS throws `inspect/bad-cms`. Best-effort.
886
+ *
887
+ * @example
888
+ * pki.inspect.cms(cmsDer).split("\n")[0]; // "CMS ContentInfo:"
889
+ */
890
+ // A ContentInfo whose content type pki.schema.cms.parse does not dispatch (id-data,
891
+ // digestedData, ...) is a VALID CMS the parser defers, not a malformed message -- render an
892
+ // outer-only summary (the named content type) rather than failing the report.
893
+ function _cmsOuterSummary(input) {
894
+ var ct = null;
895
+ try {
896
+ var der = pkix.coerceToDer(input, _INSPECT_ENTRY);
897
+ ct = asn1.read.oid(asn1.decode(der).children[0]);
898
+ } catch (_e) { /* best-effort: name unknown */ }
899
+ return "CMS ContentInfo:\n Content Type: " + (ct ? (oid.name(ct) || ct) + " (" + ct + ")" : "unknown") +
900
+ "\n (content type not further parsed; outer ContentInfo only)\n";
901
+ }
902
+
903
+ function cmsReport(input) {
904
+ var m;
905
+ try { m = _parseCms(input); }
906
+ catch (e) {
907
+ // A ContentInfo whose contentType the parser does not dispatch is a valid CMS, not a malformed
908
+ // message: a known-but-deferred type (cms/unsupported-content-type, e.g. id-data) or a private/
909
+ // unregistered OID (cms/unknown-content-type). Either renders the outer-only summary.
910
+ var cc = e && e.cause && e.cause.code;
911
+ if (e && e.code === "inspect/bad-cms" && (cc === "cms/unsupported-content-type" || cc === "cms/unknown-content-type")) return _cmsOuterSummary(input);
912
+ throw e;
913
+ }
914
+ var L = ["CMS ContentInfo:"];
915
+ L.push(" Content Type: " + (m.contentTypeName || oid.name(m.contentType) || m.contentType) + " (" + m.contentType + ")");
916
+ if (m.contentType === OID_SIGNED_DATA) {
917
+ L.push(" SignedData:");
918
+ L.push(" Version: " + m.version);
919
+ L.push(" Digest Algorithms:");
920
+ (m.digestAlgorithms || []).forEach(function (a) { L.push(" " + _algName(a)); });
921
+ if (m.encapContentInfo) {
922
+ L.push(" Encapsulated Content Info:");
923
+ L.push(" Content Type: " + (oid.name(m.encapContentInfo.eContentType) || m.encapContentInfo.eContentType) + " (" + m.encapContentInfo.eContentType + ")");
924
+ L.push(" " + (m.encapContentInfo.eContent == null ? "<no content (detached)>" : (m.encapContentInfo.eContent.length + " content byte(s)")));
925
+ }
926
+ (m.certificates || []).forEach(function (el) { L.push(_cmsEmbedded("Certificate", el, " ")); });
927
+ (m.crls || []).forEach(function (el) { L.push(_cmsEmbedded("CRL", el, " ")); });
928
+ (m.signerInfos || []).forEach(function (si) { L.push(_signerInfo(si, " ")); });
929
+ } else {
930
+ // Non-SignedData: a stable top-field summary (no plaintext to show); never throws.
931
+ L.push(" " + (m.contentTypeName || "content") + ":");
932
+ if (m.version != null) L.push(" Version: " + m.version);
933
+ (m.recipientInfos || []).forEach(function (ri) { L.push(" RecipientInfo: " + (ri.type || "?") + (ri.ridType ? " (" + ri.ridType + ")" : "")); });
934
+ if (m.encryptedContentInfo) {
935
+ L.push(" Content Type: " + (oid.name(m.encryptedContentInfo.contentType) || m.encryptedContentInfo.contentType));
936
+ if (m.encryptedContentInfo.contentEncryptionAlgorithm) L.push(" Content Encryption Algorithm: " + _algName(m.encryptedContentInfo.contentEncryptionAlgorithm));
937
+ }
938
+ if (m.macAlgorithm) L.push(" MAC Algorithm: " + _algName(m.macAlgorithm));
939
+ if (m.compressionAlgorithm) L.push(" Compression Algorithm: " + _algName(m.compressionAlgorithm));
940
+ }
941
+ return L.join("\n") + "\n";
942
+ }
943
+
944
+ // ---- unified detect-and-dispatch ---------------------------------------------
945
+
946
+ var _INSPECT_BY_FORMAT = { x509: certificate, crl: crlReport, csr: csrReport, cms: cmsReport };
947
+
948
+ /**
949
+ * @primitive pki.inspect.any
950
+ * @signature pki.inspect.any(input) -> string
951
+ * @since 0.3.8
952
+ * @status experimental
953
+ * @spec RFC 5280
954
+ * @related pki.schema.detectFormat, pki.inspect.certificate
955
+ *
956
+ * Detect which PKI format `input` (a PEM string or DER Buffer) encodes and render
957
+ * it with the matching report -- the inspect analogue of `pki.schema.parse`. Routes
958
+ * a certificate / CRL / CSR / CMS to `certificate` / `crl` / `csr` / `cms`; a
959
+ * detected but out-of-scope format (OCSP, TSP, PKCS#8/#12, CRMF, CMP, ...) throws
960
+ * `inspect/unsupported-format` naming it, and an unrecognized input
961
+ * `inspect/bad-input`.
962
+ *
963
+ * @example
964
+ * pki.inspect.any(der); // routes to the right report by detected format
965
+ */
966
+ function any(input) {
967
+ // Unwrap to DER ONCE (any PEM label), detect from the DER, then route the DER Buffer -- the
968
+ // renderer parses a Buffer directly and never re-applies its own strict PEM label.
969
+ var der, fmt;
970
+ try {
971
+ der = pkix.coerceToDer(input, _INSPECT_ENTRY);
972
+ fmt = schemaAll.detectFormat(der); // decodes the root -- a non-DER Buffer throws here, not at coerce
973
+ } catch (e) { throw _err("inspect/bad-input", "input is not a decodable DER Buffer or PEM string", e); }
974
+ if (fmt === null) throw _err("inspect/bad-input", "input does not match any registered PKI format");
975
+ var render = _INSPECT_BY_FORMAT[fmt];
976
+ if (!render) throw _err("inspect/unsupported-format", "inspect does not support the detected format \"" + fmt + "\" (supported: certificate, crl, csr, cms)");
977
+ return render(der);
978
+ }
979
+
593
980
  module.exports = {
594
981
  certificate: certificate,
982
+ crl: crlReport,
983
+ csr: csrReport,
984
+ cms: cmsReport,
985
+ any: any,
595
986
  // The extension names certificate() renders to their decoded values (vs the raw
596
987
  // hex fallback). The inspect test asserts this covers every extension the shared
597
988
  // decoders decode, so a newly-decodable extension cannot silently hex-dump.
@@ -39,6 +39,7 @@ var x509 = require("./schema-x509");
39
39
  var crl = require("./schema-crl");
40
40
  var ocsp = require("./schema-ocsp");
41
41
  var ocspVerify = require("./ocsp-verify");
42
+ var crlVerify = require("./crl-verify");
42
43
  var guard = require("./guard-all");
43
44
  var constants = require("./constants");
44
45
  var validator = require("./validator-all");
@@ -1762,7 +1763,7 @@ function crlChecker(crls) {
1762
1763
  // replayed old CRL must not read "good". Treat it as unusable.
1763
1764
  if (!theCrl.nextUpdate || theCrl.nextUpdate < time) continue; // stale / no bound
1764
1765
 
1765
- var sigOk = await verifyCrlSignature(theCrl, issuer);
1766
+ var sigOk = await crlVerify.verifyCrlSignature(theCrl, issuer.workingPublicKey);
1766
1767
  if (!sigOk) continue; // unverifiable -> not authoritative
1767
1768
 
1768
1769
  // The CRL is now authoritative + current + verified. An authoritative
@@ -1853,10 +1854,10 @@ function _verifyWithSpki(sigAlg, rawSig, spkiBytes, tbsBytes) {
1853
1854
  }).then(function (ok) { return ok === true; }, function () { return false; });
1854
1855
  }
1855
1856
 
1856
- function verifyCrlSignature(theCrl, issuer) {
1857
- if (!guard.crypto.isOctetAligned(theCrl.signatureValue)) return Promise.resolve(false); // non-octet-aligned signature
1858
- return _verifyWithSpki(theCrl.signatureAlgorithm, theCrl.signatureValue.bytes, issuer.workingPublicKey, theCrl.tbsBytes);
1859
- }
1857
+ // Inject this validator's signature engine into the shared internal crl-verify seam, so pki.crl.verify and
1858
+ // pki.path.crlChecker both route through this ONE engine (no second, weaker CRL verifier) without exposing
1859
+ // the seam on the public pki.path surface.
1860
+ crlVerify.setEngine(_verifyWithSpki);
1860
1861
 
1861
1862
  // ---- the OCSP revocation checker (RFC 6960) -------------------------------
1862
1863
 
package/lib/schema-all.js CHANGED
@@ -290,6 +290,34 @@ function parse(input) {
290
290
  throw new SchemaError("schema/unknown-format", "input does not match any registered PKI format (" + all().join(", ") + ")");
291
291
  }
292
292
 
293
+ /**
294
+ * @primitive pki.schema.detectFormat
295
+ * @signature pki.schema.detectFormat(input) -> string | null
296
+ * @since 0.3.8
297
+ * @status experimental
298
+ * @spec RFC 5280
299
+ * @related pki.schema.parse, pki.schema.all
300
+ *
301
+ * Detect which registered PKI format `input` (a DER `Buffer` or PEM string)
302
+ * encodes and return its name -- one of `pki.schema.all()` -- WITHOUT parsing it,
303
+ * or `null` when the decoded bytes match no registered format. This is the
304
+ * detection half of `pki.schema.parse`, running the same authoritative `FORMATS`
305
+ * ordering, exposed for a caller (e.g. `pki.inspect.any`) that needs the format
306
+ * name rather than the parsed result. Input that does not decode as DER throws the
307
+ * same coercion / decode error `parse` throws.
308
+ *
309
+ * @example
310
+ * pki.schema.detectFormat(der); // "x509" | "crl" | "csr" | "cms" | ... | null
311
+ */
312
+ function detectFormat(input) {
313
+ var der = pkix.coerceToDer(input, ENTRY);
314
+ var root = pkix.decodeRoot(der, ENTRY);
315
+ for (var i = 0; i < FORMATS.length; i++) {
316
+ if (FORMATS[i].detect(root)) return FORMATS[i].name;
317
+ }
318
+ return null;
319
+ }
320
+
293
321
  // Curated public surface: each format exposes only its operator primitives. The
294
322
  // `matches` detector is internal dispatch infrastructure (used by FORMATS above),
295
323
  // not an operator API, so it is NOT re-exported here.
@@ -313,4 +341,5 @@ module.exports = {
313
341
  smime: { parseSigningCertificate: smime.parseSigningCertificate, parseSigningCertificateV2: smime.parseSigningCertificateV2, parseSmimeCapabilities: smime.parseSmimeCapabilities, decodeAttribute: smime.decodeAttribute },
314
342
  all: all,
315
343
  parse: parse,
344
+ detectFormat: detectFormat,
316
345
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
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",
@@ -72,7 +72,7 @@
72
72
  "check:vendor-currency": "node scripts/check-vendor-currency.js"
73
73
  },
74
74
  "devDependencies": {
75
- "c8": "11.0.0",
75
+ "c8": "12.0.0",
76
76
  "esbuild": "0.28.1",
77
77
  "eslint": "10.7.0"
78
78
  }
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:dc01f613-ee62-4b7d-b2d5-42e3bb2f5560",
5
+ "serialNumber": "urn:uuid:c2eb240a-1337-4a5a-862e-43dad1701426",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-18T01:11:02.665Z",
8
+ "timestamp": "2026-07-23T16:52:13.594Z",
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.3.7",
22
+ "bom-ref": "@blamejs/pki@0.3.9",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.3.7",
25
+ "version": "0.3.9",
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.3.7",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.3.9",
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.3.7",
57
+ "ref": "@blamejs/pki@0.3.9",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]