@blamejs/pki 0.5.12 → 0.5.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ 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.5.13 — 2026-08-19
8
+
9
+ `pki.csr.verify` checks an inbound certification request's proof of possession, so a CA built on this toolkit can tell that the requester holds the key they are asking it to certify.
10
+
11
+ ### Added
12
+
13
+ - `pki.csr.verify(request)` verifies a certification request's signature over its exact `certificationRequestInfo` bytes under the `subjectPKInfo` inside them (RFC 2986 sec. 4.2), the check `openssl req -verify` performs. `request` is DER, PEM, or a parsed request. It composes the one path-validation signature engine, with the same algorithm-confusion (RFC 9814 sec. 4) and EdDSA low-order-point gates, in place of the producing side's self-check, which waives that gate because it runs over a key the caller already controls. It fails closed on any import or verification fault, and malformed input throws a typed `CsrError`.
14
+ - The answer is `{ verified, subject, subjectPublicKeyInfo, attributes, certificationRequestInfoBytes }`, every field re-derived from the bytes the signature covers. Issue from those. A CA that normalizes a request before verifying it holds an object carrying its own edits, and a bare boolean would report on the signed bytes while the certificate got built from the edits; the fields travel with the verdict so the two cannot come apart.
15
+ - What `verified: true` establishes is stated in full, because the bound is the point: the producer held the private half of the key inside the request, and the subject and every requested extension are the ones covered by that signature. A CSR carries no issuer and its key is self-asserted, so a requester free to choose both can prove possession of a key they generated a moment ago under any name they like. Binding that name to an identity stays with the enrollment protocol.
16
+
17
+ ### Changed
18
+
19
+ - `pki.schema.csr.parse` now records the bytes it read, the way the certificate, CRL and CMS parsers do, and `pki.csr.verify` accepts only a result carrying that record. A parsed request presents the signed byte range and the fields that range encodes as separate properties, so a rebuilt one (`Object.assign`, a spread, a JSON round-trip) can hold a genuine requester's signed bytes beside a substituted subject: the proof of possession verifies from the recorded range while the certificate a CA issues is for a name nobody signed. Passing DER or PEM is unaffected, and a parse result used as-is still works.
20
+ - One visible consequence of that record: the raw byte views on a parsed request (`certificationRequestInfoBytes`, `tbsBytes`, `subject.bytes` and the rest) now read from the parser's own copy of the input rather than from your buffer, matching what `pki.schema.x509.parse` has always done. Writing into the buffer you passed no longer changes what an already-parsed request reports, and writing through one of those views no longer reaches your buffer. Code that read the fields is unaffected; code that relied on either aliasing needs to re-parse instead.
21
+
7
22
  ## v0.5.12 — 2026-08-19
8
23
 
9
24
  `pki.crl.isRevoked` can be asked at an instant, and a CRL that stopped speaking for that instant is refused rather than read as a clean bill of health.
package/README.md CHANGED
@@ -230,7 +230,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
230
230
  | `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk`, `encode`, `embeddedDer`, and the schema combinators |
