@blamejs/pki 0.9.5 → 0.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +7 -6
- package/bin/pki.js +110 -34
- package/lib/acme.js +91 -46
- package/lib/attrcert-sign.js +4 -3
- package/lib/cmp-build.js +16 -6
- package/lib/cmp-session.js +19 -3
- package/lib/cmp-verify.js +25 -2
- package/lib/cms-compress.js +3 -3
- package/lib/cms-digest.js +3 -3
- package/lib/cms-verify.js +18 -8
- package/lib/constants.js +1 -0
- package/lib/crl-sign.js +3 -3
- package/lib/csr-sign.js +1 -1
- package/lib/ct.js +16 -3
- package/lib/est.js +44 -29
- package/lib/guard-all.js +2 -0
- package/lib/guard-distrust.js +224 -0
- package/lib/guard-json.js +1 -1
- package/lib/http-transport.js +299 -36
- package/lib/jose.js +1 -1
- package/lib/lint.js +67 -7
- package/lib/ocsp-verify.js +9 -3
- package/lib/ocsp.js +8 -4
- package/lib/oid.js +4 -0
- package/lib/path-validate.js +112 -16
- package/lib/possession.js +6 -2
- package/lib/related-cert.js +2 -2
- package/lib/revocation-fetch.js +38 -9
- package/lib/scep.js +28 -13
- package/lib/schema-pkix.js +3 -3
- package/lib/sigstore.js +11 -6
- package/lib/smime.js +18 -13
- package/lib/tls-cert-compress.js +6 -5
- package/lib/trust.js +121 -20
- package/lib/trustanchor-build.js +16 -1
- package/lib/tsp-sign.js +7 -1
- package/lib/webauthn-mds.js +29 -8
- package/lib/webauthn.js +46 -14
- package/lib/x509-sign.js +8 -4
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,43 @@ 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.9.6 — 2026-10-21
|
|
8
|
+
|
|
9
|
+
Refuse a distrusted CA key wherever it appears, and trust only the platform store under useSystemStore.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `pki.trust.spkiDigest(cert)`: the SHA-256 of a certificate's DER SubjectPublicKeyInfo in canonical form (an uncompressed EC point with its named curve, RSA with NULL parameters) as lower-case hex, the entry `distrustedKeys` takes. For nearly every certificate this is the digest of the bytes it carries and the SPKI SHA-256 root programs publish. `cert` is DER, PEM or a parsed certificate.
|
|
14
|
+
- `lint/key-compromise/small-ec-private-key` (CABF TLS BR 6.1.1.3 (4)) reports a P-256, P-384, P-521 or secp256k1 public key whose private scalar d, or n-d, is below 256. It runs in the default rule set for `pki.lint.certificate` and `pki.lint.csr` and under the new `key-compromise` profile, and it reports every one of the sixteen eMudhra certificates. The first check on each curve builds a table once per process (about 140 ms for P-384 and 320 ms for P-521 on Node 26).
|
|
15
|
+
- `pki verify` and `pki fetch` take `--distrusted-key <hex>[,<hex>...]`, repeatable.
|
|
16
|
+
- `pki.path.validate` results carry `distrustedKeysApplied`.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- `tls.useSystemStore` no longer includes Node's compiled-in roots. A caller that relied on them, for example in a container with no CA bundle installed, now gets `transport/no-trust-anchors`: install the host's CA bundle, or pass the roots you mean to trust as `tls.anchors` (`require("tls").rootCertificates` for Node's set).
|
|
21
|
+
- `pki.trust.systemAnchors` no longer returns a certificate that is not self-issued as an anchor. Pass `result.intermediates` to `pki.path.build` as `candidates` to build a chain through them.
|
|
22
|
+
- An unknown name in a `tls` bag, a `Proxy` as the `tls` bag of the ACME, EST, SCEP or CMP client, and an unknown flag or `--name=value` on the `pki` command are now refused. Spell the option as documented, pass a plain object, and give a flag's value as the next argument. A malformed `tls.distrustedKeys` in `pki.transport.https(defaults)` is refused when the transport is built.
|
|
23
|
+
- The `domain/reason` error codes are a frozen contract, recorded in the api-snapshot and checked on every change: a code is not renamed or removed in a patch release. The seven ACME resource codes (`acme/bad-directory`, `acme/bad-order`, and the rest) are unchanged in value.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- A request to a URL that names no port now sends `Host: name` rather than `Host: name:80` or `Host: name:443`. Some servers answer the port form with an error: emSign's CRL and OCSP endpoints returned HTTP 503, so `pki.path.fetchingChecker` could not determine the status of the revoked eMudhra intermediates, and with `softFail` it waived them.
|
|
28
|
+
- An undetermined `pki.path.fetchingChecker` verdict names the OCSP failure and the CRL failure (`OCSP: ...; CRL: ...`). It previously kept only the CRL's reason.
|
|
29
|
+
- `pki.transport.https` reads a `tls.anchors` Buffer that holds PEM text with a comment ahead of the armor as PEM. It previously read such a buffer as DER, and every connection under that anchor failed with `transport/server-auth-failed`.
|
|
30
|
+
- A `null` option is read as the omitted option it stands for wherever an omitted option selects a documented default. `pki.est.serverkeygen` no longer refuses `requestedEncryption: null`, `pki.cms.compress` compresses at the default level for `level: null`, `pki.cms.digest` uses sha256 for `digestAlgorithm: null`, `pki.acme.downloadCertificate` no longer reports `boundToKey: true` for binding material given as `null`, `pki.path.validate` applies its documented limits for `maxPolicyNodes: null` and `minRsaModulusBits: null`, and `pki.x509.extension` accepts `critical: null` as non-critical.
|
|
31
|
+
|
|
32
|
+
### Security
|
|
33
|
+
|
|
34
|
+
- `distrustedKeys` refuses a CA key by the SHA-256 of its DER SubjectPublicKeyInfo (64 hex characters or 32 bytes; `pki.trust.spkiDigest(cert)` computes one). `pki.path.validate` and `pki.path.build` fail a path that carries a listed key as the anchor, an intermediate or the target, with `path/distrusted-key`, and ask no revocation status at or below it. One entry covers every certificate for the key, so a cross-certificate under another root is refused too, including one that encodes the key another way: a compressed or hybrid EC point, explicit curve parameters, curve parameters inherited from the issuer, the id-ecDH or id-ecMQV identifier, RSA parameters without NULL, or an RSA key under the RSASSA-PSS or RSAES-OAEP identifier. A composite key is its own key, so listing one of its component keys does not list it. To refuse the eMudhra keys, list the eight SPKI digests published with the incident.
|
|
35
|
+
- The verbs that validate a chain for the caller take the same option and apply it to every chain they build: `pki.cms.verify` (signer and timestamp chains), `pki.smime.verify`, `pki.tsp.verify`, `pki.cmp.verify`, `pki.cmp.openKeyPackage`, `pki.cmp.session` (response signer, issued certificate, key generation authority, root key update), `pki.possession.verifyRequest`, `pki.webauthn.verify` (pinned roots, metadata and SafetyNet roots), `pki.webauthn.verifyMetadataBlob` and `pki.sigstore.verifyBundle` (Fulcio and timestamp chains). `pki.ocsp.verify`, `pki.path.verifyOcspResponse`, `pki.path.ocspChecker` and `pki.path.fetchingChecker` do not accept an answer signed by a listed key, and a listed issuing CA authorizes no responder, its own or a delegate it certified. `pki.path.crlChecker` does not take a CRL whose issuer key is listed. A malformed list is the verb's own `<domain>/bad-input`, including on the verbs that otherwise report a chain failure as untrusted.
|
|
36
|
+
- `tls.distrustedKeys` on `pki.transport.https` refuses a server whose verified chain carries a listed key, at any position including the root, with `transport/server-key-distrusted`, on the direct connection, `pki.transport.peerChain` and the origin inside a CONNECT tunnel. The transport checks the chain as a path of its own (each link signed by the certificate above it, each certificate in its validity period, each issuer a CA, the top a configured anchor's key), and it also follows every branch up through the configured anchors where an anchor that is not a root (an intermediate or cross-certificate, as the Windows platform store holds) issues a certificate on that chain. A server that sends another certificate with a listed intermediate's name, or a second path to a root the client does not trust, therefore cannot hide the chain the session was verified through. Node does not expose the chain OpenSSL verified, so while a list is set a session is refused when any chain it could have been verified through carries a listed key or fails these checks. An HTTPS proxy whose chain carries a key listed in `proxy.tls.distrustedKeys` or in the origin's list is refused with `transport/proxy-key-distrusted`. A request's list is added to the transport default's, which is read when the transport is built. The ACME, EST, SCEP, CMP and CT clients carry the list to every request, including a cross-origin redirect, under their own prefix (`acme/server-key-distrusted`, and so on).
|
|
37
|
+
- A `tls` bag naming an option the transport does not read, such as a misspelled `distrustKeys`, is refused with `<domain>/bad-input` rather than ignored, on `pki.transport.https`, the ACME, EST, SCEP, CMP and CT clients, `pki.path.fetchingChecker` and the AIA fetch of `pki.path.build`. The `pki` command refuses a flag a verb does not take, and the `--name=value` form, so `pki verify ... --distrusted-keys <hex>` is a usage error rather than a verification with no list.
|
|
38
|
+
- The `pki.est` verbs read `opts.tls` when they are called. They read it after an internal await, so a caller that changed the bag right after the call changed the policy the request was sent with. `pki.scep.enroll`, `getCert` and `getCrl` now carry a `tls` setting the bag inherits, as the other SCEP verbs already did, and `pki.est.fullcmc` carries a `tls` taken from a defaults object.
|
|
39
|
+
- `tls.useSystemStore` trusts `tls.getCACertificates("system")` alone. It previously added Node's compiled-in roots, so a root that the host's CA package had removed was still trusted, and on Windows a chain that Node's own default verification rejected was accepted. When the platform store is empty, a request that names no `tls.anchors` is now refused with `transport/no-trust-anchors`; before, the empty list left the connection on Node's default store, which includes `NODE_EXTRA_CA_CERTS`. The same applies to `proxy.tls`. `pki fetch --system` reads the platform store only.
|
|
40
|
+
- `pki.trust.systemAnchors` returns only self-issued certificates as anchors. A certificate whose issuer name differs from its subject name, such as the Windows Intermediate Certification Authorities cache in a `certutil` export, is listed in `skipped` as `trust/not-self-issued` and returned in a new `intermediates` list for `pki.path.build` to use as candidates. It previously became an anchor that was never revocation-checked. `systemAnchors({ distrustedKeys })` leaves out a listed certificate, and `allowNodeBundle` now refuses a value that is not a boolean with `trust/bad-input` rather than reading it as off.
|
|
41
|
+
- A `null` is refused where an omitted option means no restriction at all, because reading it as omitted dropped the check the caller asked for: `pki.path.validate` `requiredEku` and `checkPurpose`; `pki.webauthn.verify` `allowedAlgorithms`, `rootCertificates`, `metadata` and `tpmPolicy`; the metadata verb's `metadata`, `previousNo` and `statusPolicy`; `pki.trust` `purpose`; `pki.est` `expectedRecipientKeyId`; `pki.attrcert` `revocationStatus`; `pki.path.fetchingChecker` `allowPlaintextHttp`; and `pki.tls.decompressCertificate` `allowedAlgorithms`. A `null` `rootCertificates` or `metadata` on `pki.webauthn.verify` returned `attestationVerified: true` with `anchoredTo: null`. Each is now its module's `<domain>/bad-input`.
|
|
42
|
+
- Every `expected*` and `require*` option on `pki.webauthn.verify`, and every member of a trust anchor, refuses a `null` rather than reading it as omitted. A `null` `expectedChallenge` would otherwise have verified the ceremony without the challenge comparison.
|
|
43
|
+
|
|
7
44
|
## v0.9.5 — 2026-10-12
|
|
8
45
|
|
|
9
46
|
Refuse a non-boolean switch option instead of reading it as the state the caller did not ask for.
|
package/README.md
CHANGED
|
@@ -266,14 +266,14 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
|
|
|
266
266
|
| `pki.schema.csrattrs` | EST CSR Attributes (`CsrAttrs`, RFC 8951 §3.5 / RFC 9908) — the `AttrOrOID` items a server sends to shape an enrollment: bare OIDs, attributes with raw values, and decoded views of the RFC 9908 meaningful types (extension requests, EC and RSA key-type conventions, the certification-request-info template). Unknown types are surfaced raw; structure and the RFC 9908 semantic MUSTs are fail-closed — `parse` |
|
|
267
267
|
| `pki.est` | Enrollment over Secure Transport (RFC 7030 / 8951 / 9908 / 7616). The client verbs `cacerts`, `simpleenroll`, `simplereenroll`, `serverkeygen`, `csrattrs`, and `fullcmc` drive the RFC 7030 flow over `pki.transport` (inject your own via `opts.transport`, or take the fail-closed default): HTTPS only, an explicit trust anchor required, same-origin redirects followed while a downgrade or loop is refused, a 202 Retry-After surfaced but never slept, HTTP Basic or Digest (RFC 7616, SHA-256 / SHA-512-256; MD5 and no-qop refused by default) answered only after the server is authenticated, and the issued certificate chosen by public-key match. `serverkeygen` requests a server-generated key, cleartext or an opaque CMS EnvelopedData, with encryption bound to the CSR's key-identifier attribute over a confidentiality-bearing cipher. `csrattrs` fetches the CA's RFC 9908 attributes policy. For §3.5 channel binding, `simpleenroll` and `simplereenroll` accept a CSR builder `(tls) -> csr` instead of a CSR: it runs after the handshake with that connection's `tlsUnique`, so the request is signed over the session it is sent on, and it re-runs on any new connection a redirect or auth retry opens, recreating the CSR for that session (§3.2.1). A builder alongside Digest authentication is refused, since that header is computed over the body before the handshake. `fullcmc` (§4.3) carries a CMC Full PKI Request and reduces the CA's answer through `pki.cmc.verify` to one terminal outcome, refusing a response that fails to echo the transaction and nonce the request carried, or that covers a key the request never asked for; a certs-only reply to a bound request is refused rather than read as an issuance. Under the verbs sit the transport-agnostic codecs they compose: the RFC 8951 base64 transfer codec (which ignores Content-Transfer-Encoding), the `multipart/mixed` splitter, the certs-only and serverkeygen response validators over CMS, the enroll-attribute builders, and the HTTP response classifier — `transferDecode`/`transferEncode`, `parseCertsOnly`, `splitMultipartMixed`, `parseServerKeygenResponse`, `findIssuedCert`, `classifyResponse`, `paths`, and the builders |
|
|
268
268
|
| `pki.scep` | SCEP pkiMessage codec (RFC 8894), the client side of Simple Certificate Enrollment. `build(spec)` assembles a request: a `PKCSReq` or `RenewalReq` wraps a PKCS#10 certification request, whose own proof-of-possession it verifies first; a `CertPoll` wraps an IssuerAndSubject naming the issuing CA and the request subject; and a `GetCert` or `GetCRL` wraps an IssuerAndSerialNumber naming a certificate by its issuer and a positive serial of at most 20 octets. It encrypts the messageData to the CA as the inner EnvelopedData (`pkcsPKIEnvelope`, AES-128-CBC), and signs that under the transaction attributes RFC 8894 §3.2 defines, a messageType, a caller-unique transactionId, and a 16-byte senderNonce. The recipient CA certificate must assert `keyEncipherment`. `build` also issues the CA-side `CertRep` response (§3.3.2): a SUCCESS envelopes a certs-only SignedData of the issued certificate (or the CRL for a GetCRL) to the requester and signs it under the CA key, while a FAILURE (carrying a `failInfo`) or a PENDING omits the pkcsPKIEnvelope and is signed detached, each echoing the request's senderNonce as `recipientNonce`. `parse(bytes, opts?)` reads a `PKCSReq`, `RenewalReq`, `CertPoll`, `GetCert`, `GetCRL`, or `CertRep` back (a messageType outside the RFC 8894 registry is refused): it verifies the outer signature first and refuses a message whose signature does not check, so the transaction attributes it returns come only from a verified signer, never alongside a false verdict. Because a valid signature only proves self-consistency with the embedded certificate, pass `opts.signerCert` to authenticate a CA response against the CA certificate you hold (a public-key mismatch is refused, `signerAuthenticated` reports the result); a caller that omits it gets a crypto-only verdict and must authenticate the surfaced `signerCert` itself. It maps the messageType, pkiStatus, failInfo, and the nonces from their RFC 8894 enumerants and refuses an unknown one; given `recipientKey` it decrypts the `pkcsPKIEnvelope` to the messageData, and given `expectedSenderNonce` it refuses a response whose recipientNonce does not echo it. It reads a CA's CertRep in full. `getCACaps(baseUrl, opts?)`, `getCACert(baseUrl, opts?)`, `getNextCACert(baseUrl, opts)`, `enroll(baseUrl, opts)`, `renew(baseUrl, opts)`, `getCert(baseUrl, opts)`, and `getCrl(baseUrl, opts)` carry that codec to a live CA over HTTP (RFC 8894 §4) through an injectable transport (`pki.transport.https` by default): `getCACaps` reports the CA's advertised capabilities and never lowers the client's algorithm choice on the unauthenticated response, `getCACert` pins the CA certificate to an out-of-band `expectedFingerprint` a returned certificate must match, `getNextCACert` fetches the CA's next (rollover) certificate and verifies the SignedData response against the current CA certificate (`caCertificate`) before returning it, refusing a response signed by any other key, and `enroll` and `renew` POST a PKCSReq or RenewalReq, authenticate the CertRep against the CA certificate and its nonce echo, select the issued certificate by public-key match, throw on a FAILURE with the CA's failInfo, and poll a PENDING response with CertPoll requests until the CA answers or a bounded poll-count and total-wait budget is spent. `getCert` and `getCrl` retrieve an already-issued certificate or a CA CRL by issuer and serial number, authenticated the same way; RFC 8894 §2.6, §2.7, and §3.3.4 recommend these queries only where HTTP certificate-store access (RFC 4387), LDAP, or a CRL distribution point is unavailable, so prefer those where a CA offers them. A PKIOperation goes over HTTP POST by default; `httpMethod: "GET"` carries it base64-encoded in the URL query for a legacy CA that accepts only GET (RFC 8894 §4.1). Every verb reaches an https CA through a forward proxy with `opts.proxy = { url, auth?, tls? }`: a `CONNECT` tunnel is opened to the proxy and the origin TLS is negotiated inside it, so the proxy carries the enrollment without being able to read it or substitute the origin certificate. Basic proxy authentication (RFC 7617) is sent only over an `https://` proxy, where it rides the authenticated TLS-to-proxy channel; an `http://` proxy is tunnel-only, and credentials over a plaintext proxy are refused rather than exposed (see `pki.transport`) — `build`, `parse`, `parseCapabilities`, `getCACaps`, `getCACert`, `getNextCACert`, `enroll`, `renew`, `getCert`, `getCrl` |
|
|
269
|
-
| `pki.transport` | The shared, fail-closed `node:https` transport the enrollment clients drive. `pki.transport.https(defaults)` returns a `transport(request) → { status, headers, body, tls }`, where `tls` carries the negotiated `protocol`, `cipher`, raw `peerCertificate`, and one channel binding per TLS version: on TLS 1.2 the RFC 5929 `tlsUnique` that `pki.est.challengePasswordFromTlsUnique` links to an EST enrollment (RFC 7030 §3.5), and on TLS 1.3 the RFC 9266 `tlsExporter` (32 bytes under the label `EXPORTER-Channel-Binding`, zero-length context). Each is `null` on the version that does not define it, and `tlsExporter` is withheld below TLS 1.3 because RFC 9266 §2 also requires the extended master secret of RFC 7627 (obsoleted by RFC 9846), which Node gives no way to confirm. Since `tlsUnique` on the response arrives after the body was sent, a request `body` may instead be a function `(tls) -> bytes` the transport invokes after the handshake and before it writes the body, so the caller builds a channel-bound CSR from `tlsUnique` on the same connection it posts over. It may return a promise, so the CSR can be signed with `pki.csr.sign` while the connection is held open; a callback that throws, rejects, or returns anything other than bytes or a string fails the request closed rather than posting an empty enrollment. This is the toolkit's only socket choke point: an explicit trust anchor (or an opt-in to the
|
|
269
|
+
| `pki.transport` | The shared, fail-closed `node:https` transport the enrollment clients drive. `pki.transport.https(defaults)` returns a `transport(request) → { status, headers, body, tls }`, where `tls` carries the negotiated `protocol`, `cipher`, raw `peerCertificate`, and one channel binding per TLS version: on TLS 1.2 the RFC 5929 `tlsUnique` that `pki.est.challengePasswordFromTlsUnique` links to an EST enrollment (RFC 7030 §3.5), and on TLS 1.3 the RFC 9266 `tlsExporter` (32 bytes under the label `EXPORTER-Channel-Binding`, zero-length context). Each is `null` on the version that does not define it, and `tlsExporter` is withheld below TLS 1.3 because RFC 9266 §2 also requires the extended master secret of RFC 7627 (obsoleted by RFC 9846), which Node gives no way to confirm. Since `tlsUnique` on the response arrives after the body was sent, a request `body` may instead be a function `(tls) -> bytes` the transport invokes after the handshake and before it writes the body, so the caller builds a channel-bound CSR from `tlsUnique` on the same connection it posts over. It may return a promise, so the CSR can be signed with `pki.csr.sign` while the connection is held open; a callback that throws, rejects, or returns anything other than bytes or a string fails the request closed rather than posting an empty enrollment. This is the toolkit's only socket choke point: an explicit trust anchor (or an opt-in to the platform certificate store, refused when that store is empty) is required, `rejectUnauthorized` is always on, TLS is floored at 1.2, the response body is capped while it streams, and a stalled socket times out. `tls.distrustedKeys` refuses a server, or an https proxy, whose verified chain carries a listed key. The EST, ACME, and CMP clients reuse it verbatim. A `request.proxy = { url, auth?, tls? }` reaches the origin through a forward proxy: a `CONNECT` tunnel is opened to the proxy and the origin TLS is negotiated inside it under the identical origin trust policy, so the proxy cannot read the origin's encrypted session or substitute an authenticated origin certificate. Proxy credentials are sent only over an `https://` proxy, whose own certificate is verified against `proxy.tls`, so they ride an authenticated channel; a plaintext `http://` proxy is tunnel-only and carrying `auth` on one is refused (`proxy-auth-requires-tls`) rather than exposing the credentials to the proxy hop. `auth.scheme` is `"basic"` (RFC 7617), which sends its credential on the first CONNECT, or `"digest"` (RFC 7616), which answers the proxy's `Proxy-Authenticate` challenge on a `407` by hashing the method `CONNECT` over the authority-form target, and answers exactly once, so a proxy repeating its challenge has rejected the credential rather than obtained another attempt. Digest runs the policy the origin verbs apply, refusing MD5 and a challenge carrying no `qop` unless `auth.allowMD5` / `auth.allowLegacyQop` says otherwise. A malformed option, a failed CONNECT, an unverified proxy certificate, or an unaccepted credential fails closed. The SCEP, EST, ACME, and CMP clients each thread it through as `opts.proxy`. If you inject your own transport, return `tls` too: `pki.est.serverkeygen` asserts the negotiated cipher can protect the delivered private key, and a transport that reports no cipher is trusted rather than refused, so omitting the field silently skips that check. `pki.transport.peerChain(request, defaults?)` negotiates TLS and returns that same channel without sending a request, plus `peerChain`: the certificates the endpoint presented as DER, leaf first, walked up from the peer certificate to the one that issues itself. Reading a certificate off an endpoint through the request path would reach the application behind it; this does not. It prepares the connection through the same code the request path uses, so the anchors, the SNI, the version floor, the identity hook, the private-address block and the https-only rule are one policy rather than a second copy of one, and a configured proxy is refused rather than bypassed. A chain it returns is not a validated one: the handshake checked that it builds to a configured anchor and matches the name in the URL, and checked no revocation status and no policy — `https`, `peerChain` |
|
|
270
270
|
| `pki.jose` | Flattened JWS (RFC 7515) and JWK thumbprints (RFC 7638). `sign` and `verify` run a Flattened JWS against declarative profiles (ACME outer, EAB inner, keyChange inner) that carry the required and forbidden header rules as data. `base64url` is the strict RFC 4648 §5 codec, rejecting padding, non-alphabet characters, and non-canonical trailing bits. `parseJson` is a bounded reader that refuses duplicate members at any depth. `thumbprint` is the RFC 7638 / 8037 / 9964 canonical digest. The algorithm registry binds each `alg` to its key type (ES/RS/PS/EdDSA/ML-DSA), leaving no code path for `alg:none`, an RS256→HS256 key confusion, or an all-zero ECDSA signature; `assertPublicJwk` refuses a JWK carrying private material, so an exported private key is never published; `verify` holds a JWS's own embedded `jwk` to the same rule (RFC 7515 §4.1.3), so a message carrying a private key in its header is refused rather than returned in the verdict. `opts.key` names the key a message must be signed under and governs: where the profile also permits an embedded header `jwk`, the two must be the same key — compared as RFC 7638 thumbprints, so member order cannot make equal keys differ — and a disagreement is refused rather than resolved in the message's favor. `keySource` reports which key answered, since a signature checked against a key you named is a different claim from one checked against the key the message carried — `sign`, `verify`, `base64url`, `parseJson`, `thumbprint`, `assertPublicJwk` |
|
|
271
271
|
| `pki.acme` | ACME (RFC 8555 / 8737 / 8738 / 9773). `client(directoryUrl, opts)` is a stateful client driving a live CA directory over `pki.transport`: `newAccount`, `newOrder`, `newAuthz`, `getOrder`, `getAuthorization`, `getChallenge`, `respondToChallenge`, `finalize`, `pollOrder`, `pollAuthorization`, and `downloadCertificate` walk the issuance flow, with `newAuthz` pre-authorizing a single identifier (§7.4.1) and `downloadCertificate` binding the issued certificate to the order — its key, its identifiers, or both; at least one is required by default and the result reports `boundToKey` / `boundToIdentifiers` — before choosing among alternate chains (`Link rel="alternate"`, §7.4.2, via a `selectChain` predicate bounded by `maxAlternates`). `revokeCert` (account-key or certificate-key signed), `keyChange`, `updateAccount` (change the account contacts, §7.3.2), `listOrders` (the account's orders list, following the §7.1.2.1 `Link rel="next"` pagination bounded by a page cap and confined to the orders origin), `deactivateAccount`, `deactivateAuthorization`, `renewalInfo` (ARI), `renewalWindow` (the RFC 9773 §4.2/4.3 renewal decision), and `scheduleRenewal` (the §4.1 auto-sleeping renewal loop that sleeps until the selected instant and refetches until it is time to renew, stopping at the certificate's notAfter) complete the lifecycle. Every URL is HTTPS only, an explicit trust anchor is required, each request carries a fresh single-use nonce with a bounded badNonce retry (and, when `opts.resignKeys` supplies caller-approved alternative account-key algorithms, a bounded RFC 8555 §6.2 badSignatureAlgorithm re-sign that the CA's advertised list can only filter, never widen, so it cannot force a weaker algorithm), reads are POST-as-GET, polling is bounded and sleeps on a Retry-After via an injectable sleeper capped by a poll count and a total-wait budget, and every response body is size-capped. The transport is injectable via `opts.transport`. Over the message layer it composes resource-object validators (closed status enums, conditional-required fields, unknown fields ignored), the three §7.1.6 state machines, the request builders (newAccount with External Account Binding, newOrder with `replaces`, finalize with a CSR identifier-set match and account-key-reuse rejection, challenge responses, deactivation, revokeCert in both key modes, the keyChange nested JWS, POST-as-GET), the http-01 / dns-01 / tls-alpn-01 challenge computations, the dns and ip identifier validators, and the ARI certID with serial sign-padding preserved — `client`, `validate`, `identify`, `assertTransition`, the builders, `keyAuthorization`, `http01`, `dns01`, `tlsAlpn01Extension`, `verifyTlsAlpn01`, `ariCertId` |
|
|
272
272
|
| `pki.schema.smime` | S/MIME ESS signed-attribute values (RFC 5035 / RFC 8551). `parseSigningCertificate` and `parseSigningCertificateV2` bind a signature to its signing certificate (cert hash, hash algorithm, issuer `GeneralNames` and serial); `parseSmimeCapabilities` decodes the ordered capability list; `decodeAttribute` dispatches a CMS attribute by OID, enforcing the single-value rule and deferring on unknown types. A companion decoder for CMS signed attributes rather than an auto-routed format — `parseSigningCertificate`, `parseSigningCertificateV2`, `parseSmimeCapabilities`, `decodeAttribute` |
|
|
273
273
|
| `pki.cmc` | Build and interpret CMC messages (RFC 5272, obsoleted by RFC 10002). `build(spec, signer)` assembles a Full PKI Request across all three request arms (PKCS#10, CRMF, other) and carries it either way RFC 5272 §3.2 allows: `{ cert, key }` signs it, while `{ mac: { identifier, secret } }` authenticates it under a shared secret for a client that holds no key yet, using the PasswordRecipientInfo that section requires and deriving the key from the identifier followed by the secret. A key that cannot sign for itself proves possession another way: `spec.popChallenge` answers the authority's encrypted challenge (RFC 5272 §6.7), decrypting the proof value, confirming it hashes to the witness the challenge carries, and returning the MAC the authority asked for, with `cmc/pop-failed` when the value is not the one issued. A signed request goes through `pki.cms.sign` under `id-cct-PKIData`. A PKCS#10 (`tcr`) request is verified against its own proof-of-possession before signing, so a request whose self-signature does not verify under its subject public key is refused rather than signed into a message a CA would reject. Body-part identifiers are unique across the whole message and never the reserved 0; a caller's clash is refused rather than renumbered, since a control may already reference it. An Identity Proof V2 witness is computed over the `reqSequence` bytes exactly as emitted (§6.2.1 step 1) rather than a re-serialization, a POP Link Witness is emitted only alongside the POP Link Random control §6.3.1.1 requires beside it, and a renewal carries neither Identification nor Identity Proof in either version. `verify(response, sent)` takes what the CA returned plus the state the client retained and reduces it to one terminal outcome: `issued`, `pending`, `confirm-required`, `pop-required`, or `rejected`. It binds the exchange first — Transaction Identifier, the Sender and Recipient Nonce echo compared in constant time and by full value so a truncation cannot match, and the Data Return echo — with each check applying only if the client sent that half, and, once sent, an absent or differing echo being a refusal, which is the replay defense. `bodyPartIDs` extends the same rule to what the response is about: a status reporting on a body part the request never sent is refused, which the transaction and nonce cannot catch, because a server can echo both correctly while answering about a different message. Several status controls are permitted and the worst governs, so a failure cannot hide behind an earlier success; the absence of any status control is success, per §6.1.2. The carrier's signature must verify (§3.2.1.3.4): a conforming SignedData carries its own signer certificate, so the ordinary build-then-verify flow needs nothing extra and the verdict reports `signatureVerified: true`. Where the signer is found nowhere the posture is fail-closed with a named opt-out — supply `certs` with the responder's certificate, or `allowUnverified: true`, in which case the verdict reports `signatureVerified: false`. Doing neither is refused, the opt-out never excuses a signature that is present and wrong, and a carrier with no signer at all is refused outright. The response's own `cmsSequence` and `otherMsgs` come back raw, since a request whose only arm was the other-message form has no certificate to return and §4.1 puts its answer there. Nothing is trusted: issued certificates are read from the CMS certificate bag (§4.2) and surfaced raw for `pki.path.validate`, and a Publish Trust Anchors control is surfaced with `trusted: false` rather than added to a store — `build`, `verify` |
|
|
274
274
|
| `pki.schema.cmc` | Decode CMC messages (RFC 5272 as updated by RFC 6402, the pair obsoleted by RFC 10002 / 10003 / 10004): a Full PKI Request (`PKIData`) or Full PKI Response (`PKIResponse`) riding inside a CMS SignedData, reached by the encapsulated content type (`id-cct-PKIData` / `id-cct-PKIResponse`). Controls are surfaced in wire order with their values raw, so an unrecognized one is data rather than a fault. Tagged requests decode across all three arms. The status verdicts (`CMCStatusInfo` v1 and `CMCStatusInfoV2`) are collected as an ordered list, and the RFC 6402 two-module `OtherStatusInfo` ambiguity — `pendInfo` and `extendedFailInfo` are both untagged SEQUENCEs in the 1988 module, told apart only by their first element — is resolved by inspection and refused when it cannot be told apart. Body-part identity is unique across the whole message rather than per sequence, 0 is reserved as the reference to the enclosing PKIData, and the `reqSequence` bytes are surfaced exactly as they appeared so an Identity Proof witness is computed over the wire bytes. A companion decoder for CMS content rather than an auto-routed format — `parse`, `parsePkiData`, `parsePkiResponse` |
|
|
275
275
|
| `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk`, `encode`, `embeddedDer`, and the schema combinators |
|
|
276
|
-
| `pki.path` | Certification-path validation (RFC 5280 §6). `validate` runs the §6.1 state machine over an ordered path and a trust anchor: signature chaining across RSA, ECDSA, EdDSA, ML-DSA, SLH-DSA and hybrid composite ML-DSA (a composite is accepted only when both its post-quantum and traditional components verify), validity windows, name chaining, basic constraints and path length, key usage, name constraints, the RFC 7633 §4.2.2 constraint that a certificate issued under a signing certificate advertising TLS features must offer the same set or a superset, and the certificate policies, processed as the RFC 9618 `valid_policy_graph` in place of the RFC 5280 §6.1 policy tree. The graph holds one node per depth and policy identifier and gives it several parents where the tree made a separate node under each, so its size is linear in the policies and mappings on the path rather than exponential in the path length: a six-certificate chain whose policies map to each other needs 25 nodes where the tree needed 5461, and chains the tree could not hold inside `maxPolicyNodes` now validate. It returns a structured verdict with per-check reason codes and enforces a `pki.trust` anchor's per-purpose distrust-after dates and delegator purposes through `checkPurpose` — an anchor that carries either one with no purpose named to select by is refused as a configuration fault rather than validated as though it carried none. The verdict says what was established. `revocationChecked` takes the weakest outcome on the path: `false` with no checker supplied, `"determined"` when every certificate got an explicit good or revoked answer, `"waived"` when `softFail` turned an undetermined one into a pass, and `"undetermined"` when one could not be answered at all — which includes a checker that throws, a fault `softFail` does not waive because `softFail` opts into an undetermined answer, not into a broken checker. `anchorConstraints` names the purpose the anchor was judged under and whether each of its two constraints applied. `crlChecker` supplies CRL-based revocation, covering partitioned and sharded CRLs, whose §6.3.3 Distribution Point ↔ IDP correspondence lets corresponding shards accumulate reason coverage until all eight revocation reasons are covered, and delta CRLs, merged onto the complete CRL they may be combined with (§5.2.4) so a held certificate its delta releases reads good, while a delta that merges with nothing still reports what it lists and still withholds good. `ocspChecker` supplies OCSP-based revocation (RFC 6960: CertID binding, responder authorization, signature, currency) over the same pluggable hook. `build(leaf, opts)` is the discovering complement (RFC 4158): from a leaf, an untrusted pool of candidate CA certificates, and a trust store, it finds the ordered leaf-to-anchor path `validate` accepts, using name chaining plus the RFC 4158 §3.5 sort hints (an AKI/SKI match, an anchor-adjacent issuer, CA plus keyCertSign, validity at the check time — ordering hints, never filters), a depth-first search with backtracking so the first accepted path wins, and a bounded search (chain-length cap, candidate-expansion cap, identity-tuple visited set) so a cross-certificate cycle or Bridge-CA fan-out terminates deterministically. Every accept flows through `validate`, and its verdict is cross-checked against `openssl verify`. Opt-in AIA `caIssuers` fetching (`opts.fetchAia: true`) discovers a missing intermediate from a certificate's Authority Information Access URL (§4.2.2.1) over `pki.transport`, triggered only on a pool miss and bounded against SSRF and amplification: HTTPS only, a total fetch budget that caps fetching silently rather than throwing, a per-cert URL cap, a build-wide URL dedupe, a response-size and certificate-count cap, no redirect following, and every fault a silent skip. The TLS trust (`opts.tls`) stays distinct from the PKI `trustAnchors`, and every fetched certificate remains untrusted pool material that still flows through `validate` (never a trust anchor). It is off by default, so the default build is byte-identical offline. An `opts.direction` of `"reverse"` searches from a trust anchor toward the leaf (RFC 4158 §3.1), narrower when the anchor set is small and the leaf's issuer options fan out, and `"auto"` picks the direction by first-hop fan-out; reverse building reuses the same pool, visited set, caps, and `validate` gate, so the direction changes only search order, and the anchor is excluded from the returned path. Reverse building is pool-only and does not combine with `fetchAia`. When `opts.userInitialPolicySet` names acceptable certificate-policy OIDs and more than one path validates, build returns the accepted path whose user-constrained policy set best satisfies that set (RFC 4158 §4) rather than the first accepted path, the ranking bounded by the same candidate cap and a malformed set refused with `path/bad-input`; an `anyPolicy` entry is unconstrained and returns the first accepted path. In a mesh of cross-certified domains, where many candidates chain by name to the same place, a candidate is ranked by how many leading relative distinguished names it shares with the name the search is heading for (§3.5.16 and the §3.5.19 reverse method), so a certificate in the target's own domain is tried before one from a cross-certified domain, and forward building sorts a candidate under every candidate that can complete the path when its path length constraint cannot cover the certificates already below it (§3.5.7), an ordering that outranks the other hints rather than competing with them. No candidate is removed and an unbounded search still reaches every path the pool holds; a bounded one spends `maxCandidatesConsidered` on whichever branches the order steers it to. `fetchingChecker(opts)` is the checker that goes and gets what it needs: it reads the cRLDistributionPoints and id-ad-ocsp locations out of the certificate and asks for them over `opts.transport`, OCSP first and a CRL after (RFC 5019 §3.1). It is off unless an operator builds it and passes it, so the default validation touches no network, and it runs during `validate` and not during `build`, because §3.2 has a status check requested only after the chain signatures are validated. A distribution point is fetched only as a `fullName` carrying a URI whose scheme this client speaks; ldap, ftp, a `nameRelativeToCRLIssuer` and a point naming a cRLIssuer and no location are counted as skips, so a certificate naming nothing fetchable reads as that rather than as a failed fetch. Nothing is trusted by having been fetched: a CRL goes to `crlChecker` and a response to `verifyOcspResponse`, and the structure a body parses as is decided by which extension named the URL, so a Content-Type mismatch is reported and never selects a parser. Every fault is an undetermined status carrying its reason and `check` throws nothing, which is what leaves `softFail` able to waive one. Plaintext `http` is accepted by default, these being signed objects the toolkit verifies, with `allowPlaintextHttp: false` to narrow it; the SSRF guards hold either way (no private, loopback or link-local destination, no redirect following, a response-size cap, a per-certificate URL cap, a total fetch count, and a wall-clock deadline across the validation). A verified response is cached in process on its CertID and responder URL and a CRL on its request URL and issuer name, with reuse decided from the signed thisUpdate and nextUpdate alone. Pure and re-entrant — `validate`, `build`, `crlChecker`, `ocspChecker`, `fetchingChecker` |
|
|
276
|
+
| `pki.path` | Certification-path validation (RFC 5280 §6). `validate` runs the §6.1 state machine over an ordered path and a trust anchor: signature chaining across RSA, ECDSA, EdDSA, ML-DSA, SLH-DSA and hybrid composite ML-DSA (a composite is accepted only when both its post-quantum and traditional components verify), validity windows, name chaining, basic constraints and path length, key usage, name constraints, the RFC 7633 §4.2.2 constraint that a certificate issued under a signing certificate advertising TLS features must offer the same set or a superset, and the certificate policies, processed as the RFC 9618 `valid_policy_graph` in place of the RFC 5280 §6.1 policy tree. The graph holds one node per depth and policy identifier and gives it several parents where the tree made a separate node under each, so its size is linear in the policies and mappings on the path rather than exponential in the path length: a six-certificate chain whose policies map to each other needs 25 nodes where the tree needed 5461, and chains the tree could not hold inside `maxPolicyNodes` now validate. It returns a structured verdict with per-check reason codes and enforces a `pki.trust` anchor's per-purpose distrust-after dates and delegator purposes through `checkPurpose` — an anchor that carries either one with no purpose named to select by is refused as a configuration fault rather than validated as though it carried none. The verdict says what was established. `revocationChecked` takes the weakest outcome on the path: `false` with no checker supplied, `"determined"` when every certificate got an explicit good or revoked answer, `"waived"` when `softFail` turned an undetermined one into a pass, and `"undetermined"` when one could not be answered at all — which includes a checker that throws, a fault `softFail` does not waive because `softFail` opts into an undetermined answer, not into a broken checker. `anchorConstraints` names the purpose the anchor was judged under and whether each of its two constraints applied. `crlChecker` supplies CRL-based revocation, covering partitioned and sharded CRLs, whose §6.3.3 Distribution Point ↔ IDP correspondence lets corresponding shards accumulate reason coverage until all eight revocation reasons are covered, and delta CRLs, merged onto the complete CRL they may be combined with (§5.2.4) so a held certificate its delta releases reads good, while a delta that merges with nothing still reports what it lists and still withholds good. `ocspChecker` supplies OCSP-based revocation (RFC 6960: CertID binding, responder authorization, signature, currency) over the same pluggable hook. `build(leaf, opts)` is the discovering complement (RFC 4158): from a leaf, an untrusted pool of candidate CA certificates, and a trust store, it finds the ordered leaf-to-anchor path `validate` accepts, using name chaining plus the RFC 4158 §3.5 sort hints (an AKI/SKI match, an anchor-adjacent issuer, CA plus keyCertSign, validity at the check time — ordering hints, never filters), a depth-first search with backtracking so the first accepted path wins, and a bounded search (chain-length cap, candidate-expansion cap, identity-tuple visited set) so a cross-certificate cycle or Bridge-CA fan-out terminates deterministically. Every accept flows through `validate`, and its verdict is cross-checked against `openssl verify`. Opt-in AIA `caIssuers` fetching (`opts.fetchAia: true`) discovers a missing intermediate from a certificate's Authority Information Access URL (§4.2.2.1) over `pki.transport`, triggered only on a pool miss and bounded against SSRF and amplification: HTTPS only, a total fetch budget that caps fetching silently rather than throwing, a per-cert URL cap, a build-wide URL dedupe, a response-size and certificate-count cap, no redirect following, and every fault a silent skip. The TLS trust (`opts.tls`) stays distinct from the PKI `trustAnchors`, and every fetched certificate remains untrusted pool material that still flows through `validate` (never a trust anchor). It is off by default, so the default build is byte-identical offline. An `opts.direction` of `"reverse"` searches from a trust anchor toward the leaf (RFC 4158 §3.1), narrower when the anchor set is small and the leaf's issuer options fan out, and `"auto"` picks the direction by first-hop fan-out; reverse building reuses the same pool, visited set, caps, and `validate` gate, so the direction changes only search order, and the anchor is excluded from the returned path. Reverse building is pool-only and does not combine with `fetchAia`. When `opts.userInitialPolicySet` names acceptable certificate-policy OIDs and more than one path validates, build returns the accepted path whose user-constrained policy set best satisfies that set (RFC 4158 §4) rather than the first accepted path, the ranking bounded by the same candidate cap and a malformed set refused with `path/bad-input`; an `anyPolicy` entry is unconstrained and returns the first accepted path. In a mesh of cross-certified domains, where many candidates chain by name to the same place, a candidate is ranked by how many leading relative distinguished names it shares with the name the search is heading for (§3.5.16 and the §3.5.19 reverse method), so a certificate in the target's own domain is tried before one from a cross-certified domain, and forward building sorts a candidate under every candidate that can complete the path when its path length constraint cannot cover the certificates already below it (§3.5.7), an ordering that outranks the other hints rather than competing with them. No candidate is removed and an unbounded search still reaches every path the pool holds; a bounded one spends `maxCandidatesConsidered` on whichever branches the order steers it to. `fetchingChecker(opts)` is the checker that goes and gets what it needs: it reads the cRLDistributionPoints and id-ad-ocsp locations out of the certificate and asks for them over `opts.transport`, OCSP first and a CRL after (RFC 5019 §3.1). It is off unless an operator builds it and passes it, so the default validation touches no network, and it runs during `validate` and not during `build`, because §3.2 has a status check requested only after the chain signatures are validated. A distribution point is fetched only as a `fullName` carrying a URI whose scheme this client speaks; ldap, ftp, a `nameRelativeToCRLIssuer` and a point naming a cRLIssuer and no location are counted as skips, so a certificate naming nothing fetchable reads as that rather than as a failed fetch. Nothing is trusted by having been fetched: a CRL goes to `crlChecker` and a response to `verifyOcspResponse`, and the structure a body parses as is decided by which extension named the URL, so a Content-Type mismatch is reported and never selects a parser. Every fault is an undetermined status carrying its reason and `check` throws nothing, which is what leaves `softFail` able to waive one. Plaintext `http` is accepted by default, these being signed objects the toolkit verifies, with `allowPlaintextHttp: false` to narrow it; the SSRF guards hold either way (no private, loopback or link-local destination, no redirect following, a response-size cap, a per-certificate URL cap, a total fetch count, and a wall-clock deadline across the validation). A verified response is cached in process on its CertID and responder URL and a CRL on its request URL and issuer name, with reuse decided from the signed thisUpdate and nextUpdate alone. `distrustedKeys` lists SubjectPublicKeyInfo SHA-256 digests that `validate`, `build` and the OCSP responder checks refuse wherever they appear on a path, as the anchor, an intermediate, the target or a delegated responder (`path/distrusted-key`); one entry covers every cross-certificate issued for the key. Pure and re-entrant — `validate`, `build`, `crlChecker`, `ocspChecker`, `fetchingChecker` |
|
|
277
277
|
| `pki.identity` | Service identity (RFC 9525, which obsoletes RFC 6125). `match(cert, references, opts)` says whether a certificate presents a name the client was trying to reach, which is a separate answer from whether it chains: §1.2 has an application need both, and `pki.path.validate` takes `opts.identity` to run them together and report each. A reference identity is a domain name (DNS-ID), a textual IP address or a 4- or 16-octet `BufferSource` (IP-ID), `{ type: "srv", service: "_imap", value: "example.com" }` (SRV-ID, RFC 4985), or `{ type: "uri", scheme: "https", value: "www.example.com" }` (URI-ID). A record carries its service type and its domain together, so no comparison can pair the service of one reference with the domain of another (§6.5). The subject `commonName` is never read, under any option or fallback, including when the certificate carries no subjectAltName at all (§2). Names are compared label by label under an ASCII-only fold with one trailing dot normalized on both sides; an A-label is compared as the ASCII it is and never decoded, so a reference carrying a character outside ASCII is refused rather than guessed at, this toolkit shipping no IDNA implementation. An IP-ID is compared octet for octet against an `iPAddress` entry, never against a `dNSName` holding the same address as text, and no prefix or mask applies (§6.2, §6.4). Wildcards are on and `opts.wildcards: false` turns them off, which §3 requires stating either way: one wildcard, the whole of the left-most label, reaching exactly one reference label, and a presented name failing those rules is ignored with the search continuing rather than refusing the certificate (§6.3). A wildcard spanning an administrative boundary such as `*.co.uk` is out of scope as it is in §7.1, since no public-suffix list ships here. The verdict names which reference matched, so a caller can use it as the validated identity (§6.6), and lists every entry that was skipped with the reason — `match` |
|
|
278
278
|
| `pki.x509` | Certificate issuance (RFC 5280 §4). `sign(spec, issuer, opts)` builds and signs a certificate from a `spec` of subject (a common-name string, an array of RDNs, or raw Name DER), the public key being certified, the validity window, an optional serial, and an optional `extensions` object. `randomSerial()` draws the same 20-octet CSPRNG serial the signer uses when the spec names none, so a serial can be reserved or logged before the certificate is issued. `parseDn(dn)` reads a distinguished-name string into `{ rdns, dn, bytes }`, the inverse of the `dn` every parsed name carries, with `bytes` being the raw Name DER `spec.subject` and `spec.issuer` take. A name string that opens as a distinguished name is refused rather than certified as one commonName holding the whole text, so `subject: "CN=Example CA, O=Example"` names the two forms that do work instead of issuing `CN=CN=Example CA\, O=Example`; a common name that contains an `=` further along, such as `Example CA (v=2)`, is unaffected. The `issuer` is a key alone (self-signed: issuer equals subject, signed with that key), a name plus public key plus key, or an issuing certificate plus key. The signature algorithm is resolved from the signing key through the shared registry, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA P-256/384/521, Ed25519, Ed448, ML-DSA-44/65/87, the twelve SLH-DSA sets, and the composite arms all issue without a per-algorithm branch. It encodes basic constraints, key usage, extended key usage, subject and authority key identifiers (the SKI derived by SHA-1 of the subject key; the AKI optionally naming the issuing certificate by name and serial; both emitted without being named, the SKI on every certificate and the AKI on every certificate that is not self-signed, which is what RFC 5280 §4.2.1.1 and §4.2.1.2 place on the issuing CA, and `false` declines each only where the RFC allows the omission), subject alternative names, certificate policies with their RFC 5280 section 4.2.1.4 qualifiers (a CPS pointer and a user notice), name constraints, authority information access, CRL distribution points, the freshest CRL, issuer alternative names, the policy machinery path validation acts on (policy constraints, inhibit anyPolicy, policy mappings), the RFC 3739 qualified-certificate statements including the ETSI EN 319 412-5 set, the RFC 6962 certificate-transparency pair (the precertificate poison and the embedded SCT list), the Active Directory Certificate Services enrollment extensions (certificate template, legacy template name, CA version, previous-CA-certificate hash, application policies), subject information access, subject directory attributes, the RFC 6960 OCSP no-check responder marker, and the RFC 7633 TLS Feature extension that declares must-staple, whose values are TLS ExtensionType numbers or the registry names for them, from the spec. That is every extension the shared certificate-extension decoders read, so any certificate `pki.lint.certificate` and `pki.inspect` can read in full the toolkit can also issue; any other extension is taken as pre-encoded DER. `extensions.nameConstraints` takes `{ permitted, excluded }` lists of GeneralName-form objects, restricting what the CA being issued may itself issue; each base is held to the rule for a constraint base, and the extension is emitted critical as §4.2.1.10 requires. It derives the version from the field set and enforces the serial bounds, the UTCTime/GeneralizedTime cutover, the DER default omissions, and the CA cross-field rules; a violation throws a typed `CertificateError`. Before the signing key is reached it lints the certificate the spec describes against the RFC 5280 profile and refuses an error-severity finding with `x509/profile-violation` naming the rule and its clause, which is what refuses an EC subject key asserting `keyEncipherment` or `dataEncipherment` (RFC 8813 §3), so a non-conforming certificate is never signed rather than signed and discarded; `opts.profile` names a further profile from `pki.lint.profiles()` to hold it to at build time, and `opts.profile: "none"` runs no rules and emits what the spec describes. `pki.csr.sign` and `pki.crl.sign` carry the same gate against RFC 2986 and the RFC 5280 §5 profile. Returns DER, or a PEM `CERTIFICATE` with `opts.pem`. Every arm is independently verified by OpenSSL. `extension(name, value, opts)` encodes one Extension DER from the same plain value form, for the pre-encoded array form on the certificate, CSR, and CRL signers: the criticality follows what the signer emits, `opts.critical` reaches only the extensions whose criticality is the issuer's choice, and an unregistered dotted OID takes its extnValue as bytes. Parsing stays at `pki.schema.x509.parse` — `sign`, `randomSerial`, `extension`, `parseDn` |
|
|
279
279
|
| `pki.csr` | PKCS#10 certification-request issuance (RFC 2986 / RFC 2985). `sign(spec, key, opts)` builds and signs a `CertificationRequest` from a `spec` of subject (which may be empty), the public key being certified, an optional `extensionRequest` carrying the requested v3 extensions a CA copies into the issued certificate (subject alternative names, key usage, extended key usage, basic constraints, certificate policies, subject key identifier, the RFC 3739 qualified-certificate statements, the Microsoft certificate template and application policies, subject information access, subject directory attributes, or an array of pre-encoded Extension DER; an extension the issuing CA assigns, such as the authority key identifier or the OCSP no-check marker, is refused by name rather than requested), and an optional `challengePassword`. `key` (or `{ key }`) is the subject's own private key: the request is self-signed to prove possession of the private half of `subjectPublicKey`, and that proof is verified before the request is returned, which is what `openssl req -verify` checks. The signature algorithm is resolved from the subject key, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. Returns DER, or a PEM `CERTIFICATE REQUEST` with `opts.pem`; malformed input throws a typed `CsrError`. `verify(request)` asks the same question of a request someone else produced, over its exact signed `certificationRequestInfo` bytes under the `subjectPKInfo` inside them, through the one path-validation signature engine. It answers `{ verified, subject, subjectPublicKeyInfo, attributes, certificationRequestInfoBytes }`, all re-derived from the signed bytes, so a CA issues from the fields the signature covers rather than from the object it passed in. A `verified: true` says the producer held that private key and that the subject and requested extensions are the ones they signed; who they are is the enrollment protocol's question, not this one. Parsing stays at `pki.schema.csr.parse` — `sign`, `verify` |
|
|
@@ -295,7 +295,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
|
|
|
295
295
|
| `pki.possession` | Statement of possession (RFC 9883): how a key that cannot sign gets certified. PKCS#10 proves possession of the key being certified by signing the request with it, so an ML-KEM, X25519 or X448 key could not be certified through a certification request at all. RFC 9883 signs the request with a different key the requester already holds a certificate for, and names that certificate in an attribute. `pki.csr.sign` takes `spec.privateKeyPossessionStatement` and signs with the key given, which is the one case where it accepts a signing key that is not the subject's and a subject key that cannot sign; both stay refused for a request that declares no statement. `includeCertificate: false` emits the compact form the RFC allows, where the CA looks the certificate up. `pki.crmf.build` takes the same structure as `controls.statementOfPossession`, which the RFC gives the attribute's own OID. `pki.possession.verifyRequest` is the CA's side and enforces the two MUSTs: the request's signature is validated with the public key from the signature certificate, and that certificate's certification path is validated, so `opts.trustAnchors` is required and the verb throws without it rather than returning a verdict that skipped a MUST. The two name comparisons the RFC states as SHOULDs are reported and not enforced, because each ends in a question about certificate policy that no library can answer: `subjectMatches` and `subjectAltNamesMatch` carry the comparison and `valid` is `false` with a reason naming the decision. `requestsSignatureCertificate` reports the prohibition on using the attribute to obtain a signature certificate, which `pki.lint.csr` with `profile: "rfc9883"` grades as an error. A statement carrying a certificate its `signer` does not name is refused in both directions — `parse`, `verifyRequest` |
|
|
296
296
|
| `pki.altSig` | Alternative signatures (ITU-T X.509 (2019) clause 9.8), the shape widely called Catalyst: one certificate carrying two signatures, so a PKI migrating between algorithm sets does not have to reissue to every relying party at once. A relying party that has not migrated reads the native signature and ignores the extensions; one that has reads the alternative signature instead. `pki.x509.sign` takes `issuer.altKey` beside the issuing key and `extensions.subjectAltPublicKeyInfo` for the subject's alternative key, and emits all three extensions: clause 7.2.2 requires the alternative signature to be generated first, over the structure without it, and the native signature then to cover it, so the ordering is not the caller's to get right. `pki.crl.sign` takes `issuer.altKey` for the same mechanism on a CRL, which clause 7.10.3 gives the same procedure; `subjectAltPublicKeyInfo` is certificate-only. `signedData` builds the bytes that signature covers: the `PreTBSCertificate` of `draft-truskovsky-lamps-pq-hybrid-x509` section 4, which is the `tbsCertificate` without its `signature` field and without the `altSignatureValue` extension, and the `PreTBSCertList` of section 5 for a CRL. Those are the bytes Bouncy Castle emits and checks, so an alternative signature made here verifies there and one made there verifies here. Clause 7.2.2 requires a verifier to rebuild that encoding, and this is the one place the toolkit re-encodes what it parsed: every retained component keeps the bytes it had and only the two SEQUENCE headers whose lengths change are recomputed, so nothing is re-serialized from a decoded model. `verify` checks the signature under the issuer's alternative public key, which `subjectAltPublicKey` reads out of the issuer's own certificate; `SubjectAltPublicKeyInfo` has the same encoding as `SubjectPublicKeyInfo`, so that value is an SPKI and needs no conversion. The algorithm is not derived: the structure states it in `altSignatureAlgorithm`, and that stated algorithm is bound to the key through the engine path validation uses, so a key that does not match it resolves `false` rather than being read under some other algorithm; a structure stating no alternative signature throws instead, there being nothing to check. `pki.lint.certificate` with `profile: "x509-altsig"` grades the clause 9.8.3 pairing and the clause 7.2.2 full set as errors and a critical extension as a warning, criticality being stated only in a NOTE — `signedData`, `verify`, `subjectAltPublicKey` |
|
|
297
297
|
| `pki.relatedCert` | Related certificates (RFC 9763): the two structures that let one subject hold a classical certificate and a post-quantum one and have each say the other exists. `requestSignedData` builds the bytes a requester signs to prove it holds the certificate it names, which §3.1 fixes as the DER `IssuerAndSerialNumber` followed by the DER `BinaryTime` and nothing else; `locationInfo` travels in the attribute outside those bytes, so a verified proof says nothing about where the certificate can be fetched. `pki.csr.sign` takes the whole attribute as `relatedCertRequest` and encodes those two fields from that same function, so the bytes signed and the bytes emitted cannot drift apart; its `locationInfo` is a `SEQUENCE OF` IA5String URIs rather than one URI, and it keeps the order the requester wrote. `verifyRequest` checks the proof through the engine the certificate path uses, so the algorithm it derives is bound to the key, and returns `{ valid, verified, signerMaySign, signatureAlgorithm }` with a `code` and `reason` when it did not pass. The two checks are reported apart because they fail for different reasons: `verified` is the proof's own signature, and `signerMaySign` is whether the named certificate's keyUsage authorizes that signature at all, which §4.2.1.3 of RFC 5280 decides. A key-establishment certificate passes it, RFC 9763 §3.1 having such a key sign once for proof of possession. The key decides the algorithm, resolved through the same resolver `pki.x509.sign` and `pki.csr.sign` use, so every algorithm those verbs sign with is reached, including an RSASSA-PSS key whose parameters belong to its identifier; the digest comes from the hash the certificate's signature OID indicates, with `digestAlgorithm` or a DER `signatureAlgorithm` naming either outright. A key whose algorithm fixes its own digest refuses `digestAlgorithm` rather than ignoring it, and a key that cannot sign is refused. A certificate other than the one `certID` names throws rather than returning `false`. `certificateHash` and `matchesCertificate` are the extension side, over the whole related certificate: the algorithm defaults as §4.1 directs, to the hash the related certificate's own signature OID indicates, and a certificate that indicates none throws rather than falling back to one the document does not name. `sha1` is refused. `pki.x509.sign` takes `extensions.relatedCertificate`, and the form handed the certificate itself computes the digest rather than trusting a caller to name the algorithm it used; supplying both a `hashValue` and a certificate is refused, the two being able to disagree. The extension is emitted non-critical, which §4.1 asks at SHOULD NOT, and a critical one still parses: `pki.lint.certificate` with the `rfc9763` profile grades it, beside the MUST that it appear only in a chain's end-entity certificate — `requestSignedData`, `verifyRequest`, `certificateHash`, `matchesCertificate` |
|
|
298
|
-
| `pki.trust` | Mozilla and CCADB trust-store ingestion. `parseCertdata` reads the NSS `certdata.txt` object stream and `parseCcadbCsv` the CCADB CSV export, both into one constraint-carrying anchor shape: the per-purpose trust bits, where only `CKT_NSS_TRUSTED_DELEGATOR` grants, and the per-purpose distrust-after dates the bare root list omits. Certificate and trust objects pair by byte-exact issuer and serial rather than adjacency and are cross-checked against the parsed DER, so metadata cannot attach to the wrong root. `anchor()` hands an entry to `pki.path.validate({ trustAnchors, checkPurpose })`, and `anchor(entry, { nameConstraints })` attaches the namespace a root program trusts a root for when that is narrower than the root certificate states, a `{ permitted, excluded }` pair of `{ tag, base }` subtrees that `validate` seeds as the RFC 5280 §6.1.1(h)(i) initial value, intersecting with each certificate's own constraints. `parseTrustAnchorList` reads the IETF's own interchange format for the same thing, an RFC 5914 `TrustAnchorList`, into the same entries, so a list a root program publishes drives `validate` without being mapped by hand. An RFC 5914 anchor's constraints are applied as RFC 5937 §3.2 states, which is that the stricter of the anchor's value and the caller's always wins: its name constraints intersect the caller's permitted subtrees and union its excluded ones, its policy set intersects `userInitialPolicySet`, each of its policy flags may turn the matching `initial*` option on and never off, and its path length bounds the path. `pki.trustanchor.build` writes the list. `systemAnchors` reads the host's own CA bundle into the same entries and names which set it read, `"ca-bundle"` with the path or `"node-bundled"` for the Mozilla set compiled into Node, since the two are not the same trust. A host whose roots are not in a readable PEM file is refused with the command that exports them, and a block that is not a certificate or a certificate the strict reader refuses is listed in `skipped` with its reason. Offline, fail-closed, bounded — `parseCertdata`, `parseCcadbCsv`, `parseTrustAnchorList`, `systemAnchors`, `anchor` |
|
|
298
|
+
| `pki.trust` | Mozilla and CCADB trust-store ingestion. `parseCertdata` reads the NSS `certdata.txt` object stream and `parseCcadbCsv` the CCADB CSV export, both into one constraint-carrying anchor shape: the per-purpose trust bits, where only `CKT_NSS_TRUSTED_DELEGATOR` grants, and the per-purpose distrust-after dates the bare root list omits. Certificate and trust objects pair by byte-exact issuer and serial rather than adjacency and are cross-checked against the parsed DER, so metadata cannot attach to the wrong root. `anchor()` hands an entry to `pki.path.validate({ trustAnchors, checkPurpose })`, and `anchor(entry, { nameConstraints })` attaches the namespace a root program trusts a root for when that is narrower than the root certificate states, a `{ permitted, excluded }` pair of `{ tag, base }` subtrees that `validate` seeds as the RFC 5280 §6.1.1(h)(i) initial value, intersecting with each certificate's own constraints. `parseTrustAnchorList` reads the IETF's own interchange format for the same thing, an RFC 5914 `TrustAnchorList`, into the same entries, so a list a root program publishes drives `validate` without being mapped by hand. An RFC 5914 anchor's constraints are applied as RFC 5937 §3.2 states, which is that the stricter of the anchor's value and the caller's always wins: its name constraints intersect the caller's permitted subtrees and union its excluded ones, its policy set intersects `userInitialPolicySet`, each of its policy flags may turn the matching `initial*` option on and never off, and its path length bounds the path. `pki.trustanchor.build` writes the list. `systemAnchors` reads the host's own CA bundle into the same entries and names which set it read, `"ca-bundle"` with the path or `"node-bundled"` for the Mozilla set compiled into Node, since the two are not the same trust. A host whose roots are not in a readable PEM file is refused with the command that exports them, and a block that is not a certificate or a certificate the strict reader refuses is listed in `skipped` with its reason. Only a self-issued certificate becomes an anchor; the others, such as the intermediate cache a Windows store export includes, come back in `intermediates` for `pki.path.build`. `systemAnchors({ distrustedKeys })` leaves out a certificate whose key is listed, and `spkiDigest(cert)` computes the digest that list takes. Offline, fail-closed, bounded — `parseCertdata`, `parseCcadbCsv`, `parseTrustAnchorList`, `systemAnchors`, `anchor`, `spkiDigest` |
|
|
299
299
|
| `pki.shbs` | Stateful hash-based signature verification: HSS/LMS (RFC 8554), carried in X.509 by RFC 9802 and in CMS by RFC 9708, profiled by NIST SP 800-208 for CNSA 2.0 firmware signing. `verify` checks an HSS signature, where every level must pass, and `verifyLms` a single-tree LMS, over the raw public-key and signature blobs the parsers already surface. Pure public-input SHA-256 and SHAKE256 hashing, a data-driven typecode registry, and bounds-before-slice reads; a malformed blob throws a typed `ShbsError` while a well-formed but wrong signature returns `false`. Verification only by design, since stateful signing needs atomic one-time-key state that belongs in an HSM. `pki.path.validate` verifies an HSS-signed certificate on its own, so a chain reaching one needs no separate call: the signature covers the whole tbsCertificate rather than a digest of it (RFC 9802 §7.1), and a CertificateList naming the algorithm takes the same route. It is proven against the self-signed certificate RFC 9802 Appendix A publishes — `verify`, `verifyLms` |
|
|
300
300
|
| `pki.hpke` | Hybrid Public Key Encryption (RFC 9180), the encrypt-to-a-public-key primitive behind TLS ECH, MLS, and OHTTP. `setupS` and `setupR` establish a sender or recipient context (KEM encapsulation plus the key schedule); the context's `seal` and `open` AEAD-encrypt with a sequence-counter nonce, and `export` derives further secrets; the module-level `seal` and `open` are single-shot wrappers. DHKEM (P-256, P-384, P-521, X25519, X448) in all four modes, proven against the RFC 9180 Appendix A vectors, the post-quantum ML-KEM-512/768/1024 KEMs (IANA 0x0040 to 0x0042) in the base and psk modes, proven against the draft-ietf-hpke-pq Appendix A vectors, and the PQ/T hybrid KEMs MLKEM768-P256 (0x0050), MLKEM1024-P384 (0x0051) and MLKEM768-X25519 (0x647a, X-Wing), each pairing an ML-KEM with a nominal group under the CFRG C2PRI combiner and proven against the draft-irtf-cfrg-concrete-hybrid-kems Appendix B vectors; the two-stage HKDF-SHA256/SHA384/SHA512 and the single-stage SHAKE128/SHAKE256 key schedules, proven against the draft-ietf-hpke-pq Appendix A.7/A.8/A.11/A.12 vectors, by AES-GCM / ChaCha20Poly1305 / export-only. On a single-stage KDF `info`, `psk`, `psk_id` and an export are each capped at 65535 bytes; TurboSHAKE128/256 are not offered while no released OpenSSL carries the XOF. `deriveKeyPair`, `generateKeyPair`, `encap` and `decap` reach a KEM on its own. Pure composition over `node:crypto` — `suites`, `setupS`, `setupR`, `seal`, `open`, `encap`, `decap` |
|
|
301
301
|
| `pki.kem` | Composite ML-KEM key establishment (draft-ietf-lamps-pq-composite-kem), a post-quantum ML-KEM hybridized with a traditional RSA-OAEP, ECDH, X25519, or X448 so the established secret holds if either component is later broken. `encapsulate` turns a recipient's composite `SubjectPublicKeyInfo` into a 256-bit shared secret and a ciphertext; `decapsulate` recovers the same secret from the ciphertext and the composite PKCS#8 private key. Each component KEM runs independently and the two secrets are mixed through a SHA3-256 combiner that binds the traditional ciphertext, the traditional public key, and a per-algorithm label. The twelve algorithms pair ML-KEM-768 and ML-KEM-1024 with RSA-OAEP 2048/3072/4096, ECDH over P-256/P-384/P-521 and brainpoolP256r1/P384r1, X25519, and X448, each verified against the draft Appendix G known-answer vectors; a malformed key or ciphertext, an unsupported algorithm, or a component decapsulation failure throws a typed `KemError` — `encapsulate`, `decapsulate` |
|
|
@@ -303,10 +303,10 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
|
|
|
303
303
|
the checkpoint read through `pki.tlog` and verified under the key the entry names in `logId.keyId`, so a signed tree root from one pinned log cannot satisfy an entry claiming another; the size and root the proof folds against come from that verified checkpoint rather than from the `inclusionProof` fields no signature covers, and the in-toto SLSA subject digest the caller confirms against the published artifact. It reuses the X.509 parser, the RFC 5280 path validator, and the Merkle verifier; the net-new codecs are the DSSE PAE byte-builder and a fail-closed JSON reader. `verified: true` says the artifact was signed and logged, not that a party you trust signed it — Fulcio issues to anyone who completes an OIDC flow, so who signed is decided only by `opts.identity`, and `identityChecked` reports which of its fields were compared. An identity policy naming no field, or a field name that is not one of the three, is refused rather than satisfied — either would accept every signer while reading as a policy in force. A sixth leg is opt-in: pinning the certificate-transparency logs in `opts.ctLogs` checks the receipt Fulcio embeds in the certificate it issues (RFC 6962 §3.2), verified over the certificate as it stood before the receipt was added and under the key of the log that issued it, so the verdict says the signing certificate was public when it was issued rather than handed out quietly. At least one receipt must verify against a pinned log, the verdict reports `sctChecked` and `validScts`, and an empty log set is refused rather than read as a policy that checks nothing. Both content arms are read: a DSSE-wrapped in-toto attestation, and a message signature over an artifact's own bytes, which `cosign sign-blob` produces. For that arm `opts.artifact` supplies the bytes, and they are held to the hash the Rekor `hashedrekord` entry records before the signature is checked over them; the `messageDigest` the bundle carries is covered by no signature, so it is compared against the computed digest and never used in its place. A Rekor v2 `hashedrekord` v0.0.2 entry is read on either arm: Rekor v2 supports no DSSE entry type, so a DSSE signature is logged as a `hashedrekord` over the hash of its signing preimage, and the entry is bound by the signature it records, the certificate that made it, and that digest. Where the entry states `keyDetails`, that statement is held to the leaf key rather than read in its place. Rekor v2 issues no signed entry timestamp and writes `integratedTime` as zero, so it is ignored and `opts.tsaRoots` pins the timestamp-authority anchors for an RFC 3161 token over the signature bytes; with neither that nor `opts.time` the verdict is `sigstore/no-attested-time`, because a certificate that lives about ten minutes cannot be validated against an unattested instant. `timestampSource` names which of the three dated it, beside `logOrigin`, `treeSize` and `checkpointKeyId` from the verified checkpoint. `contentType` names the arm and the verdict has one shape for both — `pae`, `parseBundle`, `verifyBundle` |
|
|
304
304
|
| `pki.inspect` | Human-readable inspection, the pure-JS equivalent of `openssl x509/crl/req/cms -text`. `certificate(pem \| der \| parsed)` renders an OpenSSL-style report: version, serial, signature algorithm, issuer and subject distinguished names, validity, public-key details (curve or modulus size plus the raw point or modulus), every decoded extension with its critical flag, and the signature. `crl`, `csr`, and `cms` render the other formats the same way — a CRL like `openssl crl -text`, a CSR like `openssl req -text`, and a CMS message like `openssl cms -cmsout -print`, with a stable summary for a non-SignedData ContentInfo — and `any(input)` detects the format and routes to the right report. Built over the strict parsers and the two-way OID registry with one set of field renderers, it names extension and algorithm OIDs an OpenSSL build shows only as raw bytes. No OpenSSL dependency, and the format is stable and OpenSSL-familiar rather than pinned to one OpenSSL version. A certificate policy's user notice renders as text, both its explicit text and a notice reference with the notice numbers that identify it, rather than hex, and a malformed part falls back to a hex dump rather than throwing. Every format `pki.schema.all()` detects has a report and its own named verb, so a PKCS#8 key, a PKCS#12 store, a CRMF request, a CMP message, an EST CSR-attributes response, an RFC 5914 trust anchor list, an OCSP request or response, an RFC 3161 timestamp response and an RFC 5755 attribute certificate each read the same way a certificate does. No report prints private key material: a PKCS#8 and a PKCS#12 name the algorithm and say a private key is present, never its bytes, and a PKCS#12's encrypted safes are named rather than decrypted since these verbs take no password. `asn1(input)` is the structural dump below the reports, shaped after `openssl asn1parse`: one line per value carrying the byte offset, the nesting depth, the header and content lengths, the form, the tag name, and a primitive's rendered content, with an OBJECT IDENTIFIER named from the registry and a time rendered as the bytes on the wire. A conforming input is read by the same strict decoder the parsers use; when that decode refuses the bytes, the report names the refusal and a header walk renders the prefix it can read and the offset where it stopped, so a truncated, trailing-byte, indefinite-length or non-minimal-length file still reads. It withholds no value: where the other verbs here name a key and print none of it, this one prints bytes, private key material included, so a report that must not print a key is `any` or `pkcs8` — `certificate`, `crl`, `csr`, `cms`, `pkcs8`, `pkcs12`, `crmf`, `cmp`, `csrattrs`, `trustanchor`, `ocspRequest`, `ocspResponse`, `tsp`, `attrcert`, `asn1`, `any` |
|
|
305
305
|
| `pki.webauthn` | WebAuthn and passkey verification, both halves: offline trust evaluation of a W3C WebAuthn (Level 3) registration, and signature verification of the assertion every login returns. `parseAttestationObject(bytes)` decodes the CBOR attestation object, authenticatorData, and COSE credential key over the strict `pki.cbor` codec; `parseAuthenticatorData(bytes)` reads the bare form an assertion carries through the same parser; `parseClientData(bytes, opts)` decodes the `clientDataJSON` no signature check looks inside, through the shared JSON guard since these are attacker-chosen bytes, returning the challenge decoded so a caller compares bytes rather than spellings, and checking the ceremony type, challenge, and origin when the relying party supplies what it issued. `verifyAssertion(input)` verifies an assertion signature over `authenticatorData \|\| SHA-256(clientDataJSON)` — raw bytes, no COSE_Sign1, an ES256 signature in ASN.1 DER — and applies the §7.2 step 21 counter rule when a stored `previousSignCount` is given, so a counter that fails to advance is refused as a cloned authenticator. `verify(attestationObject, clientDataHash, opts)` checks the attestation-statement signature and each format's structural bindings for packed, tpm, android-key, apple, fido-u2f, and none: the x5c leaf key, the apple nonce, the tpm `certInfo` Name and `extraData` over the `pubArea`, the android `KeyDescription`, and the fido-u2f `verificationData`. It binds the credential public key to each attestation, through the signed authenticatorData for packed and fido-u2f or a cert or `pubArea`-key equality check for android-key, apple, and tpm, and enforces each leaf's certificate requirements. The credential-key check covers the full WebAuthn COSE algorithm set — ES256/384/512, RS256/384/512, PS256/384/512, EdDSA (Ed25519), and the RFC 9864 fully-specified identifiers ESP256/384/512, Ed25519, and Ed448 — validating the public-key point on its curve, rejecting the compressed EC point form, and enforcing a minimally encoded DER ECDSA signature. The verdict field is `attestationVerified`, and `signatureVerified` for an assertion, rather than a bare `verified`, because a sound statement is a different claim from an acceptable ceremony: an attestation naming another origin's RP ID with user presence clear is perfectly sound and must not be registered. Pass `expectedRpId`, `requireUserPresence`, `requireUserVerification`, or `allowedAlgorithms` and those are checked, with `bindingChecked` reporting which ran, so a check that passed can be told from one that never happened. Pass `opts.clientDataJSON` instead of the digest and the client data is read here too: the ceremony type is checked unconditionally, since a login response replayed into a registration is what that rule stops, and the challenge, origin, and top-level origin are checked against what the relying party issued, with `clientData.checked` reporting which comparisons ran. A registration verdict also carries the `credentialId`, `credentialPublicKey`, and initial `signCount` a later login needs. A credential key declaring COSE algorithm `-65535` (RSASSA-PKCS1-v1_5 with SHA-1) is refused unless `allowedAlgorithms` names it, since every signature that credential ever makes would use SHA-1. Anchoring the trust path has two routes: `opts.metadata` resolves the roots the authenticator's own model registered, and `opts.rootCertificates` pins roots directly, which is what anchors the formats FIDO MDS does not cover, Apple's authenticators and the Google hardware-attestation roots among them. `metadata` governs when both are given, and `anchoredTo` names every route that anchored the path, joined with `+` when more than one did: `"metadata"`, `"rootCertificates"`, and `"safetyNetRoots"` for the android-safetynet chain, which anchors through the roots that format requires whether or not either other route was asked for. It is `null` only when nothing anchored the path. `verifyMetadataBlob(blob, opts)` reads a FIDO Metadata Service (MDS v3) BLOB, the signed catalogue of registered authenticator models, verifying its JWS and chaining its signer to an operator-supplied FIDO root before the payload is parsed, with sequence-number rollback and `nextUpdate` freshness checks. Passing the result as `opts.metadata` to `verify` resolves the authenticator's registered attestation roots from its identifier and requires the trust path to fully validate to one of them, refusing an unlisted or revoked model. `metadataAnchors(entry, opts)` applies the same status gate for a caller anchoring the path themselves: a model the catalogue has disqualified registers no anchors. Both of the catalogue's key spaces are covered: an aaguid, and the attestation-certificate key identifiers a U2F authenticator is listed under instead. No FIDO root is bundled, there is no trust-on-first-use, and retrieving the BLOB is out of scope. Fail-closed with typed `webauthn/*` errors — `parseAttestationObject`, `verify`, `verifyMetadataBlob`, `metadataFor`, `metadataAnchors` |
|
|
306
|
-
| `pki.lint` | Certificate linting, the zlint or pkilint of JavaScript. `certificate(pem \| der \| parsed, opts)` walks a parsed certificate and emits graded advisory findings, each with a stable id, a severity (`fatal`, `error`, `warn`, `notice`), a source, a spec-clause citation, and a message, against the RFC 5280 profile, the post-quantum certificate profiles, and a representative CA/Browser Forum TLS BR subset. The RFC 5280 profile carries the rows that document's own clauses delegate: §4.2.1.3 states that "appropriate values for keyUsage extensions for particular algorithms are specified in [RFC3279], [RFC4055], and [RFC4491]", and RFC 8813 §3 is where the elliptic-curve answer now is, so an EC certificate asserting keyEncipherment or dataEncipherment is `lint/rfc5480/ec-key-usage-forbidden` there and under `rfc5480` alone. The RFC 5280 rows themselves: serial sign and size, validity ordering and the SC081v3 reducing validity schedule, keyCertSign coherence, extension criticality (basicConstraints, nameConstraints, policyConstraints and inhibitAnyPolicy must be critical, and keyUsage should be), nameConstraints CA-scope, unknown critical extensions, empty-subject SAN, SKI and AKI presence including the end-entity subjectKeyIdentifier, the six extensions a conforming CA must mark non-critical (reported by clause rather than as unrecognized) and the two it should (issuerAltName, cRLDistributionPoints), a policy mapping to or from anyPolicy, a unique identifier a conforming CA must not generate, a critical subjectAltName beside a non-empty subject, a critical extKeyUsage naming anyExtendedKeyUsage, a userNotice noticeRef, SAN required and CN-in-SAN, dNSName syntax, serverAuth EKU, weak keys, the CA/Browser Forum subscriber certificate profile of §7.1.2.7 (the extension presence and criticality table, the authorityInformationAccess accessMethod and accessLocation tables, basicConstraints cA and pathLenConstraint, certificatePolicies against anyPolicy and the exactly-one reserved identifier rule and the permitted-qualifier table, the forbidden extended key purposes, the two Key Usage tables that differ by key type, and the subjectAltName GeneralName table with its criticality rule in both directions), the CA/Browser Forum S/MIME BR subscriber profile of §7.1.2.3 (a) through (n) as `cabf-smime`, whose requirements follow the generation a certificate declares through its reserved policy identifier (the distribution-point and accessMethod URI scheme rules, the policy qualifier rules for an id-qt-cps URL and an id-qt-unotice explicitText and noticeRef, the key-usage table for the certificate's own key algorithm including a bit set past the nine named positions, the extended key purposes forbidden at every generation and the ones Strict alone forbids, the authorityKeyIdentifier field rules, a required mailbox in the subjectAltName as an rfc822Name or an id-on-SmtpUTF8Mailbox otherName, the criticality each extension must carry, the subjectDirectoryAttributes and Adobe extensions the Strict generation prohibits, the Legal Entity Identifier extensions keyed on the certificate type rather than the generation, and the Legacy generation retired on 15 July 2025), the CA/Browser Forum Code Signing BR subscriber profile of §7.1.2.3 as `cabf-cs`, whose rows follow the kind a certificate declares through one of the three reserved identifiers of §7.1.6.1 (the extension presence and criticality rules at the strengths this document states them, a HTTP URL required in the distribution points and in each accessMethod location, a critical keyUsage asserting digitalSignature with `keyCertSign` and `cRLSign` forbidden and every other bit discouraged, the extended key purpose each kind must carry with the critical extKeyUsage a timestamp certificate needs, the §7.1.6.4 exactly-one-identifier rule gated to 15 September 2026, the §6.3.2 ceilings of 39 months, 460 days and 135 months, and the §6.1.5.2 floors of a 3072-bit RSA modulus, NIST P-256, P-384 or P-521, or DSA parameters of 2048 bits with a 224- or 256-bit modulus length, and the §7.1.4.2 subject distinguished name rows: a required commonName, a prohibited domainComponent, a subject attribute made only of the metadata characters the clause names, a required organizationName and two-letter countryName, one of localityName and stateOrProvinceName, and on an EV certificate a businessCategory from the four strings the clause names, a jurisdictionCountryName and a serialNumber), the ETSI qualified-certificate profile of EN 319 412-5 as `etsi-qc` (a non-critical qcStatements extension, a QcType naming one and only one of the three purposes the clause defines, a QcPDS providing an https URL and, on an EU qualified certificate, a document in English and no more than one per language, plus the EN 319 412-1 semantics identifiers holding a subject serialNumber or organizationIdentifier to a three-character identity type reference, a two-character country code, a hyphen-minus and an identifier, with XG required of an LEI value and a name registration authority URI required of a locally defined reference), the Mozilla Root Store Policy as `mozilla-root`, which is named rather than detected because a root program's requirements apply by virtue of the store a certificate chains to (the §5.1 AlgorithmIdentifier encodings compared as DER, an RSA modulus of at least 2048 bits and a whole number of octets, a public exponent above 1, an EdDSA key only beside id-kp-emailProtection, the §5.1.3 SHA-1 prohibitions on their own dates, a serial greater than zero with a short magnitude reported as a warning, an extKeyUsage on every end entity without anyExtendedKeyUsage, and the §5.3 intermediate rules from 2019, which pass over a certificate issued in its own name), the Chrome Root Program Policy v1.8 as `chrome-root`, named on the same grounds (the §1.3.2 dedicated-hierarchy rules holding a subordinate CA certificate's extKeyUsage to id-kp-serverAuth with id-kp-clientAuth reported as a warning, scoped to the unexpired certificates the clause names by passing over a notAfter before 15 June 2026, the same rules over a subscriber certificate issued on or after 15 March 2027 where id-kp-serverAuth stands alone and an extKeyUsage naming id-kp-OCSPSigning and nothing else is a delegated OCSP responder rather than a subscriber, the §1.3.1.3 three-year subordinate CA validity ceiling, and the §1.3.1.2 root term limit, which reports a self-signed CA certificate whose validity runs past the removal date its own notBefore fixes and passes over a CA reissued in its own name whose authorityKeyIdentifier names a key other than its own subjectKeyIdentifier), the Microsoft Trusted Root Program requirements as `microsoft-root`, named on the same grounds (the §3.A.1 root commonName and the keyUsage that must be present, critical and set keyCertSign and cRLSign, the §3.A.3 term of eight to 25 years, the §3.A.5 revocation pointer and §3.A.8 separation of uses on an issuing CA, the §3.A.10 seventeen reserved policy identifiers on an end entity, the §3.A.14 exclusivity of id-kp-OCSPSigning, the §3.B signature and key table including its code signing column, and the §3.E.2 extended key usages the program enables on a root), the Apple Root Program Policy as `apple-root`, named on the same grounds (its Appendix A trust purposes read as extended key usage sets, the §1.5 root key floor of RSA 4096 or NIST P-384, the §1.7 subordinate CA rules gated to 1 August 2026 and 1 July 2027, the §2.3 rfc822Name an S/MIME subscriber needs from 1 February 2027, and the subscriber purpose scoping that binds when its permission expires on 1 July 2027), the two algorithm-policy profiles, named for the same reason a root program's are (which policy a certificate is held to is the caller's assertion): `nist-pqc-transition` carries NIST IR 8547's two transitions, an error for a classical subject key or signature algorithm in a certificate issued from 2036 and a warning for a subject key at or below the 112-bit security strength issued from 2031, with the strength read off SP 800-57 Part 1 Rev 5 Table 2 and a composite signature passed over as the dual-algorithm operation the guidance endorses; `rfc7633` reads the values of a TLS Feature extension, reporting a feature outside the 16-bit width of the IANA registry RFC 7633 §4.1 points at, a repeated feature, the transparency_info feature RFC 9162 §7.2 asks a CA not to issue, and the status_request_v2 feature RFC 9846 leaves out of its ExtensionType enum, while a critical TLS Feature extension is reported by the default set; and `cnsa-2.0` carries the suite profile of `draft-jenkins-cnsa2-pkix-profile-05` (ML-DSA-87 or ML-KEM-1024 as the subject public key, ML-DSA-87 as the signature, and the extension, criticality and key-usage rules each of the three certificate kinds carries, where the key-usage set follows the kind the subject key declares), the same signature rule reaching `crl` and `ocsp` under that name because the document states it once for all three, the post-quantum key-usage rules binding a certificate's keyUsage bits to its subject key type (ML-KEM under RFC 9935 §5, ML-DSA under RFC 9881 §5, and SLH-DSA under RFC 9909 §6 across both its pure and prehash identifiers) together with the ML-KEM encapsulation-key encoding and size, and the §4.2.1.4 certificate-policy user-notice rules (a VisibleString or BMPString explicitText, a notice past 200 characters, an empty notice, control characters, and a non-NFC UTF8String notice, each at the strength the clause states). Alone among these entries the data path never throws: hostile bytes return a `fatal` `lint/unparseable` finding carrying the strict parser's code, so a whole directory lints without a try/catch, and only config-time misuse throws a typed `LintError` — `certificate`, `crl` (the RFC 5280 §5 revocation-list profile: nextUpdate presence, update ordering, cRLNumber presence and its 20-octet ceiling, authorityKeyIdentifier presence, the criticality each profiled CRL and entry extension must carry, freshestCRL in a delta CRL, and unrecognized critical extensions), `ocsp` (the RFC 6960 response profile: an empty signature, each SingleResponse's update ordering and its relation to producedAt, an empty `certs` field, the extended revoked definition and the fixed shape of a revoked answer for a non-issued certificate, the list each extension belongs in, the criticality and value syntax of the CRL entry extensions a SingleResponse may carry, unrecognized critical extensions, the reasons unspecified and removeFromCRL, and two answers for one CertID; the RFC 5019 lightweight rows run on request), `csr` (the artifact a CA reads before it issues: an empty subject naming no identity, an empty subject whose subjectAltName is not critical, a request for an extension the issuer determines, a request asking for a CA certificate, a broken signing digest, and a challengePassword traveling with the request; `opts.profile: "cabf-tls"` adds the certificate profile's own commonName-in-SAN, dNSName syntax, SAN-required and key-strength rows, applied to what the request asks for, and they run only when named because a request carries no extKeyUsage. It does not check the request's signature, which is a cryptographic operation `pki.csr.verify` owns), `cms` (the RFC 5652 profile: a signing-time naming a year from 1950 to 2049 encoded as GeneralizedTime where §11.3 requires UTCTime, a signer digest the digestAlgorithms set does not list, a version 1 attribute certificate, and notices for a SignedData with no signers and for a detached signature), `tsp` (the RFC 3161 profile: a tsa hint naming a subject the token's own certificate does not carry, a serial wider than 160 bits, and a token stating no accuracy), `attrcert` (the RFC 5755 profile: the §4.3 criticality table, a critical extension the profile does not define, a Holder naming more than one option, and an issuerUniqueID), each short because the strict parser behind it already refuses most of what its document requires and those clauses arrive as `lint/unparseable` carrying the parser's own code, `rules`, `profiles` |
|
|
306
|
+
| `pki.lint` | Certificate linting, the zlint or pkilint of JavaScript. `certificate(pem \| der \| parsed, opts)` walks a parsed certificate and emits graded advisory findings, each with a stable id, a severity (`fatal`, `error`, `warn`, `notice`), a source, a spec-clause citation, and a message, against the RFC 5280 profile, the post-quantum certificate profiles, and a representative CA/Browser Forum TLS BR subset. The RFC 5280 profile carries the rows that document's own clauses delegate: §4.2.1.3 states that "appropriate values for keyUsage extensions for particular algorithms are specified in [RFC3279], [RFC4055], and [RFC4491]", and RFC 8813 §3 is where the elliptic-curve answer now is, so an EC certificate asserting keyEncipherment or dataEncipherment is `lint/rfc5480/ec-key-usage-forbidden` there and under `rfc5480` alone. The RFC 5280 rows themselves: serial sign and size, validity ordering and the SC081v3 reducing validity schedule, keyCertSign coherence, extension criticality (basicConstraints, nameConstraints, policyConstraints and inhibitAnyPolicy must be critical, and keyUsage should be), nameConstraints CA-scope, unknown critical extensions, empty-subject SAN, SKI and AKI presence including the end-entity subjectKeyIdentifier, the six extensions a conforming CA must mark non-critical (reported by clause rather than as unrecognized) and the two it should (issuerAltName, cRLDistributionPoints), a policy mapping to or from anyPolicy, a unique identifier a conforming CA must not generate, a critical subjectAltName beside a non-empty subject, a critical extKeyUsage naming anyExtendedKeyUsage, a userNotice noticeRef, SAN required and CN-in-SAN, dNSName syntax, serverAuth EKU, weak keys, the CA/Browser Forum subscriber certificate profile of §7.1.2.7 (the extension presence and criticality table, the authorityInformationAccess accessMethod and accessLocation tables, basicConstraints cA and pathLenConstraint, certificatePolicies against anyPolicy and the exactly-one reserved identifier rule and the permitted-qualifier table, the forbidden extended key purposes, the two Key Usage tables that differ by key type, and the subjectAltName GeneralName table with its criticality rule in both directions), the CA/Browser Forum S/MIME BR subscriber profile of §7.1.2.3 (a) through (n) as `cabf-smime`, whose requirements follow the generation a certificate declares through its reserved policy identifier (the distribution-point and accessMethod URI scheme rules, the policy qualifier rules for an id-qt-cps URL and an id-qt-unotice explicitText and noticeRef, the key-usage table for the certificate's own key algorithm including a bit set past the nine named positions, the extended key purposes forbidden at every generation and the ones Strict alone forbids, the authorityKeyIdentifier field rules, a required mailbox in the subjectAltName as an rfc822Name or an id-on-SmtpUTF8Mailbox otherName, the criticality each extension must carry, the subjectDirectoryAttributes and Adobe extensions the Strict generation prohibits, the Legal Entity Identifier extensions keyed on the certificate type rather than the generation, and the Legacy generation retired on 15 July 2025), the CA/Browser Forum Code Signing BR subscriber profile of §7.1.2.3 as `cabf-cs`, whose rows follow the kind a certificate declares through one of the three reserved identifiers of §7.1.6.1 (the extension presence and criticality rules at the strengths this document states them, a HTTP URL required in the distribution points and in each accessMethod location, a critical keyUsage asserting digitalSignature with `keyCertSign` and `cRLSign` forbidden and every other bit discouraged, the extended key purpose each kind must carry with the critical extKeyUsage a timestamp certificate needs, the §7.1.6.4 exactly-one-identifier rule gated to 15 September 2026, the §6.3.2 ceilings of 39 months, 460 days and 135 months, and the §6.1.5.2 floors of a 3072-bit RSA modulus, NIST P-256, P-384 or P-521, or DSA parameters of 2048 bits with a 224- or 256-bit modulus length, and the §7.1.4.2 subject distinguished name rows: a required commonName, a prohibited domainComponent, a subject attribute made only of the metadata characters the clause names, a required organizationName and two-letter countryName, one of localityName and stateOrProvinceName, and on an EV certificate a businessCategory from the four strings the clause names, a jurisdictionCountryName and a serialNumber), the ETSI qualified-certificate profile of EN 319 412-5 as `etsi-qc` (a non-critical qcStatements extension, a QcType naming one and only one of the three purposes the clause defines, a QcPDS providing an https URL and, on an EU qualified certificate, a document in English and no more than one per language, plus the EN 319 412-1 semantics identifiers holding a subject serialNumber or organizationIdentifier to a three-character identity type reference, a two-character country code, a hyphen-minus and an identifier, with XG required of an LEI value and a name registration authority URI required of a locally defined reference), the Mozilla Root Store Policy as `mozilla-root`, which is named rather than detected because a root program's requirements apply by virtue of the store a certificate chains to (the §5.1 AlgorithmIdentifier encodings compared as DER, an RSA modulus of at least 2048 bits and a whole number of octets, a public exponent above 1, an EdDSA key only beside id-kp-emailProtection, the §5.1.3 SHA-1 prohibitions on their own dates, a serial greater than zero with a short magnitude reported as a warning, an extKeyUsage on every end entity without anyExtendedKeyUsage, and the §5.3 intermediate rules from 2019, which pass over a certificate issued in its own name), the Chrome Root Program Policy v1.8 as `chrome-root`, named on the same grounds (the §1.3.2 dedicated-hierarchy rules holding a subordinate CA certificate's extKeyUsage to id-kp-serverAuth with id-kp-clientAuth reported as a warning, scoped to the unexpired certificates the clause names by passing over a notAfter before 15 June 2026, the same rules over a subscriber certificate issued on or after 15 March 2027 where id-kp-serverAuth stands alone and an extKeyUsage naming id-kp-OCSPSigning and nothing else is a delegated OCSP responder rather than a subscriber, the §1.3.1.3 three-year subordinate CA validity ceiling, and the §1.3.1.2 root term limit, which reports a self-signed CA certificate whose validity runs past the removal date its own notBefore fixes and passes over a CA reissued in its own name whose authorityKeyIdentifier names a key other than its own subjectKeyIdentifier), the Microsoft Trusted Root Program requirements as `microsoft-root`, named on the same grounds (the §3.A.1 root commonName and the keyUsage that must be present, critical and set keyCertSign and cRLSign, the §3.A.3 term of eight to 25 years, the §3.A.5 revocation pointer and §3.A.8 separation of uses on an issuing CA, the §3.A.10 seventeen reserved policy identifiers on an end entity, the §3.A.14 exclusivity of id-kp-OCSPSigning, the §3.B signature and key table including its code signing column, and the §3.E.2 extended key usages the program enables on a root), the Apple Root Program Policy as `apple-root`, named on the same grounds (its Appendix A trust purposes read as extended key usage sets, the §1.5 root key floor of RSA 4096 or NIST P-384, the §1.7 subordinate CA rules gated to 1 August 2026 and 1 July 2027, the §2.3 rfc822Name an S/MIME subscriber needs from 1 February 2027, and the subscriber purpose scoping that binds when its permission expires on 1 July 2027), the two algorithm-policy profiles, named for the same reason a root program's are (which policy a certificate is held to is the caller's assertion): `nist-pqc-transition` carries NIST IR 8547's two transitions, an error for a classical subject key or signature algorithm in a certificate issued from 2036 and a warning for a subject key at or below the 112-bit security strength issued from 2031, with the strength read off SP 800-57 Part 1 Rev 5 Table 2 and a composite signature passed over as the dual-algorithm operation the guidance endorses; `rfc7633` reads the values of a TLS Feature extension, reporting a feature outside the 16-bit width of the IANA registry RFC 7633 §4.1 points at, a repeated feature, the transparency_info feature RFC 9162 §7.2 asks a CA not to issue, and the status_request_v2 feature RFC 9846 leaves out of its ExtensionType enum, while a critical TLS Feature extension is reported by the default set; and `cnsa-2.0` carries the suite profile of `draft-jenkins-cnsa2-pkix-profile-05` (ML-DSA-87 or ML-KEM-1024 as the subject public key, ML-DSA-87 as the signature, and the extension, criticality and key-usage rules each of the three certificate kinds carries, where the key-usage set follows the kind the subject key declares), the same signature rule reaching `crl` and `ocsp` under that name because the document states it once for all three, the post-quantum key-usage rules binding a certificate's keyUsage bits to its subject key type (ML-KEM under RFC 9935 §5, ML-DSA under RFC 9881 §5, and SLH-DSA under RFC 9909 §6 across both its pure and prehash identifiers) together with the ML-KEM encapsulation-key encoding and size, and the §4.2.1.4 certificate-policy user-notice rules (a VisibleString or BMPString explicitText, a notice past 200 characters, an empty notice, control characters, and a non-NFC UTF8String notice, each at the strength the clause states). Alone among these entries the data path never throws: hostile bytes return a `fatal` `lint/unparseable` finding carrying the strict parser's code, so a whole directory lints without a try/catch, and only config-time misuse throws a typed `LintError` — `certificate`, `crl` (the RFC 5280 §5 revocation-list profile: nextUpdate presence, update ordering, cRLNumber presence and its 20-octet ceiling, authorityKeyIdentifier presence, the criticality each profiled CRL and entry extension must carry, freshestCRL in a delta CRL, and unrecognized critical extensions), `ocsp` (the RFC 6960 response profile: an empty signature, each SingleResponse's update ordering and its relation to producedAt, an empty `certs` field, the extended revoked definition and the fixed shape of a revoked answer for a non-issued certificate, the list each extension belongs in, the criticality and value syntax of the CRL entry extensions a SingleResponse may carry, unrecognized critical extensions, the reasons unspecified and removeFromCRL, and two answers for one CertID; the RFC 5019 lightweight rows run on request), `csr` (the artifact a CA reads before it issues: an empty subject naming no identity, an empty subject whose subjectAltName is not critical, a request for an extension the issuer determines, a request asking for a CA certificate, a broken signing digest, and a challengePassword traveling with the request; `opts.profile: "cabf-tls"` adds the certificate profile's own commonName-in-SAN, dNSName syntax, SAN-required and key-strength rows, applied to what the request asks for, and they run only when named because a request carries no extKeyUsage. It does not check the request's signature, which is a cryptographic operation `pki.csr.verify` owns), `cms` (the RFC 5652 profile: a signing-time naming a year from 1950 to 2049 encoded as GeneralizedTime where §11.3 requires UTCTime, a signer digest the digestAlgorithms set does not list, a version 1 attribute certificate, and notices for a SignedData with no signers and for a detached signature), `tsp` (the RFC 3161 profile: a tsa hint naming a subject the token's own certificate does not carry, a serial wider than 160 bits, and a token stating no accuracy), `attrcert` (the RFC 5755 profile: the §4.3 criticality table, a critical extension the profile does not define, a Holder naming more than one option, and an issuerUniqueID), each short because the strict parser behind it already refuses most of what its document requires and those clauses arrive as `lint/unparseable` carrying the parser's own code. The `key-compromise` profile, in the default set for certificates and requests, reports an elliptic curve key whose private scalar is below 256 (`lint/key-compromise/small-ec-private-key`), `rules`, `profiles` |
|
|
307
307
|
| `pki.C` / `pki.constants` | Version-stable constants: the functional scale helpers `C.TIME.*` and `C.BYTES.*`, the codec `LIMITS`, and `version` |
|
|
308
308
|
| `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError`, `Asn1Error`, `OidError`, `PemError`, `CertificateError`, `CrlError`, `CsrError`, `Pkcs8Error`, `CmsError`, `OcspError`, `TspError`, `AttrCertError`, `CrmfError`, `Pkcs12Error`, `CmpError`, `PathError`, `CtError`, `JoseError`, `AcmeError`, `WebauthnError`, and `LintError`, each carrying a stable `code` in `domain/reason` form |
|
|
309
|
-
| `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>`, `pki inspect <file> [--asn1]`, `pki keygen --out <key>`, `pki csr --key <k> --subject <dn>`, `pki issue (--key <k> --subject <dn> \| --csr <req>)`, `pki fetch <https-url>`, `pki lint <file>`, `pki convert <file> --to der\|pem`, `pki verify <cert>... --anchor <cert
|
|
309
|
+
| `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>`, `pki inspect <file> [--asn1]`, `pki keygen --out <key>`, `pki csr --key <k> --subject <dn>`, `pki issue (--key <k> --subject <dn> \| --csr <req>)`, `pki fetch <https-url>`, `pki lint <file>`, `pki convert <file> --to der\|pem`, `pki verify <cert>... --anchor <cert>` (`fetch` and `verify` take `--distrusted-key <hex>[,<hex>...]`), `pki sign <file> --cert <c> --key <k>`. `pki <verb> --help` prints that verb's usage on stdout and exits 0. Issuing from a request answers what the request asks for: `--copy-requested-san` writes the requested subjectAltName into the certificate, `--ignore-requested-extensions` issues without it, and a request asking for anything neither accounts for is refused rather than certified short |
|
|
310
310
|
|
|
311
311
|
### Runnable examples
|
|
312
312
|
|
|
@@ -341,13 +341,14 @@ pki csr --key k.key --subject "CN=host.example" --san host.example --out req.der
|
|
|
341
341
|
pki issue --key k.key --subject "CN=ca.example" --ca --days 3650 --out ca.der
|
|
342
342
|
pki issue --csr req.der --issuer-cert ca.der --issuer-key k.key --copy-requested-san --out leaf.der
|
|
343
343
|
pki issue --csr req.der --issuer-cert ca.der --issuer-key k.key --ignore-requested-extensions --out bare.der
|
|
344
|
-
pki fetch https://example.com/ --system # the chain the endpoint presents; NOT a verification
|
|
344
|
+
pki fetch https://example.com/ --system # the chain the endpoint presents, against the platform store; NOT a verification
|
|
345
345
|
pki lint cert.pem # graded conformance findings; exit 1 on an error
|
|
346
346
|
pki lint cert.pem --json --profile cabf-tls
|
|
347
347
|
pki lint list.crl # detects a CRL or an OCSP response and runs its profile
|
|
348
348
|
pki lint status.ors --profile rfc5019 # the RFC 5019 lightweight rows for an OCSP response
|
|
349
349
|
pki convert cert.pem --to der > cert.der # transcode between PEM and DER (round-trips)
|
|
350
350
|
pki verify leaf.pem --anchor root.pem --time 2026-01-01T00:00:00Z # RFC 5280 path validation
|
|
351
|
+
pki verify int.pem leaf.pem --anchor root.pem --distrusted-key <spki-sha256-hex> # a path carrying a listed key is invalid
|
|
351
352
|
pki sign msg.txt --cert signer.pem --key signer-key.pem --out msg.p7s # CMS SignedData (pki.cms.sign)
|
|
352
353
|
pki sign msg.txt --cert signer.pem --key signer-key.pem --detached --pem # detached, PEM to stdout
|
|
353
354
|
```
|
package/bin/pki.js
CHANGED
|
@@ -17,16 +17,26 @@
|
|
|
17
17
|
* | --csr <req>) [--ca]
|
|
18
18
|
* [--issuer-cert <c> --issuer-key <k>] [--days N] [--serial HEX] [--san a,b] [--out F] [--pem]
|
|
19
19
|
* pki fetch <https-url> --anchor <c> the chain a live endpoint presents; NOT a verification
|
|
20
|
-
* [--system] [--out F] [--der]
|
|
20
|
+
* [--system] [--distrusted-key H[,H...]] [--out F] [--der]
|
|
21
|
+
* --system reads the platform certificate store only, without Node's bundled roots,
|
|
22
|
+
* and fails when that store is empty and no --anchor is given
|
|
21
23
|
* pki lint <file> [--profile P] lint the structure the file holds; exit non-zero on an error finding
|
|
22
24
|
* [--severity S] [--json]
|
|
23
25
|
* pki convert <file> --to der|pem transcode a DER/PEM file between the two encodings
|
|
24
26
|
* [--label LABEL]
|
|
25
27
|
* pki verify <cert>... --anchor <cert> validate an ordered certification path (anchor->target)
|
|
26
|
-
* [--time ISO]
|
|
28
|
+
* [--time ISO] [--distrusted-key H[,H...]]
|
|
27
29
|
* pki sign <file> --cert <c> --key <k> produce a CMS SignedData over the file (pki.cms.sign)
|
|
28
30
|
* [--detached] [--pss] [--digest D] [--pem] [--out F]
|
|
29
31
|
*
|
|
32
|
+
* H in --distrusted-key is the SHA-256 digest of a certificate's DER SubjectPublicKeyInfo, as 64
|
|
33
|
+
* hexadecimal characters. The flag repeats, and one value may hold several digests separated by
|
|
34
|
+
* commas. fetch refuses an endpoint whose chain carries a listed key, and verify reports a path that
|
|
35
|
+
* carries one as invalid.
|
|
36
|
+
*
|
|
37
|
+
* A flag the verb does not take, and the --name=value form, exit non-zero with a message naming
|
|
38
|
+
* the flag and the verb's usage on stderr. A flag's value is the next argument.
|
|
39
|
+
*
|
|
30
40
|
* The CLI is a thin operator convenience over the library surface: it validates its
|
|
31
41
|
* arguments (the entry-point tier, where bad input exits non-zero with a message) and never does
|
|
32
42
|
* anything the public API cannot. inspect / lint / convert / verify / sign compose pki.inspect,
|
|
@@ -84,24 +94,65 @@ function _onOutputError(stream, e) {
|
|
|
84
94
|
process.stdout.on("error", function (e) { _onOutputError("stdout", e); });
|
|
85
95
|
process.stderr.on("error", function (e) { _onOutputError("stderr", e); });
|
|
86
96
|
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
var
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
97
|
+
// The flags each verb takes, by verb, in the order its usage line names them. "value" takes the
|
|
98
|
+
// next argument, "list" takes the next argument and may repeat (each occurrence is appended to an
|
|
99
|
+
// array), and "switch" takes none. `--help` and `-h` are answered by `main` before any verb parses
|
|
100
|
+
// its arguments, so they are not listed here.
|
|
101
|
+
var VERB_FLAGS = {
|
|
102
|
+
version: {},
|
|
103
|
+
oid: {},
|
|
104
|
+
parse: {},
|
|
105
|
+
inspect: { asn1: "switch" },
|
|
106
|
+
keygen: { out: "value", pub: "value", alg: "value", pem: "switch" },
|
|
107
|
+
csr: { key: "value", subject: "value", san: "value", out: "value", pem: "switch" },
|
|
108
|
+
issue: { key: "value", subject: "value", csr: "value", "issuer-cert": "value", "issuer-key": "value",
|
|
109
|
+
days: "value", serial: "value", ca: "switch", san: "value", out: "value", pem: "switch",
|
|
110
|
+
"copy-requested-san": "switch", "ignore-requested-extensions": "switch" },
|
|
111
|
+
fetch: { anchor: "value", system: "switch", "distrusted-key": "list", out: "value", der: "switch" },
|
|
112
|
+
lint: { profile: "value", severity: "value", json: "switch" },
|
|
113
|
+
convert: { to: "value", label: "value" },
|
|
114
|
+
verify: { anchor: "value", time: "value", "distrusted-key": "list" },
|
|
115
|
+
sign: { cert: "value", key: "value", detached: "switch", pss: "switch", digest: "value",
|
|
116
|
+
pem: "switch", out: "value" },
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// A refused command line: the reason on one line, then the verb's usage, on stderr, and a non-zero
|
|
120
|
+
// exit. The usage is this file's own text and is written as it is; the reason names what the
|
|
121
|
+
// caller typed and goes through the same escaper as every other report.
|
|
122
|
+
function usageError(verb, message) {
|
|
123
|
+
var usage = _usageFor(verb);
|
|
124
|
+
process.stderr.write("pki: " + _oneLine(message) + "\n" +
|
|
125
|
+
(usage === null ? "" : (usage.charAt(usage.length - 1) === "\n" ? usage : usage + "\n")));
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Flag parser for one verb: `--flag value` for a value or list flag, `--flag` for a switch, and
|
|
130
|
+
// every other argument a positional in `_`. A flag the verb does not take is a usage error, and so
|
|
131
|
+
// is the `--flag=value` form, which this parser does not read. A value flag whose value is absent
|
|
132
|
+
// or is itself another flag is a usage error, never coerced to `true` (`--time` would otherwise
|
|
133
|
+
// parse as `new Date(true)`, a real 1970 timestamp). Short options are not clustered.
|
|
134
|
+
function parseArgs(argv, verb) {
|
|
135
|
+
var flags = VERB_FLAGS[verb];
|
|
95
136
|
var out = { _: [] };
|
|
96
137
|
for (var i = 0; i < argv.length; i++) {
|
|
97
138
|
var a = argv[i];
|
|
98
|
-
if (a.indexOf("--")
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
139
|
+
if (a.indexOf("--") !== 0) { out._.push(a); continue; }
|
|
140
|
+
var key = a.slice(2);
|
|
141
|
+
var eq = key.indexOf("=");
|
|
142
|
+
if (eq !== -1) {
|
|
143
|
+
var name = key.slice(0, eq);
|
|
144
|
+
usageError(verb, "pki " + verb + " does not read --" + name + "=<value>; " +
|
|
145
|
+
"write the value as the next argument: --" + name + " <value>");
|
|
146
|
+
}
|
|
147
|
+
if (!Object.prototype.hasOwnProperty.call(flags, key)) {
|
|
148
|
+
usageError(verb, "pki " + verb + " does not take --" + key + "; it takes " +
|
|
149
|
+
Object.keys(flags).map(function (f) { return "--" + f; }).concat(["--help"]).join(", "));
|
|
150
|
+
}
|
|
151
|
+
var kind = flags[key];
|
|
152
|
+
if (kind === "switch") { out[key] = true; continue; }
|
|
153
|
+
if (i + 1 >= argv.length || argv[i + 1].indexOf("--") === 0) fail("--" + key + " requires a value");
|
|
154
|
+
if (kind === "list") { (out[key] = out[key] || []).push(argv[++i]); }
|
|
155
|
+
else { out[key] = argv[++i]; }
|
|
105
156
|
}
|
|
106
157
|
return out;
|
|
107
158
|
}
|
|
@@ -118,6 +169,16 @@ function usageOrArg(arg, usage) {
|
|
|
118
169
|
return arg;
|
|
119
170
|
}
|
|
120
171
|
|
|
172
|
+
// The digests every --distrusted-key occurrence names, split at commas, or undefined when the flag is
|
|
173
|
+
// absent. An empty entry is kept, so the library refuses it as a malformed digest.
|
|
174
|
+
function distrustedKeysArg(args) {
|
|
175
|
+
var given = args["distrusted-key"];
|
|
176
|
+
if (given === undefined) return undefined;
|
|
177
|
+
var list = [];
|
|
178
|
+
given.forEach(function (v) { list = list.concat(v.split(",")); });
|
|
179
|
+
return list;
|
|
180
|
+
}
|
|
181
|
+
|
|
121
182
|
// For the library entry points (parse / inspect / lint / verify) that accept EITHER a DER
|
|
122
183
|
// Buffer or a PEM string and own the decode + error handling: hand a Buffer when the bytes
|
|
123
184
|
// are a well-formed DER structure, otherwise the text so the library pemDecodes it (and
|
|
@@ -775,7 +836,8 @@ function cmdIssue(args) {
|
|
|
775
836
|
}).then(undefined, function (e) { return fail((e.code || "x509/sign-error") + ": " + e.message); });
|
|
776
837
|
}
|
|
777
838
|
|
|
778
|
-
var FETCH_USAGE = "usage: pki fetch <https-url> [--anchor <cert>] [--system] [--
|
|
839
|
+
var FETCH_USAGE = "usage: pki fetch <https-url> [--anchor <cert>] [--system] [--distrusted-key <hex>[,<hex>...]]\n" +
|
|
840
|
+
" [--out <file>] [--der]\n" +
|
|
779
841
|
" Prints the certificate chain the TLS session resolved, leaf first, as PEM. The chain is\n" +
|
|
780
842
|
" completed from the anchors in scope, so an entry above the leaf may come from a configured\n" +
|
|
781
843
|
" anchor rather than from the endpoint; the leaf is the one the endpoint certainly sent.\n" +
|
|
@@ -783,7 +845,12 @@ var FETCH_USAGE = "usage: pki fetch <https-url> [--anchor <cert>] [--system] [--
|
|
|
783
845
|
" a configured anchor and that its name matches the URL; it checked no revocation status, no\n" +
|
|
784
846
|
" policy, and nothing about what the certificate is authorized to do. pki verify is the verb\n" +
|
|
785
847
|
" that validates a path.\n" +
|
|
786
|
-
" An anchor is required: --anchor names one, --system uses the platform
|
|
848
|
+
" An anchor is required: --anchor names one, and --system uses the platform certificate store only.\n" +
|
|
849
|
+
" Node's bundled roots are not included under --system, and an empty platform store with no\n" +
|
|
850
|
+
" --anchor fails with transport/no-trust-anchors.\n" +
|
|
851
|
+
" --distrusted-key takes the SHA-256 digest of a certificate's DER SubjectPublicKeyInfo as 64\n" +
|
|
852
|
+
" hexadecimal characters. Repeat the flag or separate digests with commas. An endpoint whose\n" +
|
|
853
|
+
" chain carries a listed key fails with transport/server-key-distrusted.\n" +
|
|
787
854
|
" --der writes one certificate. A chain of several is refused under it, several concatenated DER\n" +
|
|
788
855
|
" certificates not being a DER document that pki parse can read back.";
|
|
789
856
|
|
|
@@ -796,6 +863,8 @@ function cmdFetch(args) {
|
|
|
796
863
|
var tlsOpts = {};
|
|
797
864
|
if (anchors) tlsOpts.anchors = anchors;
|
|
798
865
|
if (args.system) tlsOpts.useSystemStore = true;
|
|
866
|
+
var distrusted = distrustedKeysArg(args);
|
|
867
|
+
if (distrusted !== undefined) tlsOpts.distrustedKeys = distrusted;
|
|
799
868
|
return pki.transport.peerChain({ url: url }, { tls: tlsOpts }).then(function (channel) {
|
|
800
869
|
var chain = channel.peerChain || [];
|
|
801
870
|
if (!chain.length) return fail("fetch: the endpoint presented no certificate");
|
|
@@ -844,7 +913,10 @@ function cmdConvert(args) {
|
|
|
844
913
|
|
|
845
914
|
// pki verify <cert>... --anchor <cert> -- validate an ordered certification path
|
|
846
915
|
// (anchor->target) against a trust anchor via pki.path.validate (RFC 5280 sec. 6.1).
|
|
847
|
-
var VERIFY_USAGE = "usage: pki verify <cert>... --anchor <anchor-cert> [--time ISO]"
|
|
916
|
+
var VERIFY_USAGE = "usage: pki verify <cert>... --anchor <anchor-cert> [--time ISO] [--distrusted-key <hex>[,<hex>...]]\n" +
|
|
917
|
+
" --distrusted-key takes the SHA-256 digest of a certificate's DER SubjectPublicKeyInfo as 64\n" +
|
|
918
|
+
" hexadecimal characters. Repeat the flag or separate digests with commas. A path that carries a\n" +
|
|
919
|
+
" listed key at the anchor, an intermediate or the target is invalid with path/distrusted-key.";
|
|
848
920
|
function cmdVerify(args) {
|
|
849
921
|
var certFiles = args._, anchorFile = args.anchor;
|
|
850
922
|
if (!certFiles.length || !anchorFile) fail(VERIFY_USAGE);
|
|
@@ -856,10 +928,13 @@ function cmdVerify(args) {
|
|
|
856
928
|
var time = args.time ? new Date(args.time) : new Date();
|
|
857
929
|
if (isNaN(time.getTime())) fail("verify: --time must be an ISO-8601 date");
|
|
858
930
|
var spki = anchor.subjectPublicKeyInfo;
|
|
859
|
-
|
|
931
|
+
var validateOpts = {
|
|
860
932
|
time: time,
|
|
861
933
|
trustAnchors: { name: anchor.subject, publicKey: spki.bytes, algorithm: spki.algorithm.oid, parameters: spki.algorithm.parameters },
|
|
862
|
-
}
|
|
934
|
+
};
|
|
935
|
+
var distrusted = distrustedKeysArg(args);
|
|
936
|
+
if (distrusted !== undefined) validateOpts.distrustedKeys = distrusted;
|
|
937
|
+
return pki.path.validate(certs, validateOpts).then(function (res) {
|
|
863
938
|
process.stdout.write(res.valid ? "valid\n" : "invalid\n");
|
|
864
939
|
if (!res.valid) {
|
|
865
940
|
(res.results || []).forEach(function (r, i) {
|
|
@@ -925,19 +1000,20 @@ function main(argv) {
|
|
|
925
1000
|
process.stdout.write(verbUsage.charAt(verbUsage.length - 1) === "\n" ? verbUsage : verbUsage + "\n");
|
|
926
1001
|
return;
|
|
927
1002
|
}
|
|
1003
|
+
var rest = argv.slice(1);
|
|
928
1004
|
switch (cmd) {
|
|
929
|
-
case "version": case "--version": case "-v": return cmdVersion();
|
|
930
|
-
case "oid": return cmdOid(
|
|
931
|
-
case "parse": return cmdParse(
|
|
932
|
-
case "inspect": return cmdInspect(parseArgs(
|
|
933
|
-
case "keygen": return cmdKeygen(parseArgs(
|
|
934
|
-
case "csr": return cmdCsr(parseArgs(
|
|
935
|
-
case "issue": return cmdIssue(parseArgs(
|
|
936
|
-
case "fetch": return cmdFetch(parseArgs(
|
|
937
|
-
case "lint": return cmdLint(parseArgs(
|
|
938
|
-
case "convert": return cmdConvert(parseArgs(
|
|
939
|
-
case "verify": return cmdVerify(parseArgs(
|
|
940
|
-
case "sign": return cmdSign(parseArgs(
|
|
1005
|
+
case "version": case "--version": case "-v": parseArgs(rest, "version"); return cmdVersion();
|
|
1006
|
+
case "oid": return cmdOid(parseArgs(rest, "oid")._[0]);
|
|
1007
|
+
case "parse": return cmdParse(parseArgs(rest, "parse")._[0]);
|
|
1008
|
+
case "inspect": return cmdInspect(parseArgs(rest, "inspect"));
|
|
1009
|
+
case "keygen": return cmdKeygen(parseArgs(rest, "keygen"));
|
|
1010
|
+
case "csr": return cmdCsr(parseArgs(rest, "csr"));
|
|
1011
|
+
case "issue": return cmdIssue(parseArgs(rest, "issue"));
|
|
1012
|
+
case "fetch": return cmdFetch(parseArgs(rest, "fetch"));
|
|
1013
|
+
case "lint": return cmdLint(parseArgs(rest, "lint"));
|
|
1014
|
+
case "convert": return cmdConvert(parseArgs(rest, "convert"));
|
|
1015
|
+
case "verify": return cmdVerify(parseArgs(rest, "verify"));
|
|
1016
|
+
case "sign": return cmdSign(parseArgs(rest, "sign"));
|
|
941
1017
|
case undefined: case "help": case "--help": case "-h":
|
|
942
1018
|
process.stdout.write(USAGE);
|
|
943
1019
|
return;
|