X.509 certificates: parse, sign and inspect

X.509 certificate handling per RFC 5280. The seed surface is parse, which turns a DER or PEM certificate into a structured, fully-decoded object: version, serial, signature algorithm, issuer and subject distinguished names, validity window (as real Dates), subject public-key info, and the extension list. The parser composes the strict DER codec and the OID registry, so every field is validated on the way in and every algorithm / attribute / extension is named where the registry knows it.

The raw tbsCertificate bytes are returned alongside the parsed fields. A signature-verification layer hashes exactly the bytes that were signed, with no re-encoding step whose round-trip fidelity it would have to trust.

pki.schema.x509.pemDecode

since 0.1.7 stable
pki.schema.x509.pemDecode(text, label?) -> Buffer

Extract the DER bytes from a PEM block (default label CERTIFICATE, the RFC 7468 sec. 5 armor, the canonical-label default every sibling format applies). Pass a label to enforce a different block type, or an explicit null to accept a block of any type. The text holds one object: a file of several is refused with pem/multiple-blocks and read with pki.schema.pem.decodeBundle. 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.x509.sign({ subject: "example.com", subjectPublicKey: await pki.key.export(pair.publicKey),
    notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") },
    { key: await pki.key.export(pair.privateKey) }, { pem: true });
  var der = pki.schema.x509.pemDecode(pemText);
}
example();

References

pki.schema.x509.pemEncode

since 0.1.7 stable
pki.schema.x509.pemEncode(der, label) -> string

Wrap DER bytes in a PEM envelope with 64-column base64 lines.

Example

