Quick start
Read a certificate in three calls, then learn the four things every pkinative call shares: strict DER, typed errors, diagnostics and limits. Everything below runs on pkinative 1.0 as it is tested; every export named here is in
docs/assets/api.json.
Install #
pkinative is on npm, published with provenance from the tagged commit after the full gate:
npm install pkinative
npm audit signatures verifies the registry signature and the provenance of what you installed. Every GitHub release also carries the tarball, attested with Sigstore build provenance: npm install https://github.com/Nizoka/pkinative/releases/download/v1.0.0/pkinative-1.0.0.tgz, and gh attestation verify pkinative-1.0.0.tgz --repo Nizoka/pkinative checks where it was built. Every runtime loads the same build, and there is no runtime dependency. CI tests it on Node.js 22 on Linux, Windows and macOS and on Node.js 24 on Linux, with a Deno, a Bun and a headless Chromium smoke test, and on Node.js 26 (Current) in an advisory run until its LTS date; other Web Crypto runtimes, such as Cloudflare Workers, are expected to work and are not tested in CI.
Read a certificate #
A certificate usually arrives as PEM text. decodePem returns its blocks, and the label option refuses any block that is not a certificate โ a private key pasted into the same file is an error, not a certificate:
import { decodePem, formatDistinguishedName, getExtension, parseCertificate } from 'pkinative';
const [block] = decodePem(pemText, { label: 'CERTIFICATE' });
const cert = parseCertificate(block.bytes);
formatDistinguishedName(cert.subject); // 'CN=letsencrypt.org'
cert.serialNumber.hex; // '0543933be386b3a2b3add534f2103bc8b65f'
new Date(cert.validity.notAfter.epochMilliseconds);
getExtension(cert, 'subjectAltName')?.names; // [{ kind: 'dNSName', value: 'cp.letsencrypt.org', โฆ }, โฆ]
getExtension(cert, 'basicConstraints')?.cA; // false
parseCertificate takes DER bytes. The result is frozen and holds zero-copy views of your input, so do not mutate the bytes while you use it (pass bytes.slice() to decouple). Times are epochMilliseconds numbers, exact from year 0000 to 9999.
What a certificate holds #
Every field is listed with its type in docs/assets/api.json (the members of Certificate and of each extension interface); the ones you reach for first:
| Field | Holds |
|---|---|
der, tbsDer |
The whole certificate, and the tbsCertificate bytes its signature covers |
version |
1, 2 or 3 (the encoded INTEGER is 0, 1 or 2) |
serialNumber |
{ bytes, hex, value } โ the content octets, their lowercase hex, and a bigint |
signatureAlgorithm, tbsSignatureAlgorithm |
{ oid, parameters, der }; RFC 5280 requires the two to be equal (a diagnostic says when they are not) |
signatureValue |
{ bytes, unusedBits } โ the signature; verifyCertificateSignature(cert, issuer) checks it |
issuer, subject |
{ rdns, der }: each RDN a list of { type, value, valueDer }, value a { stringType, value, raw } string when the attribute is one |
validity |
{ notBefore, notAfter }, each { type, epochMilliseconds, text } |
subjectPublicKeyInfo |
{ kind, algorithm, publicKey, der, โฆ }, by kind: rsa and rsa-pss add modulus, modulusBits, publicExponent; ec adds namedCurve, curve (P-256, P-384, P-521 or undefined), pointFormat, point; ed25519, ed448, x25519, x448, ml-dsa-44, ml-dsa-65, ml-dsa-87 add key; unknown adds nothing |
issuerUniqueId, subjectUniqueId |
{ bytes, unusedBits } or undefined |
extensions, diagnostics |
In encoded order, below |
The fields of the extensions getExtension returns:
| Kind | Fields |
|---|---|
basicConstraints |
cA, pathLenConstraint (undefined when absent) |
keyUsage |
usages (digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment, keyAgreement, keyCertSign, cRLSign, encipherOnly, decipherOnly), bits |
extendedKeyUsage |
purposes: KeyPurposeId OIDs, e.g. 1.3.6.1.5.5.7.3.1 (serverAuth) |
subjectAltName, issuerAltName |
names: GeneralNames, by kind โ dNSName, rfc822Name, uniformResourceIdentifier with value; iPAddress with version, address, mask, bytes; directoryName with name; otherName with typeId, value; registeredID with oid; x400Address, ediPartyName with value |
subjectKeyIdentifier |
keyIdentifier |
authorityKeyIdentifier |
keyIdentifier, authorityCertIssuer, authorityCertSerialNumber |
authorityInfoAccess, subjectInfoAccess |
descriptions: { accessMethod, accessLocation } โ 1.3.6.1.5.5.7.48.1 is OCSP, 1.3.6.1.5.5.7.48.2 CA issuers, the location a GeneralName (usually a uniformResourceIdentifier) |
crlDistributionPoints, freshestCRL |
points: { fullName, nameRelativeToCRLIssuer, reasons, cRLIssuer } |
certificatePolicies |
policies: { policyIdentifier, qualifiers }, a qualifier cps with uri, userNotice with noticeRef and explicitText, or unknown |
policyMappings |
mappings: { issuerDomainPolicy, subjectDomainPolicy } |
policyConstraints |
requireExplicitPolicy, inhibitPolicyMapping |
inhibitAnyPolicy |
skipCerts |
nameConstraints |
permittedSubtrees, excludedSubtrees: { base, minimum, maximum }, base a GeneralName (an iPAddress base carries its mask) |
signedCertificateTimestampList |
list: the RFC 6962 SCT list in its TLS encoding |
ocspNoCheck |
nothing beyond oid, critical, valueDer |
subjectDirectoryAttributes |
attributes: { oid, values } โ the X.501 attributes of RFC 5280 ยง4.2.1.8 (RFC 3739 ยง3.2.2 identification data), values as DER |
Every extension, typed #
cert.extensions lists every extension in encoded order, each with oid, critical and valueDer, and a kind that says how it was decoded: basicConstraints, keyUsage, extendedKeyUsage, subjectAltName, issuerAltName, subjectDirectoryAttributes, subjectKeyIdentifier, authorityKeyIdentifier, nameConstraints, certificatePolicies, policyMappings, policyConstraints, inhibitAnyPolicy, authorityInfoAccess, subjectInfoAccess, crlDistributionPoints, freshestCRL, signedCertificateTimestampList, ocspNoCheck โ or unknown for an extension pkinative does not decode. getExtension(cert, kind) returns the one of that kind, typed.
An inspection tool that must show a certificate even when one extension is malformed parses with decodeExtensions: false โ every extension stays kind: 'raw' โ and decodes what it needs with decodeExtensionValue(oid, valueDer, { critical }); critical (default false) is reported on the result and decides whether an unknown extension raises PKI_DIAG_UNKNOWN_CRITICAL_EXTENSION.
computeFingerprint(der, algorithm) takes 'SHA-1', 'SHA-256', 'SHA-384' or 'SHA-512' (anything else is PKI_INVALID_OPTION); formatFingerprint(digest, { separator, letterCase }) writes 96:BC:EC:โฆ by default โ separator defaults to ':', letterCase to 'upper' (or 'lower'), so { separator: '', letterCase: 'lower' } gives the pinning form.
Diagnostics: what real issuers get wrong #
A structural violation throws; a profile violation that real issuers commit is a diagnostic. Diagnostics are recorded on cert.diagnostics, each with a code, the RFC section it cites, a path and an offset:
const cert = parseCertificate(der, { onDiagnostic: (d) => log(d.code, d.path, d.standard) });
cert.diagnostics; // the same list, in order
By default each code is also written to console.warn, once per code in each call; onDiagnostic replaces that, and strict: true turns the first warning diagnostic โ a MUST the input broke โ into a thrown PkiError with code PKI_STRICT_DIAGNOSTIC, and still reports every info โ a SHOULD it did not follow โ the choice for a verifier that accepts only clean certificates without refusing the Web PKI's own profile.
Errors: branch on the code #
Every failure is a PkiError subclass with a stable code, a message that starts with pkinative: and names the remedy, and โ for certificates โ the path and offset of the offending field:
import { parseCertificate, PkiCertificateError, PkiError, PkiLimitError } from 'pkinative';
try {
parseCertificate(der);
} catch (error) {
if (error instanceof PkiLimitError) console.error(error.limit, error.configured, error.observed);
else if (error instanceof PkiCertificateError) console.error(error.code, error.path, error.offset);
else if (error instanceof PkiError) console.error(error.code);
else throw error;
}
Never branch on the message. The error guide lists every code with its cause and remedy.
Limits #
Twenty-two named limits bound every loop over untrusted bytes. Tighten one for a context that expects small inputs, raise one only for input you trust:
parseCertificate(der, { limits: { maxExtensions: 32, maxGeneralNames: 100 } });
The security guide lists them with their defaults and CWE.
Below the certificate: ASN.1 and OIDs #
The layers the certificate parser stands on are public too. decodeAsn1 decodes one DER value (BER with encodingRules: 'ber') into frozen nodes that keep their exact bytes, and refuses anything after that value unless you pass allowTrailingData: true; typed readers turn a node into a value; the encoders emit DER only and refuse what DER cannot represent; encodeAsn1Node re-encodes a decoded tree to the exact input bytes:
import { decodeAsn1, encodeInteger, encodeObjectIdentifier, encodeSequence, getOidName, readInteger, readObjectIdentifier } from 'pkinative';
const der = encodeSequence([encodeObjectIdentifier('2.5.29.17'), encodeInteger(65537n)]);
const [oidNode, integerNode] = decodeAsn1(der).children;
readObjectIdentifier(oidNode); // '2.5.29.17'
getOidName('2.5.29.17'); // 'subjectAltName'
readInteger(integerNode); // 65537n
A node is { tagClass, tagNumber, constructed, offset, headerLength, contentLength, indefinite, bytes, content, children }: tagClass is 'universal', 'application', 'context' or 'private', bytes the whole encoding and content the content octets, both views of the input. A reader accepts its universal tag or any implicit (non-universal) tag, which it reads as its own type; readString and readTime need the type of an implicit tag โ readString(node, { stringType: 'ia5' }), readTime(node, { timeType: 'GeneralizedTime' }) โ and throw PKI_API_MISUSE without it.
The readers are readBoolean, readInteger (a bigint), readSmallInteger (a number), readNull, readBitString, readOctetString, readObjectIdentifier, readString (UTF8String, NumericString, PrintableString, TeletexString, IA5String, VisibleString, UniversalString, BMPString) and readTime (UTCTime and GeneralizedTime); readEnumerated and readRelativeOid read ENUMERATED and RELATIVE-OID (X.690 ยง8.4, ยง8.20); REAL has no typed reader: read its content octets. For OIDs, encodeOid and decodeOid convert between dotted text and content octets, isValidOid checks a string, and getOidName names 300+ registered OIDs. decodePem returns blocks of { label, bytes, headers, offset } โ headers holds the RFC 1421 [name, value] pairs lax mode reads; it takes an optional label that every block must carry, and mode: 'lax' accepts whitespace around and inside the block, long lines and RFC 1421 headers, each reported once per call as a diagnostic. OID_REGISTRY entries are { oid, name, standard }.
Types you write #
Most of what pkinative returns you never name: you reach it through a return value, and its fields are in docs/assets/api.json. These are the ones you may have to write down โ in a signature, a variable annotation or an options object.
- Certificate:
DistinguishedName(subject,issuer),PkiTime(both ends ofvalidity),Extension(the unioncertificate.extensionsholds) andDecodedExtensionKind(the argumentgetExtensiontakes). - ASN.1:
Asn1Node(whatdecodeAsn1returns and every reader takes),TagClass(node.tagClass, and the first argument ofencodeTlv),Asn1StringandAsn1StringType(whatreadStringreturns, and the eight types it can read),BitString(readBitString,encodeBitString, andcertificate.signatureValue). - PEM:
PemBlockโ whatdecodePemyields;encodePem(label, bytes)writes one back from its two parts. - Fingerprints:
FingerprintAlgorithmโ'SHA-1','SHA-256','SHA-384'or'SHA-512'. - Options, one per function, each extending
PkiParseOptions(which carrieslimits,strictandonDiagnostic):ParseCertificateOptions,DecodeExtensionValueOptions,DecodeAsn1Options,DecodePemOptions,ReadStringOptions,ReadTimeOptions; andFormatFingerprintOptions(separator,letterCase), which parses nothing and extends nothing.
Error code unions are in the errors guide.
Next #
- recipes/ โ 22 executable recipes: the quick start, the agent brief, a CA bundle, fingerprints, extensions on demand, ASN.1 and OIDs, the ASN.1 primitives, hostile input, signature verification, certificate and CSR creation, reading and judging a CSR, path validation, the one-call verification, server-name matching, extended key usage, revocation lists, OCSP, CMS signing and verification, an external signer, RFC 3161 timestamps, PKCS#8 private keys, PKCS#12 files.
- Use cases โ the jobs end to end, from a certificate inventory to a timestamped signature and the
.p12holding its key. - Conformance โ how pkinative is held to x509-limbo, NIST PKITS, Wycheproof, the text of RFC 5280 and OpenSSL.
- Standards โ every ISO/IEC, ITU-T and IETF standard pkinative touches, what holds each, and the gaps: a tooled self-assessment, not a certification.
- Choosing a library and a surface โ when pkinative is the right tool, when another one is, and whether to call it as a library, a CLI or an MCP server.