231
231
  | `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, and the certificate-policy tree. 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. Pure and re-entrant — `validate`, `build`, `crlChecker`, `ocspChecker` |
232
232
  | `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. 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), subject alternative names, and certificate policies from the spec, taking any other extension as pre-encoded DER. 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`. Returns DER, or a PEM `CERTIFICATE` with `opts.pem`. Every arm is independently verified by OpenSSL. Parsing stays at `pki.schema.x509.parse` — `sign` |
233
- | `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, or an array of pre-encoded Extension DER), 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`. Parsing stays at `pki.schema.csr.parse` — `sign` |
233
+ | `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, or an array of pre-encoded Extension DER), 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` |
234
234
  | `pki.attrcert` | Attribute-certificate issuance (RFC 5755). `sign(spec, issuer, opts)` builds and signs an `AttributeCertificate` as an Attribute Authority: a `spec` of `holder` (exactly one of an entity name, a `baseCertificateID` reference, a `fromCertificate` binding, or an object digest), the validity window as GeneralizedTime, an optional serial (positive, at most 20 octets, randomly generated when omitted), the `attributes` (role, clearance, group, chargingIdentity, accessIdentity, authenticationInfo, or pre-encoded Attribute DER), and optional `extensions` (auditIdentity, targetInformation, noRevAvail, aaControls, acProxying, authorityKeyIdentifier, or pre-encoded Extension DER) each with its RFC 5755 criticality. An attribute certificate is never self-signed, so the `issuer` is the signing AA, supplied as `{ cert, key }` or `{ name, publicKey, key }`. The signature algorithm is resolved from the AA key, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch, and the signature is verified under the AA public key before the certificate is returned. Returns DER, or a PEM `ATTRIBUTE CERTIFICATE` with `opts.pem`; malformed input throws a typed `AttrCertError`. Parsing stays at `pki.schema.attrcert.parse` — `sign` |
235
235
  | `pki.crmf` | Certificate-request-message issuance (RFC 4211). `build(spec, key, opts)` assembles a `CertReqMessages` from a `spec` of `certReqId` (default 0, with the RFC 9483 `-1` sentinel allowed), a `certTemplate` of the requested fields (`subject`, `publicKey` as the SPKI DER of the key being certified, `validity`, requested `extensions`, an optional `version` 2), optional `controls` and `regInfo` (regToken, authenticator, utf8Pairs, oldCertID, protocolEncrKey, or pre-encoded `AttributeTypeAndValue` DER), and an optional `pop` selector. `key` (or `{ key }`) is the requester's private key: the message carries a `POPOSigningKey` proof of possession signed with the private half of `certTemplate.publicKey` and verified before the message is returned, exactly as a PKCS#10 CSR proves possession. A complete template signs the `CertRequest`; an incomplete one signs a `POPOSigningKeyInput`. The signature algorithm is resolved from the requested public key, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. `key` is optional for a `raVerified` proof. Pass an array of specs for a batch; the CA-assigned template fields are never emitted. Returns DER, or a PEM block with `opts.pem`; malformed input throws a typed `CrmfError`. Parsing stays at `pki.schema.crmf.parse` — `build` |
236
236
  | `pki.cmp` | CMP message building, transfer, and verification (RFC 9810). `build(message, opts)` assembles a protected `PKIMessage`. `message.header` carries the `sender` and `recipient` GeneralNames (including the anonymous NULL-DN) plus optional transaction metadata; `message.body` is a single-key object naming the arm — request-side `ir`, `cr`, `kur`, `p10cr`, `certConf`, `pollReq`, `genm`, `rr`, and responder-side `ip`, `cp`, `kup`, `ccp`, `rp`, `genp`, `error`, `pollRep`, `krp`, `pkiconf`. Protection is exactly one of `opts.{ key, cert }`, a signature under the sender key with the algorithm resolved from the signer certificate so RSA (PKCS#1 v1.5 / PSS), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch, or `opts.mac`, a PBMAC1 shared-secret HMAC (RFC 9481 / 9579, PBKDF2-derived). Protection covers the exact DER of the virtual `ProtectedPart` and is self-verified before the message is returned, and `protectionAlg` is derived rather than caller-set, so the message the parser accepts is coherent by construction. `transfer(url, message, opts)` carries a built message to a CMP endpoint over `pki.transport` (RFC 9811): one POST of the DER PKIMessage, with the response classified fail-closed — 200 only for success, a non-200 2xx or an un-followed 3xx refused, a 4xx or 5xx carrying a CMP error PKIMessage forwarded as the integrity-protected verdict — and protection surfaced rather than verified. `wellKnownUrl(base, opts)` builds the §3.4 `/.well-known/cmp` request-URIs. `verify(message, opts)` checks the protection on an incoming message, either a signature through the same certification-path engine `pki.crl.verify` and `pki.ocsp.verify` use, with the EdDSA low-order-point and algorithm-confusion gates, or a PBMAC1 MAC recomputed from `opts.sharedSecret` and the message's own PBKDF2 parameters and constant-time compared, over the exact `ProtectedPart` reconstructed from the parser's raw slices. It is fail-closed on an unprotected message, a legacy or KEM MAC algorithm, an omitted keyLength, or a SHA-1 PRF, and returns a `{ valid, trusted, protectionType, signer, ... }` verdict. With `opts.trustAnchors` the signer certificate is fully path-validated (RFC 5280 §6.1 plus the RFC 9483 §3.2 `keyUsage.digitalSignature` gate) at a trusted current time, or an explicit `opts.time` for historical verification and never the message's self-asserted `messageTime`, before it is reported trusted; without one the verdict is crypto-only and the signer certificate is surfaced to anchor. `session(opts)` returns a stateful enrollment session whose `enroll(request)` drives a full `ir` / `cr` / `kur` / `p10cr` transaction over the shared transport, composing `build`, `transfer`, and `verify`. Every response is protection-verified, signer-trusted, and bound to the exchange (a stable `transactionID`, a fresh-`senderNonce` and echoed-`recipNonce` chain) before its body is read, with a bounded `pollReq` / `pollRep` loop for a `waiting` status and a `certConf` / `pkiConf` (or implicit) confirmation carrying an explicit `hashAlg` for a signature algorithm that does not convey its hash. It returns a terminal `{ outcome, certificate, chain, status, trusted, confirmed, implicitConfirm, transactionID, polls, transcript }`. The signature flavor requires `opts.trustAnchors` to authenticate the CA; a verified rejection or error and an exhausted poll budget are terminal verdicts, while a tampered, untrusted, or desynchronized response is a typed throw. Returns DER, or a PEM `CMP` block with `opts.pem`; malformed input throws a typed `CmpError`. Parsing stays at `pki.schema.cmp.parse` — `build`, `transfer`, `wellKnownUrl`, `verify`, `session` |
package/lib/crl-verify.js CHANGED
@@ -11,7 +11,9 @@
11
11
  // It is not wired into index.js and is reached only by require.
12
12
 
13
13
  var guard = require("./guard-all");
14
+ var frameworkError = require("./framework-error");
14
15
 
16
+ var CrlError = frameworkError.CrlError;
15
17
  var _engine = null; // path-validate's _verifyWithSpki, injected at path-validate's module load via setEngine.
16
18
 
17
19
  function setEngine(verifyWithSpki) { _engine = verifyWithSpki; }
@@ -19,8 +21,11 @@ function setEngine(verifyWithSpki) { _engine = verifyWithSpki; }
19
21
  // Verify a parsed CRL's signature over its raw tbsCertList bytes under the issuer SubjectPublicKeyInfo DER.
20
22
  // Fail-closed to false on a non-octet-aligned signature or any engine fault (the engine never throws out).
21
23
  function verifyCrlSignature(crl, spkiBytes) {
22
- // _engine is injected by path-validate at its module load (setEngine); pki.crl.sign requires path-validate
23
- // for exactly that side-effect, so the engine is always set before a verify runs.
24
+ // pki.crl.sign requires path-validate for the injection side-effect, so the engine is set on every
25
+ // route that reaches here through the public verb. A future internal consumer that requires this
26
+ // module without it would otherwise call null and surface a raw TypeError out of a verify, which
27
+ // names neither the defect nor a verdict, so the wiring is asserted rather than assumed.
28
+ if (_engine == null) throw new CrlError("crl/bad-input", "the crl-verify signature engine is not initialized (require pki before use)");
24
29
  if (!guard.crypto.isOctetAligned(crl.signatureValue)) return Promise.resolve(false); // non-octet-aligned signature
25
30
  return _engine(crl.signatureAlgorithm, crl.signatureValue.bytes, spkiBytes, crl.tbsBytes);
26
31
  }
package/lib/csr-sign.js CHANGED
@@ -29,6 +29,8 @@ var pkix = require("./schema-pkix");
29
29
  var pkiBuild = require("./pki-build");
30
30
  var frameworkError = require("./framework-error");
31
31
  var guard = require("./guard-all");
32
+ require("./path-validate"); // side-effect: path-validate injects its signature engine into csr-verify at load
33
+ var csrVerify = require("./csr-verify");
32
34
 
33
35
  var CsrError = frameworkError.CsrError;
34
36
  var KNOWN_SPEC_KEYS = { subject: 1, subjectPublicKey: 1, extensionRequest: 1, challengePassword: 1, attributes: 1 };
@@ -175,4 +177,74 @@ function _sign(spec, key, opts) {
175
177
  });
176
178
  }
177
179
 
178
- module.exports = { sign: sign };
180
+ function _coerceCsr(request) {
181
+ return guard.parsed.acceptDerived(request, "csr", csr.parse, _err, "csr/bad-input", "the certification request");
182
+ }
183
+
184
+ /**
185
+ * @primitive pki.csr.verify
186
+ * @signature pki.csr.verify(request) -> Promise<{ verified, subject, subjectPublicKeyInfo, attributes, certificationRequestInfoBytes }>
187
+ * @since 0.5.13
188
+ * @status stable
189
+ * @spec RFC 2986 sec. 4.2
190
+ * @defends csr-proof-of-possession-bypass (CWE-347)
191
+ * @related pki.csr.sign, pki.schema.csr.parse, pki.x509.sign
192
+ *
193
+ * Verify a certification request's signature over its exact parsed `certificationRequestInfo` bytes.
194
+ * `request` is a DER `Buffer`, a PEM string, or a parsed request. A CSR carries no issuer: the
195
+ * verifying key is the `subjectPKInfo` inside the signed preimage, so this is the proof of
196
+ * possession `openssl req -verify` checks, and a CA that issues without it certifies a key the
197
+ * requester may not hold.
198
+ *
199
+ * The result carries `verified` alongside the `subject`, `subjectPublicKeyInfo`, `attributes` and
200
+ * `certificationRequestInfoBytes` that were verified, all re-derived from the request's own bytes.
201
+ * Issue from those rather than from the argument: a request normalized in place before verifying
202
+ * leaves the caller holding edited fields, and a bare boolean would answer about the signed bytes
203
+ * while the certificate got built from the edits.
204
+ *
205
+ * What `true` establishes is bounded, and the bound is the point. It says the producer held the
206
+ * private half of the key inside this request, over bytes that include the subject name and every
207
+ * requested extension, so none of them were altered after signing. It says nothing about who the
208
+ * producer is: the key is self-asserted, the name is self-asserted, and a requester free to choose
209
+ * both can prove possession of a key they generated a moment ago under any name they like. Binding
210
+ * that name to an identity is the enrollment protocol's job -- `pki.est`, `pki.cmc`, `pki.cmp`, or
211
+ * an out-of-band check -- and remains one after this returns `true`.
212
+ *
213
+ * Verification composes the one path-validation signature engine, with the same algorithm-confusion
214
+ * (RFC 9814 sec. 4 key-OID == sig-OID) and EdDSA low-order-point gates, rather than the self-check
215
+ * this module's signing side runs over a key the caller already controls. It fails closed to
216
+ * `false` on any import or verification fault; malformed input throws a typed `CsrError`.
217
+ *
218
+ * @example
219
+ * var pair = await pki.key.generate("Ed25519");
220
+ * var spki = await pki.key.export(pair.publicKey);
221
+ * var pkcs8 = await pki.key.export(pair.privateKey);
222
+ * var req = await pki.csr.sign({ subject: "CN=device-42", subjectPublicKey: spki }, { key: pkcs8 });
223
+ * var r = await pki.csr.verify(req);
224
+ * // Issue from r.subject / r.subjectPublicKeyInfo / r.attributes, which are the verified fields.
225
+ * var issued = r.verified
226
+ * ? await pki.x509.sign({ subject: r.subject.dn, subjectPublicKey: r.subjectPublicKeyInfo.bytes,
227
+ * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2027-01-01T00:00:00Z") },
228
+ * { key: pkcs8 })
229
+ * : null;
230
+ */
231
+ function verify(request) { return guard.async.deferred(function () { return _verify(request); }); }
232
+ function _verify(request) {
233
+ // The fields come off the structure the door re-derived from the recorded bytes, never off the
234
+ // argument. A caller who normalizes a parse result in place -- rewriting subject or attributes
235
+ // before verifying -- still holds their edited object afterwards, and a bare boolean would let
236
+ // them issue from it while the verdict was about something else. Handing back the verified
237
+ // structure means the fields a CA acts on are the fields the signature covers.
238
+ var parsed = _coerceCsr(request);
239
+ return Promise.resolve(csrVerify.verifyCsrSignature(parsed)).then(function (ok) {
240
+ return {
241
+ verified: ok === true,
242
+ subject: parsed.subject,
243
+ subjectPublicKeyInfo: parsed.subjectPublicKeyInfo,
244
+ attributes: parsed.attributes,
245
+ certificationRequestInfoBytes: parsed.certificationRequestInfoBytes,
246
+ };
247
+ });
248
+ }
249
+
250
+ module.exports = { sign: sign, verify: verify };
@@ -0,0 +1,44 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal -- the single PKCS#10 proof-of-possession signature seam for an INBOUND request, shared
6
+ // by pki.csr.verify and by every enrollment path that accepts one, so all of them route through one
7
+ // signature engine (algorithm-confusion + EdDSA low-order + composite gates) and no second, weaker
8
+ // verifier of untrusted requests can drift. The producing side runs its own check over what it just
9
+ // signed (pki-build's assertSignatureVerifies, called from csr-sign), which is a different question
10
+ // about a key the caller already controls. path-validate owns the engine (_verifyWithSpki) and
11
+ // injects it here at its module load (setEngine). The seam lives in this internal module, not on
12
+ // path-validate's module.exports, which keeps it off the public pki.path surface. It is not wired
13
+ // into index.js and is reached only by require.
14
+ //
15
+ // It deliberately does NOT compose pki-build's assertSignatureVerifies. That helper is a producing-
16
+ // side self-check over a key the caller already controls, and it waives the EdDSA low-order-point
17
+ // gate on exactly that reasoning. An inbound request is untrusted input, where the gate is the
18
+ // point.
19
+
20
+ var guard = require("./guard-all");
21
+ var frameworkError = require("./framework-error");
22
+
23
+ var CsrError = frameworkError.CsrError;
24
+ var _engine = null; // path-validate's _verifyWithSpki, injected at path-validate's module load via setEngine.
25
+
26
+ function setEngine(verifyWithSpki) { _engine = verifyWithSpki; }
27
+
28
+ // Verify a parsed certification request's signature over its exact certificationRequestInfo bytes
29
+ // under the subjectPKInfo the request carries. A CSR has no issuer: the verifying key is the one
30
+ // inside the signed preimage, which is what makes the signature a proof of possession and nothing
31
+ // more. Fail-closed to false on a non-octet-aligned signature or any engine fault (the engine
32
+ // never throws out).
33
+ function verifyCsrSignature(parsed) {
34
+ // pki.csr.sign requires path-validate for the injection side-effect, so the engine is set on every
35
+ // route that reaches here through the public verb. A future internal consumer that requires this
36
+ // module without it would otherwise call null and surface a raw TypeError out of a verify, which
37
+ // names neither the defect nor a verdict, so the wiring is asserted rather than assumed.
38
+ if (_engine == null) throw new CsrError("csr/bad-input", "the csr-verify signature engine is not initialized (require pki before use)");
39
+ if (!guard.crypto.isOctetAligned(parsed.signatureValue)) return Promise.resolve(false); // non-octet-aligned signature
40
+ return _engine(parsed.signatureAlgorithm, parsed.signatureValue.bytes,
41
+ parsed.subjectPublicKeyInfo.bytes, parsed.certificationRequestInfoBytes);
42
+ }
43
+
44
+ module.exports = { setEngine: setEngine, verifyCsrSignature: verifyCsrSignature };
@@ -512,11 +512,13 @@ var _CLAIMS = {
512
512
  certificate: ["tbsBytes", "subjectPublicKeyInfo", "serialNumberHex"],
513
513
  crl: ["tbsBytes", "revokedCertificates", "crlExtensions"],
514
514
  cms: ["signerInfos", "encapContentInfo"],
515
+ csr: ["certificationRequestInfoBytes", "subjectPublicKeyInfo", "attributes"],
515
516
  };
516
517
  var _WHY = {
517
518
  certificate: "the signed byte range, the signature and the fields that range encodes are separate properties of a parsed object, so a rebuilt certificate (Object.assign, spread, a JSON round-trip) could have them describe different certificates: keep a real CA certificate's signed bytes and signature, replace only its public key, and every field is still well-formed",
518
519
  crl: "the signed byte range, the revocation list and the scope extensions are separate properties of a parsed object, so a rebuilt CRL could have them describe different CRLs: empty the revocation list and a correctly signed CRL reports a revoked certificate as good",
519
520
  cms: "the signed attribute bytes, the signature, the encapsulated content and the certificates that verify it are separate properties of a parsed object, so a rebuilt SignedData could have them describe different messages: keep a genuine signer's signature and signed attributes, put other content beside them, and every part of the check passes for content that signer never signed",
521
+ csr: "the signed byte range, the key it proves possession of and the subject and requested extensions that range encodes are separate properties of a parsed object, so a rebuilt request could have them describe different requests: keep a genuine requester's signed bytes and signature, replace only the subject or the extensionRequest, and the proof of possession still verifies while the certificate a CA issues from those fields is for a name and a set of extensions nobody signed",
520
522
  };
521
523
  // @enforced-by guard-shape-reinlined -- shares the fromTrustedSource shape it composes: a door that
522
524
  // tests the claim fields itself, instead of routing here, is the re-inline both replace.
@@ -540,7 +542,7 @@ function acceptDerived(input, kind, parse, E, code, label) {
540
542
  throw E(code, who + " must be a " + kind + " DER Buffer, a PEM string, or a parsed " + kind);
541
543
  }
542
544
  }
543
- var ns = { certificate: "x509", crl: "crl", cms: "cms" }[kind];
545
+ var ns = { certificate: "x509", crl: "crl", cms: "cms", csr: "csr" }[kind];
544
546
  return fromTrustedSource(input, kind, claims, parse, E, code,
545
547
  who + " must be its DER bytes, a PEM string, or an unmodified pki.schema." + ns +
546
548
  ".parse result: " + _WHY[kind]);
@@ -40,6 +40,7 @@ var crl = require("./schema-crl");
40
40
  var ocsp = require("./schema-ocsp");
41
41
  var ocspVerify = require("./ocsp-verify");
42
42
  var crlVerify = require("./crl-verify");
43
+ var csrVerify = require("./csr-verify");
43
44
  var cmpVerify = require("./cmp-verify");
44
45
  var cmsVerify = require("./cms-verify");
45
46
  var cmpSession = require("./cmp-session");
@@ -2269,6 +2270,11 @@ function _verifyWithSpki(sigAlg, rawSig, spkiBytes, tbsBytes) {
2269
2270
  // the seam on the public pki.path surface.
2270
2271
  crlVerify.setEngine(_verifyWithSpki);
2271
2272
 
2273
+ // The same seam for PKCS#10 proof of possession, so pki.csr.verify checks an inbound certification
2274
+ // request through this ONE engine rather than the producing side's self-check, which waives the
2275
+ // EdDSA low-order-point gate because it runs over a key the caller already controls.
2276
+ csrVerify.setEngine(_verifyWithSpki);
2277
+
2272
2278
  // ---- the OCSP revocation checker (RFC 6960) -------------------------------
2273
2279
 
2274
2280
  // The OCSP response-verification core (responder authorization + CertID binding + currency +
package/lib/schema-csr.js CHANGED
@@ -185,7 +185,10 @@ var CERTIFICATION_REQUEST = pkix.signedEnvelope(NS, CERTIFICATION_REQUEST_INFO,
185
185
  * csr.subject.dn; // -> "CN=req.example"
186
186
  * csr.attributes[0].type; // -> "1.2.840.113549.1.9.14"
187
187
  */
188
- var parse = pkix.makeParser({ pemLabel: "CERTIFICATE REQUEST", PemError: PemError, ErrorClass: CsrError, prefix: "csr", what: "certification request", topSchema: CERTIFICATION_REQUEST, ns: NS });
188
+ // Recording, because pki.csr.verify reaches a verdict from this result: the certificationRequestInfo
189
+ // byte range, the key it proves possession of, and the subject and requested extensions that range
190
+ // encodes are separate properties here, so the record is what binds them to one another.
191
+ var parse = pkix.makeRecordingParser({ pemLabel: "CERTIFICATE REQUEST", PemError: PemError, ErrorClass: CsrError, prefix: "csr", what: "certification request", topSchema: CERTIFICATION_REQUEST, ns: NS }, "csr");
189
192
 
190
193
  /**
191
194
  * @primitive pki.schema.csr.pemDecode
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.5.12",
3
+ "version": "0.5.13",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:6ebd5615-4e36-40ba-85f5-0b2b1cb013fd",
5
+ "serialNumber": "urn:uuid:4bd2d60e-9fd1-4008-8d42-7a9d790812ac",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-08-19T07:54:19.360Z",
8
+ "timestamp": "2026-08-19T09:39:41.731Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/pki@0.5.12",
22
+ "bom-ref": "@blamejs/pki@0.5.13",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.5.12",
25
+ "version": "0.5.13",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
29
- "purl": "pkg:npm/%40blamejs/pki@0.5.12",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.5.13",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/pki@0.5.12",
57
+ "ref": "@blamejs/pki@0.5.13",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]