async function example() {
  var pair = await pki.key.generate("Ed25519");
  var der = await pki.x509.sign({ subject: "example.com", subjectPublicKey: await pki.key.export(pair.publicKey),
    notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") },
    { key: await pki.key.export(pair.privateKey) });
  var pem = pki.schema.x509.pemEncode(der, "CERTIFICATE");
}
example();

References

pki.schema.x509.parse

since 0.1.7 stable
pki.schema.x509.parse(input, caps?) -> certificate

Parse a DER Buffer or a PEM string/Buffer into a structured certificate: { version, serialNumber, serialNumberHex, signatureAlgorithm, issuer, subject, validity, subjectPublicKeyInfo, issuerUniqueID, subjectUniqueID, extensions, tbsBytes, signatureValue }. Distinguished names come back both as a rendered dn string and as structured rdns; the validity window is real Dates; the two unique identifiers are { unusedBits, bytes } or null (RFC 5280 sec. 4.1.2.8); tbsBytes is the exact signed byte range for a downstream verifier.

Throws CertificateError when the bytes are not a well-formed certificate and Asn1Error when the underlying DER is malformed.

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 pemString = await pki.x509.sign({ subject: [{ commonName: "example.com" }, { organizationName: "Example" }],
    subjectPublicKey: await pki.key.export(pair.publicKey),
    notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") },
    { key: await pki.key.export(pair.privateKey) }, { pem: true });
  var cert = pki.schema.x509.parse(pemString);
  cert.subject.dn;                 // "CN=example.com, O=Example"
  cert.validity.notAfter;          // Date
  cert.signatureAlgorithm.name;    // "Ed25519" (the algorithm the issuer signed with)
}
example();

References

  • spec RFC 5280
  • spec X.509
  • defends malformed-certificate-parse (CWE-20)

pki.schema.x509.decodeExtensions

since 0.8.11 stable
pki.schema.x509.decodeExtensions(certOrBytes, opts?) -> rows

Read a certificate's extensions as decoded records. parse hands back each extension's raw extnValue octets, because what an extension means is a question the reader asks rather than one the parse answers; this is the verb that asks it. The argument is the object parse returned or the bytes parse takes, and a parse result is read rather than re-derived.

Each row is { oid, name, critical, value, scope, index, state, decoded, code, profile }, with every field always present. state is the discriminator: "decoded" when the value read as the structure its identifier names, "unrecognized" when this toolkit registers no decoder for that OID, and "undecodable" when it does and the value did not read. Read state rather than testing decoded, because a valid ocspNoCheck decodes to null.

value carries the raw octets in every arm, byte-identical to the certificate's, for a caller recomputing a hash or comparing an encoding. code is the typed error code in the undecodable arm and null elsewhere. profile reports the criticality the RFC fixes for that OID, as { criticalExpected, citation, deviates, violations }, and is null for an extension whose criticality the issuer chooses.

Nothing here is a verdict. An unrecognized critical extension is reported rather than refused, whatever opts.strict says, because RFC 5280 sec. 4.2's rule about one is the caller's to apply and refusing it would make a certificate carrying a private critical extension unreadable. The two facts that rule needs, critical and state, are on the row.

Options

- `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. Each may only
  tighten the built-in ceiling, and each bounds the decoding this call performs.

Example

async function example() {
  var pair = await pki.key.generate("Ed25519");
  var certDer = await pki.x509.sign({ subject: "leaf.example", subjectPublicKey: await pki.key.export(pair.publicKey),
    notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2027-01-01T00:00:00Z"),
    extensions: { basicConstraints: { cA: false }, subjectAltName: [{ dNSName: "leaf.example" }] } },
    { key: await pki.key.export(pair.privateKey) });
  var rows = pki.schema.x509.decodeExtensions(certDer);
  rows.filter(function (r) { return r.critical && r.state !== "decoded"; }).length;  // -> 0
  // The filter above is the RFC 5280 sec. 4.2 rule: a relying party rejects a certificate whose
  // critical extensions it cannot read.
}
example();

References

pki.schema.x509.decodeExtension

since 0.8.11 stable
pki.schema.x509.decodeExtension(ext, opts?) -> row

Read one certificate extension record as a decoded row. The argument is one element of a pki.schema.x509.parse result's extensions, as { oid, name, critical, value }, and the row is the one decodeExtensions would have produced for it, with index 0.

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. Each may only
  tighten the built-in ceiling.

Example

async function example() {
  var pair = await pki.key.generate("Ed25519");
  var certDer = await pki.x509.sign({ subject: "leaf.example", subjectPublicKey: await pki.key.export(pair.publicKey),
    notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2027-01-01T00:00:00Z"),
    extensions: { basicConstraints: { cA: false } } },
    { key: await pki.key.export(pair.privateKey) });
  var parsed = pki.schema.x509.parse(certDer);
  pki.schema.x509.decodeExtension(parsed.extensions[0]).state;  // -> "decoded"
}
example();

References

pki.x509.sign

since 0.3.0 stable
pki.x509.sign(spec, issuer, opts?) -> Promise<Buffer|string>

Build, sign, and DER-encode an X.509 certificate. spec describes the certificate to issue: subject (a string CN, an array of RDNs, or raw Name DER), subjectPublicKey (the SPKI DER of the key being certified), notBefore / notAfter (Dates), an optional serialNumber, and an optional extensions object. issuer is the signing side: { key } alone issues a self-signed certificate (issuer = subject, signed with the subject's own key); { name, publicKey, key } or { cert, key } issues a CA-signed one. The signing key key is a WebCrypto CryptoKey (a pki.key.generate private key passed directly, without exporting), a node:crypto KeyObject, a PKCS#8 private key as DER (Buffer) or PEM (string), or a signer { algorithm, publicKey, sign } for a key this process cannot export, such as one in a hardware security module, a cloud key management service, a PKCS#11 token or a PIV slot. A KeyObject signs through node:crypto, so a key that refuses to export its private half still signs. sign(bytes) returns the bytes WebCrypto would return for that algorithm, which for ECDSA is the fixed-width r || s of RFC 9053 sec. 2.1, and holds its client in its own closure, since this verb snapshots its options. A public or secret key is refused whichever form carries it. A subjectAltName (or any GeneralName) entry may be a form object ({ dNSName: "..." }) or a bare string classified fail-closed into its form ("host.example", "a@b.example", "10.0.0.1", "https://host.example/"). The signature algorithm is resolved from the signing key: RSA (PKCS#1 v1.5 or PSS via opts.pss), ECDSA, EdDSA, ML-DSA, SLH-DSA, or a composite arm, so every algorithm the toolkit signs with is available here without a per-algorithm branch.

The version is derived from the field set (v3 when extensions are present, else v1). Two extensions are emitted without being named, because RFC 5280 places them on the issuing CA: a subjectKeyIdentifier derived from the subject key by method (1), and, unless the certificate is self-signed (the subject's own key signs it and the names agree), an authorityKeyIdentifier whose keyIdentifier is the issuer certificate's SKI or, without one, the issuer key's method (1) value. subjectKeyIdentifier: false omits the first on an end entity (sec. 4.2.1.2 states a SHOULD there) and is refused on a CA (a MUST); authorityKeyIdentifier: false is honored on a self-signed certificate (sec. 4.2.1.1 allows the omission there) and refused on any other. The pre-encoded array form of extensions emits exactly what it is given. Serial bounds (positive, <= 20 octets), the validity UTCTime/GeneralizedTime cutover, the DER DEFAULT omissions (v1 tag, critical=FALSE, cA=FALSE), and the CA cross-field rules (keyCertSign and pathLenConstraint require cA=TRUE) are all enforced; a violation throws a typed CertificateError. Where the spec carries raw DER (a Name Buffer, a pre-encoded Extension, an issuer publicKey SPKI), a structural fault throws CertificateError, while a malformed leaf inside those bytes throws Asn1Error, the same two-error contract the parsers present.

extensions.nameConstraints restricts what the CA being issued may itself issue, as { permitted: [...], excluded: [...] } with at least one side present. Each entry names one GeneralName form: { dNSName: ".example.com" }, { rfc822Name: "example.com" }, { uniformResourceIdentifier: ".example.com" }, { directoryName: [{ commonName: "Sub" }] }, or { iPAddress: buf } where buf is an address followed by its mask, 8 octets for IPv4 and 32 for IPv6. A base names a namespace rather than a subject, so it may carry a leading dot and is held to the rule for a constraint base. A bare string is refused here, since the name form decides which namespace is constrained. The extension is emitted critical, which RFC 5280 sec. 4.2.1.10 requires, and it appears only on a certificate whose basicConstraints sets cA: true.

extensions.authorityInfoAccess names where to reach the issuer, as a list of { accessMethod, accessLocation }. The method is a registered name ("ocsp", "caIssuers") or a dotted OID; the location is a GeneralName in either accepted form. extensions.cRLDistributionPoints names where to fetch the CRL, and extensions.freshestCRL the delta CRL, which RFC 5280 sec. 4.2.1.15 gives the same syntax. Each takes a list whose entries are either a GeneralName standing for a single fullName, or { fullName, reasons, cRLIssuer } for the full form; reasons names RFC 5280 sec. 4.2.1.13 ReasonFlags bits, whose numbering differs from the CRLReason values pki.crl.sign takes. A distribution point naming only reasons is refused, which that clause requires. All three are emitted non-critical.

The policy machinery certification-path validation acts on is available the same way. extensions.policyConstraints takes { requireExplicitPolicy, inhibitPolicyMapping } skip counts, at least one of which RFC 5280 sec. 4.2.1.11 requires; extensions.inhibitAnyPolicy takes a bare skip count (sec. 4.2.1.14); both are emitted critical, which those clauses require. extensions.policyMappings takes a list of { issuerDomainPolicy, subjectDomainPolicy } naming registered policies or dotted OIDs, and refuses a mapping to or from anyPolicy, which sec. 4.2.1.5 forbids, the same rule pki.path.validate applies at sec. 6.1.4(a); the pre-encoded form is held to it too. That clause states a SHOULD for marking it critical. The validator processes the extension on an intermediate certificate, where sec. 6.1.4 applies the mappings to the policy tree, and treats a critical one on the TARGET certificate as unprocessed, rejecting it; pki.lint.certificate grades that case unknown-critical-extension. It is emitted non-critical unless policyMappingsCritical is set. extensions.issuerAltName takes the GeneralName forms subjectAltName takes and is emitted non-critical (sec. 4.2.1.7).

extensions.certificatePolicies takes a policy as a registered name, a dotted OID, or an entry object { oid, cps, userNotice } carrying the RFC 5280 sec. 4.2.1.4 qualifiers: cps is the certification-practice-statement URI, and userNotice takes { noticeRef, explicitText }, either or both, where noticeRef is { organization, noticeNumbers }. Every DisplayText is emitted as a UTF8String, the encoding the section names for a conforming CA, and is held to its SIZE (1..200) bound counted in characters.

extensions.authorityKeyIdentifier takes true to derive the key id from the issuer, a BufferSource key id, or an object { keyIdentifier, authorityCertIssuer, authorityCertSerialNumber } naming the issuing certificate directly (RFC 5280 sec. 4.2.1.1). The issuer name and the serial are both present or both absent, which is the rule the reader applies. Each of the two also takes true, which reads it from the issuing certificate, so the pair identifies the certificate that actually signed this one; true requires an issuer given as a certificate. The object form carries the keyIdentifier whether or not it is named (keyIdentifier: false omits it on a self-signed certificate only), and a stated key id is held to the issuer certificate's subjectKeyIdentifier when the issuer is given as a certificate carrying one (sec. 4.2.1.2).

extensions.qcStatements builds the RFC 3739 sec. 3.2.6 qualified-certificate statements, as a list of { statementId, info? }. A statement the toolkit knows encodes its own value syntax: qcCompliance and qcSSCD carry no info at all; qcType and qcIdentMethod take { types } / { methods } of object identifiers, including the registered ETSI names qctEsign, qctEseal and qctWeb; qcCClegislation and qcQSCDlegislation take { countries } of two-letter codes; qcRetentionPeriod takes { years }; qcLimitValue takes { currency, amount, exponent } with an alphabetic or numeric ISO 4217 currency; qcPDS takes { locations: [{ url, language }] }; and qcsPkixQCSyntaxV1 / V2 take { semanticsIdentifier, nameRegistrationAuthorities }, at least one of the two. Each typed info accepts only the fields its own statement defines, so a misspelled key is refused rather than dropped. Any other statement id takes info as pre-encoded DER, or omits info for a presence-only statement, since statementInfo is optional on every statement and nothing here can know an unknown syntax. pki.path.validate does not process this extension, so it is emitted non-critical unless qcStatementsCritical is set.

extensions.subjectInfoAccess takes the same { accessMethod, accessLocation } list authorityInfoAccess takes, since RFC 5280 sec. 4.2.2.2 gives it sec. 4.2.2.1's syntax; the two access methods that section defines are the registered names id-ad-caRepository and id-ad-timeStamping, the latter being a different OID from the extended-key-usage timeStamping. extensions.subjectDirectoryAttributes takes a list of { type, values } with each value as pre-encoded DER, the form its reader surfaces (sec. 4.2.1.8). Both are emitted non-critical, which those sections require. extensions.ocspNoCheck: true marks an OCSP responder certificate so a client does not ask about its revocation (RFC 6960 sec. 4.2.2.2.1): the value is NULL, as that section requires, and it is emitted non-critical, which the section says it should be.

The Active Directory Certificate Services enrollment extensions are written from the same spec. extensions.msCertificateTemplate takes { templateID, templateMajorVersion, templateMinorVersion }, the versions optional and each a DWORD, a minor version requiring a major one; extensions.msEnrollCertType takes the legacy v1 template name as a BMPString; extensions.msCaVersion takes { caKeyIndex, certIndex } or the composed DWORD; extensions.msPreviousCertHash takes the 20-octet SHA-1 thumbprint of the previous CA certificate; and extensions.msApplicationPolicies takes the certificatePolicies spec, which is what its reader applies. All five are emitted non-critical, since neither pki.path.validate nor pki.lint.certificate processes them and a critical instance is refused as unrecognized.

extensions.precertificatePoison: true issues an RFC 6962 sec. 3.1 precertificate: the poison is emitted critical with an ASN.1 NULL value, which is what stops a standard X.509v3 client from validating the precertificate as an issued certificate. extensions.signedCertificateTimestampList takes the SCTs a log returned, in the shape pki.ct.signSct and pki.ct.parseSctList use, and embeds them in the certificate issued after the log answers (sec. 3.3, at least one). It is always emitted non-critical, since a client that does not recognize the OID refuses a certificate that marks it critical. The two name opposite ends of one exchange, so a spec naming both is refused. A client recovers the signed entry from the issued certificate with pki.ct.x509CertEntry.

Before the key is used, the certificate the spec describes is linted against the RFC 5280 profile and refused with x509/profile-violation if any rule grades it error, naming the rule and its clause. Two rules are passed over: one reading the not-yet-written signature, and lint/rfc5280/unknown-critical-extension, whose requirement RFC 5280 sec. 4.2 places on a certificate-using system rather than on the issuer.

Options

- `pem` (boolean) -- return a PEM `CERTIFICATE` 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 certificate to beside RFC 5280, named
  from `pki.lint.profiles()`. `"none"` runs no rules at all and emits what the spec describes,
  which is how a deliberately non-conforming certificate is produced.

Example

async function example() {
  var pair = await pki.key.generate("Ed25519");
  var signerSpki = await pki.key.export(pair.publicKey);
  var root = await pki.x509.sign(
    { subject: "Example Root CA", subjectPublicKey: signerSpki,
      notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
      extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign", "cRLSign"], subjectKeyIdentifier: true } },
    { key: pair.privateKey });   // a WebCrypto CryptoKey signs directly (or pass a PKCS#8 DER Buffer / PEM string)
  pki.schema.x509.parse(root).subject.dn;   // "CN=Example Root CA"
}
example();

References

pki.x509.randomSerial

since 0.7.12 stable
pki.x509.randomSerial() -> BigInt

Draw a certificate serial number: 20 octets from the platform CSPRNG read as a positive integer. This is the same draw pki.x509.sign makes when spec.serialNumber is omitted, so an issuer that has to record a serial before the certificate exists gets it without re-implementing the draw and without signing first. The value satisfies the section 4.1.2.2 profile a signer enforces: positive, and at most 20 octets.

The top bit is cleared so the DER INTEGER carries no sign octet, and a zero top byte is redrawn, so every value the top byte can take is equally likely.

Example

async function example() {
  var serial = pki.x509.randomSerial();            // record this before the certificate exists
  var pair = await pki.key.generate("Ed25519");
  var der = await pki.x509.sign(
    { subject: "leaf.example", subjectPublicKey: await pki.key.export(pair.publicKey),
      notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2027-01-01T00:00:00Z"),
      serialNumber: serial },
    { key: pair.privateKey });
  pki.schema.x509.parse(der).serialNumber === serial;   // true: the issued serial is the drawn one
}
example();

References

pki.x509.extension

since 0.7.33 stable
pki.x509.extension(name, value, opts?) -> Buffer

Encode one X.509 Extension as DER, ready for the pre-encoded array form of extensions on pki.x509.sign, pki.csr.sign, or pki.crl.sign. name is one of the extension names spec.extensions takes, or a dotted OID string. For a name (or the dotted OID of one), value is the same plain form spec.extensions takes for it: the key-usage and purpose name lists, the { cA, pathLen, critical } object, the GeneralName lists, the policy, constraint, access, and distribution-point objects, the statement and template objects, true for the NULL-valued markers, and for the two key identifiers their explicit forms (the key id bytes for subjectKeyIdentifier; bytes or { keyIdentifier, authorityCertIssuer, authorityCertSerialNumber } with the key id present for authorityKeyIdentifier). The forms that derive a value from the subject key or the issuer (true) are refused here, since only the assembled spec holds those. For any other dotted OID, value is a BufferSource holding the already-encoded extnValue.

The criticality follows what the signer emits for the object form. Where RFC 5280 fixes it (the key identifiers, the constraint and policy-constraint extensions, the access and distribution extensions), or the toolkit issues one form only, opts.critical may only restate that value. keyUsage (default critical), extendedKeyUsage, certificatePolicies, policyMappings, qcStatements (default non-critical), and subjectAltName (critical exactly when the subject is empty, which the caller knows) take opts.critical; basicConstraints carries critical inside its value. An unregistered OID takes opts.critical as stated.

Options

- `critical` (boolean) -- the criticality, where the extension admits a choice.

Example

async function example() {
  var ku = pki.x509.extension("keyUsage", ["digitalSignature"]);
  var san = pki.x509.extension("subjectAltName", ["leaf.example"]);
  var custom = pki.x509.extension("1.3.6.1.4.1.99999.7", pki.asn1.build.utf8("v1"), { critical: false });
  var pair = await pki.key.generate("Ed25519");
  var der = await pki.x509.sign(
    { subject: "leaf.example", subjectPublicKey: await pki.key.export(pair.publicKey),
      notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2027-01-01T00:00:00Z"),
      extensions: [ku, san, custom] },
    { key: pair.privateKey });
  pki.schema.x509.parse(der).extensions.length;   // 3
}
example();

References

pki.x509.parseDn

since 0.8.2 stable
pki.x509.parseDn(dn) -> { rdns, dn, bytes }

Read a distinguished-name string into the Name a builder takes, using the attribute names and value escaping of RFC 4514 sec. 2.4 and sec. 3. It is the inverse of the dn string every parsed name carries: bytes is the encoded Name, ready to hand to pki.x509.sign as a subject or issuer, and dn is the string re-emitted from what was read, which for a string this toolkit produced is the string it was given.

Components are read in the order they are written, so the first one written is the first element of the encoded Name. That is the order every dn here is emitted in, and the order openssl x509 -subject prints. RFC 4514 sec. 2.1 specifies the opposite order, so a string from a directory tool that follows it names the components from last to first; reverse them before parsing one.

The string form does not carry the ASN.1 string type of each value, so a parsed value takes the type this toolkit encodes that attribute with: PrintableString for countryName, IA5String for emailAddress, UTF8String otherwise. A name written by something that chose different types will re-encode to different bytes, which is why RFC 4514 sec. 2.4 defines the #-prefixed hex form: a value given that way is the exact DER of the AttributeValue and round-trips byte for byte.

Malformed input throws x509/bad-dn. A non-string throws x509/bad-input.

bytes is raw Name DER, the form spec.subject and spec.issuer take alongside a string or an array of RDNs, so a name read from a string can be signed without rebuilding it.

Example

var name = pki.x509.parseDn("C=US, O=Example Inc, CN=leaf");
name.rdns.length;               // -> 3
name.rdns[0][0].name;           // -> "countryName"
name.dn;                        // -> "C=US, O=Example Inc, CN=leaf"
Buffer.isBuffer(name.bytes);    // -> true

References