CSRs (PKCS#10 certification requests): build, sign and verify
pki.csr.sign builds a CertificationRequestInfo, signs it with the subject's own private key (proof of possession, since a CSR has no issuer), and emits a CertificationRequest (RFC 2986) that pki.schema.csr.parse, OpenSSL, and a CA enrollment pipeline all accept. Requested v3 extensions ride in a PKCS#9 extensionRequest attribute (RFC 2985) a CA copies into the issued certificate. Parsing lives at pki.schema.csr.parse.pki.csr.sign
pki.csr.sign(spec, key, opts?) -> Promise<Buffer|string>
Build, sign, and DER-encode a PKCS#10 certification request. spec describes the request: subject (a common-name string, an array of RDNs, or raw Name DER; MAY be empty), subjectPublicKey (the SPKI DER of the key being certified), and optional extensionRequest (requested v3 extensions, as an object of subjectAltName / keyUsage / extendedKeyUsage / basicConstraints / certificatePolicies / subjectKeyIdentifier / qcStatements / msCertificateTemplate / msEnrollCertType / msApplicationPolicies / subjectInfoAccess / subjectDirectoryAttributes and the rest of the subject-owned set pki.x509.sign takes, or an array of pre-encoded Extension DER) and challengePassword. An extension the issuing CA assigns (authorityKeyIdentifier, the certificate-transparency pair, the CA version and previous-certificate hash, ocspNoCheck) is not a request and is refused by name. key (or { key }) is the subject's own PKCS#8 private key, WebCrypto CryptoKey, or signer { algorithm, publicKey, sign }, so the request is self-signed to prove possession of the private half of subjectPublicKey, and that proof is verified before the request is returned. The signature algorithm is resolved from the subject key (RSA PKCS#1 v1.5 or PSS, ECDSA, EdDSA, ML-DSA, SLH-DSA, or a composite arm). Returns DER, or a PEM CERTIFICATE REQUEST with opts.pem. Malformed input throws a typed CsrError; where the spec carries raw DER (a Name Buffer, a pre-encoded requested Extension or Attribute) a malformed leaf inside those bytes throws Asn1Error instead. Certificate-request parsing is pki.schema.csr.parse.
Before the key is used, the request the spec describes is linted against the RFC 2986 profile and refused with csr/profile-violation if any rule grades it error, naming the rule and its clause.
Options
- `pem` (boolean) -- return a PEM `CERTIFICATE REQUEST` string instead of DER.
- `pss` (boolean) -- sign an RSA key with RSASSA-PSS instead of PKCS#1 v1.5.
- `digestAlgorithm` (string) -- override the message digest where the algorithm permits a choice.
- `profile` (string) -- a second lint profile to hold the request to beside RFC 2986, named from
`pki.lint.profiles()`. `"none"` runs no rules at all and emits what the spec describes, which is
how a deliberately non-conforming request is produced.
Example
async function example() {
var pair = await pki.key.generate("Ed25519");
var signerSpki = await pki.key.export(pair.publicKey);
var signerKeyPkcs8 = await pki.key.export(pair.privateKey);
var req = await pki.csr.sign(
{ subject: "req.example.com", subjectPublicKey: signerSpki,
extensionRequest: { subjectAltName: [{ dNSName: "req.example.com" }] } },
{ key: signerKeyPkcs8 });
pki.schema.csr.parse(req).subject.dn; // "CN=req.example.com"
}
example();
References
pki.csr.verify
pki.csr.verify(request) -> Promise<{ valid, verified, subject, subjectPublicKeyInfo, attributes, certificationRequestInfoBytes }>
Verify a certification request's signature over its exact parsed certificationRequestInfo bytes. request is a DER Buffer, a PEM string, or a parsed request. A CSR carries no issuer: the verifying key is the subjectPKInfo inside the signed preimage, so this is the proof of possession openssl req -verify checks, and a CA that issues without it certifies a key the requester may not hold.
The result carries verified alongside the subject, subjectPublicKeyInfo, attributes and certificationRequestInfoBytes that were verified, all re-derived from the request's own bytes. Issue from those rather than from the argument: a request normalized in place before verifying leaves the caller holding edited fields, and a bare boolean would answer about the signed bytes while the certificate got built from the edits.
What true establishes is bounded, and the bound is the point. It says the producer held the private half of the key inside this request, over bytes that include the subject name and every requested extension, so none of them were altered after signing. It says nothing about who the producer is: the key is self-asserted, the name is self-asserted, and 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 is the enrollment protocol's job (pki.est, pki.cmc, pki.cmp, or an out-of-band check), and remains one after this returns true.
Verification composes the one path-validation signature engine, with the same algorithm-confusion (RFC 9814 sec. 4 key-OID == sig-OID) and EdDSA low-order-point gates, rather than the self-check this module's signing side runs over a key the caller already controls. It fails closed to false on any import or verification fault; malformed input throws a typed CsrError.
Example
async function example() {
var pair = await pki.key.generate("Ed25519");
var spki = await pki.key.export(pair.publicKey);
var pkcs8 = await pki.key.export(pair.privateKey);
// A bare string is the commonName VALUE, so this asks for CN=device-42.
var req = await pki.csr.sign({ subject: "device-42", subjectPublicKey: spki }, { key: pkcs8 });
var r = await pki.csr.verify(req);
// Issue from r.subject / r.subjectPublicKeyInfo / r.attributes, which are the verified fields.
var issued = r.verified
? await pki.x509.sign({ subject: r.subject.dn, subjectPublicKey: r.subjectPublicKeyInfo.bytes,
notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2027-01-01T00:00:00Z") },
{ key: pkcs8 })
: null;
}
example();
References
- spec RFC 2986 sec. 4.2
- defends
csr-proof-of-possession-bypass (CWE-347)
pki.schema.csr.parse
pki.schema.csr.parse(input, caps?) -> csr
Parse a DER Buffer or a PEM (CERTIFICATE REQUEST) string into a structured PKCS#10 request: { version, subject, subjectPublicKeyInfo, attributes, certificationRequestInfoBytes, tbsBytes, signatureAlgorithm, signatureValue }. Every field is validated on the way in; a malformed CertificationRequest / CertificationRequestInfo throws a typed CsrError (csr/*) and a leaf-level codec fault surfaces as asn1/*. Attribute values are returned as raw DER buffers so an unrecognized attribute type never fails the parse; the extensionRequest attribute additionally carries its requested extensions decoded on .extensions (the { oid, name, critical, value } shape a certificate's extensions use).
Options
- `maxBytes` / `maxDepth` / `maxItems` (number) -- decode caps for this parse. Each
defaults to the matching `pki.C.LIMITS` figure and may only be set lower; a value
above it, or an option outside this set, is refused. A parse that exceeds one is
refused with the `/too-large` code of this format.
Example
async function example() {
var pair = await pki.key.generate("Ed25519");
var der = await pki.csr.sign(
{ subject: "req.example", subjectPublicKey: await pki.key.export(pair.publicKey),
extensionRequest: { subjectAltName: [{ dNSName: "req.example" }] } },
{ key: await pki.key.export(pair.privateKey) });
var csr = pki.schema.csr.parse(der);
csr.subject.dn; // -> "CN=req.example"
csr.attributes[0].type; // -> "1.2.840.113549.1.9.14"
}
example();
References
- spec RFC 2986
pki.schema.csr.pemDecode
pki.schema.csr.pemDecode(text, label?) -> Buffer
Extract the DER bytes from a PEM CSR block (default label CERTIFICATE REQUEST). Throws PemError on a missing / mismatched envelope or a non-base64 body.
Example
async function example() {
var pair = await pki.key.generate("Ed25519");
var pemText = await pki.csr.sign({ subject: "req.example", subjectPublicKey: await pki.key.export(pair.publicKey) },
{ key: await pki.key.export(pair.privateKey) }, { pem: true });
var der = pki.schema.csr.pemDecode(pemText);
}
example();
References
pki.schema.csr.pemEncode
pki.schema.csr.pemEncode(der, label?) -> string
Wrap DER bytes in a PEM CSR envelope (default label CERTIFICATE REQUEST).
Example
async function example() {
var pair = await pki.key.generate("Ed25519");
var der = await pki.csr.sign({ subject: "req.example", subjectPublicKey: await pki.key.export(pair.publicKey) },
{ key: await pki.key.export(pair.privateKey) });
var pem = pki.schema.csr.pemEncode(der);
}
example();
References
- spec RFC 7468
pki.schema.csr.decodeExtensions
pki.schema.csr.decodeExtensions(csrOrBytes, opts?) -> rows
Read the extensions a certification request asks for, as decoded records. This is the verb a CA calls to see what a request wants before deciding what to issue. The argument is the object parse returned or the bytes parse takes.
The rows come from the request's one extensionRequest attribute. A request carrying more than one is refused with csr/ambiguous-extension-request, because nothing says which of them a CA should honor; pass groups: true to read each attribute separately instead.
Each row is { oid, name, critical, value, scope, index, containerIndex, state, decoded, code, profile }, with scope "csr-requested" and containerIndex the position of the attribute the row came from. Criticality here is part of the request, not a verdict: RFC 2985 sec. 5.4.2 leaves which requested extensions to honor to the CA, and profile reports what RFC 5280 would fix for the certificate that request would produce.
Options
- `groups` (boolean) -- return `[{ attributeIndex, extensions }]`, one entry per
`extensionRequest` attribute, instead of one flat table. This is how a non-conforming request
carrying several is read.
- `strict` (boolean) -- throw the decoder's own typed error instead of reporting an
undecodable value. It does not turn an unrecognized OID into a throw.
- `maxBytes` / `maxDepth` / `maxItems` (integer) -- decode caps for this call.
Example
async function example() {
var pair = await pki.key.generate("Ed25519");
var csrDer = await pki.csr.sign({ subject: "req.example", subjectPublicKey: await pki.key.export(pair.publicKey),
extensionRequest: { subjectAltName: [{ dNSName: "req.example" }] } },
{ key: await pki.key.export(pair.privateKey) });
var rows = pki.schema.csr.decodeExtensions(csrDer);
rows.filter(function (r) { return r.name === "subjectAltName"; })[0].state; // -> "decoded"
}
example();
References
- spec RFC 2985 sec. 5.4.2
- spec RFC 5280 sec. 4.2
pki.schema.csr.decodeExtension
pki.schema.csr.decodeExtension(ext, opts?) -> row
Read one requested extension record as a decoded row, at scope "csr-requested". The argument is one element of an extensionRequest attribute's extensions, as { oid, name, critical, value }.
Options
- `strict` (boolean) -- throw the decoder's own typed error instead of reporting an
undecodable value.
- `maxBytes` / `maxDepth` / `maxItems` (integer) -- decode caps for this call.
Example
async function example() {
var pair = await pki.key.generate("Ed25519");
var csrDer = await pki.csr.sign({ subject: "req.example", subjectPublicKey: await pki.key.export(pair.publicKey),
extensionRequest: { subjectAltName: [{ dNSName: "req.example" }] } },
{ key: await pki.key.export(pair.privateKey) });
var csr = pki.schema.csr.parse(csrDer);
pki.schema.csr.decodeExtension(csr.attributes[0].extensions[0]).state; // -> "decoded"
}
example();
References
- spec RFC 2985 sec. 5.4.2