Choosing a library and a surface
Choose pkinative for strict, dependency-free PKI in any JavaScript runtime — certificates, paths and revocation, CMS and timestamps, PKCS#8 and PKCS#12 — with every refusal explained by a stable code. Choose another tool for what it will never do, listed below.
What pkinative does #
- Reads DER (and BER on request), PEM and OIDs; every field and standard extension of an RFC 5280 certificate, with profile concerns as diagnostics; PKCS#10 certification requests, with the extensions they ask for; CRLs, OCSP responses, CMS SignedData, RFC 3161 timestamp tokens, PKCS#8 keys and PKCS#12 files under PBES2.
- Builds certificates, certification requests, OCSP requests, CMS SignedData attached or detached, and timestamp requests — signed through Web Crypto by a key you hold, or by an
ExternalSignerfor a key in an HSM or a remote service; RSA PKCS#1 v1.5 and PSS, ECDSA on P-256, P-384 and P-521, Ed25519, and Ed448 where the host has it. - Judges: RFC 5280 §6 path building and validation with name constraints and the policy tree, revocation by CRL (delta lists and scoping included) or OCSP, RFC 6125 host names, extended key usage, CMS signatures and timestamps, and a certification request's proof of possession. The one-call reports —
verifyCertificateChain,verifySignedData,verifyTimeStampToken,verifyCertificationRequest,openPkcs12— return every reason at once and never throw for a problem with the input. - Proves it: scored against x509-limbo, NIST PKITS and its S/MIME messages, clause by clause against the pinned text of RFC 5280, and against OpenSSL and Windows in both directions — the conformance guide carries the current figures and every reviewed deviation.
- Runs one build on every runtime with Web Crypto, with zero runtime dependencies: tested in CI 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.
Verifying a signature is not validating a chain: verifyCertificateSignature says the issuer's key signed these bytes, and nothing about expiry, trust, revocation, or whether that issuer was entitled to sign. verifyCertificateChain is the call that answers those.
docs/data/surfaces.json lists each capability with the library exports, CLI commands and MCP tools that provide it and the version that brought it.
What it does not do, by design #
Each of these is a recorded decision, not a gap on a roadmap: the decision records say why, and what holds the line.
| You need | Why pkinative will not | Instead |
|---|---|---|
| A legacy PKCS#12 — RC2, 3DES, the RFC 7292 Appendix B MAC | Its KDF is iterated hashing with byte arithmetic over the password — secret-dependent cryptography in JavaScript (ADR 0002) | Convert it once: openssl pkcs12 -in legacy.p12 -legacy -aes256 -out bundle.pem, then openssl pkcs12 -export -in bundle.pem -pbmac1_pbkdf2 -out modern.p12 |
| To write a PKCS#8 or PKCS#12, or to generate or export a key | Web Crypto's encrypt, wrapKey, generateKey and exportKey are refused in every version (ADR 0001, ADR 0003) |
Web Crypto or node:crypto in your own code, or OpenSSL |
| To verify DSA signatures | Web Crypto implements none of it, and pkinative implements no signature algorithm (ADR 0004) | node:crypto on Node.js |
| To fetch a CRL, an OCSP response, a missing intermediate or a timestamp | The engine does no I/O: a verifier that reached the network could be pointed at a host of an attacker's choosing (ADR 0006) | Fetch them yourself and pass the bytes |
| ETSI long-term signatures — archive timestamps, chained proof of existence | Out of scope through 1.0; atTimeStamp takes one level of evidence (ADR 0009) |
A PAdES or CAdES toolkit |
| Distinguished names matched by RFC 5280 §7.1 preparation when building chains | Names are compared by encoded bytes, as Go and webpki do; the miss is a refusal (ADR 0005) | Re-issue with consistent encodings |
The alternatives, by the facts #
Read from the npm registry on 2026-09-19 (docs/data/comparison-2026-09-19.json):
| Library | Runtime dependencies | Types bundled | ES modules | Weekly downloads |
|---|---|---|---|---|
| node-forge 1.4.0 | 0 | no | no | 27.6 M |
| asn1js 3.0.10 | 3 | yes | yes | 13.7 M |
| @peculiar/x509 2.1.0 | 11 | yes | yes | 8.2 M |
| pkijs 3.4.0 | 6 | yes | yes | 6.8 M |
| jsrsasign 11.1.5 | 0 | no | no | 1.0 M |
| micro509 0.14.0 | 0 | yes | yes | 59 |
node-forge and jsrsasign implement RSA and elliptic curves in JavaScript; pkinative will not, and leaves that arithmetic to Web Crypto. asn1js, pkijs and @peculiar/x509 are typed and modern, and bring their dependency trees with them.
When pkinative is the right choice #
- You read certificates from untrusted sources — uploads, network peers, logs — and want the parser to refuse ambiguous encodings and bound its resources.
- You ship to browsers or edge runtimes and count every dependency and every kilobyte: reading a certificate ships about 87 KB minified, describing a key file about 45 KB, and each subsystem's cost is a budget
npm run verify:bundlechecks. - You build tools that must display every field of a certificate, including the ones other parsers skip.
- You drive a library from an AI agent and want machine-readable codes, diagnostics and an API manifest.
- You need a verdict you can act on: every refusal a reason code with its clause, several at once, and the one that would have been thrown kept in
errorCode.
Choosing your surface #
pkinative reaches you three ways, and all three run the same engine: a verdict, a refusal code or an artefact does not depend on the surface that produced it. Pick the one that matches where your bytes live and who drives the call.
| Surface | For | Pick it when |
|---|---|---|
The library, npm install pkinative |
Applications and services on any runtime with Web Crypto — Node.js, Deno, Bun, browsers, edge workers | The bytes live in memory, you want the whole typed API, or the key sits in an HSM or a remote service behind an ExternalSigner — which only the library takes. You own the I/O: the engine reads no file and opens no socket |
pkinative-cli, the pkinative binary |
Shell scripts, CI jobs, operators, and programs in any language that can run a process | Certificates, CRLs and signatures sit on disk or arrive through a pipe, and you want exit codes, --dry-run and one JSON envelope per run, offline. Node.js only; keys come from a PKCS#8 or PKCS#12 file, passwords never from argv |
pkinative-mcp, the pkinative-mcp server |
AI assistants and agents in any MCP client — Claude Desktop, Claude Code, Cursor, VS Code, the MCP Inspector, or any local client over stdio or loopback Streamable HTTP | A conversation needs PKI tools: 34 typed tools, seven prompts and sandboxed file resources, a negative verdict that is data rather than an error, and no key byte or network call, ever. Node.js 22.22.2, 24.14.1 or 25.8.2 and later (the engine's floor) |
The CLI and the MCP server are thin wrappers: neither adds a PKI decision of its own, both pin pkinative ^1.0.0, both carry the engine's PKI_* codes verbatim, and both reach every runtime export of pkinative 1.0.0. What they add is the boundary the engine deliberately lacks — files and exit codes for the CLI, a sandbox and an MCP protocol surface for the server — and the errors guide shows how one failure looks on each.
Every capability docs/data/surfaces.json lists, by surface:
| Capability | Library | CLI | MCP |
|---|---|---|---|
errors — typed errors with stable codes |
PkiError and its subclasses |
every command: error.pkiCode in the --json envelope; explain |
every tool: _meta.error.code; explain_code |
limits — named, CWE-tagged bounds |
DEFAULT_PKI_LIMITS, the limits option |
the 22 engine-limit --max-* flags (plus the CLI's --max-content-size); limits |
the limits input of every engine tool, under the operator ceilings; describe_engine |
asn1-decode |
decodeAsn1, decodeAsn1Sequence |
asn1 decode (--sequence) |
decode_asn1 (sequence) |
asn1-read |
readInteger, readTime and the other typed readers |
asn1 decode --path … --read … |
decode_asn1 (nodePath, read) |
asn1-encode |
encodeSequence, encodeAsn1Node and the other encoders |
asn1 encode; asn1 decode --reencode |
encode_asn1; decode_asn1 (reencode) |
oid |
encodeOid, decodeOid, isValidOid, getOidName, OID_REGISTRY |
oid name, encode, decode, validate, list |
resolve_oid |
pem |
decodePem, encodePem |
pem decode, pem encode |
convert_pem |
fingerprint |
computeFingerprint, computeFingerprintAsync, formatFingerprint, computeKeyIdentifier, shake256 |
fingerprint (--key-id, --shake256, --webcrypto) |
compute_fingerprint; inspect_certificate (fingerprints) |
x509-parse |
parseCertificate, getExtension, decodeExtensionValue, formatDistinguishedName |
cert inspect, cert decode-extension |
inspect_certificate, decode_extension |
x509-verify |
verifyCertificateSignature, verifySelfSignature, canVerify |
cert verify-signature; doctor |
inspect_certificate (issuer, checkSelfSignature); describe_engine |
x509-create |
createCertificate, createCertificationRequest, the X.509 encoders, canSign |
cert create, csr create, cert encode; doctor |
create_certificate, create_csr, encode_x509_structure; describe_engine |
x509-csr |
parseCertificationRequest, verifyCertificationRequest |
csr inspect, csr verify |
inspect_csr |
x509-verify-chain |
verifyCertificateChain |
chain verify |
verify_certificate_chain |
x509-path |
buildCertificatePath, validateCertificatePath |
chain build, chain validate |
build_certificate_path, validate_certificate_path |
x509-server-name |
checkServerName, matchDnsName |
cert check-name, cert match-name; chain verify --host |
check_certificate_name; verify_certificate_chain (serverName) |
x509-purpose |
checkExtendedKeyUsage, KEY_PURPOSES, ANY_EXTENDED_KEY_USAGE |
cert check-purpose; --purpose on chain and cms verify |
check_certificate_purpose; purposes on verify_certificate_chain, build_certificate_path and verify_cms |
x509-crl |
parseCertificateList, findRevocation, verifyCrlSignature, checkRevocation |
crl inspect, find, verify-signature, check |
inspect_crl (find, issuer), check_crl_revocation |
x509-ocsp |
createOcspRequest, encodeOcspCertId, parseOcspResponse, verifyOcspSignature, checkOcspStatus |
ocsp request, cert-id, inspect, verify-signature, check |
create_ocsp_request (certIdOnly), inspect_ocsp_response (responder), check_ocsp_status |
cms-signed-data |
createSignedData, parseSignedData, verifySignedData, verifySignerInfoSignature, addUnsignedAttribute |
cms sign, verify, inspect, verify-signer, add-attribute |
sign_cms, verify_cms, inspect_cms (signerCertificate), amend_cms (add-unsigned-attribute) |
cms-timestamp |
createTimeStampRequest, parseTimeStampResponse, parseTimeStampToken, parseTstInfo, verifyTimeStampToken, addTimeStampToken |
tsp request, inspect, verify; cms add-timestamp |
create_timestamp_request, inspect_timestamp, verify_timestamp; amend_cms (add-timestamp) |
keys-pkcs8 |
parsePrivateKeyInfo, parseEncryptedPrivateKeyInfo, importPrivateKey, decryptPrivateKey, canDecrypt |
key inspect, key check; --key on the signing commands; doctor |
inspect_private_key (tryImport); signer.key on the signing tools; describe_engine |
keys-pkcs12 |
parsePkcs12, verifyPkcs12Mac, openSafeContents, openPkcs12 |
p12 inspect, verify-mac, bags, open; --p12 on the signing commands |
inspect_pkcs12, open_pkcs12; signer.p12 on the signing tools |
What each surface adds around the engine, and what it leaves out — an em-dash means the surface does not offer it:
| Capability | Library (pkinative) |
CLI (pkinative-cli) |
MCP (pkinative-mcp) |
|---|---|---|---|
| Where the bytes come from | Uint8Array values your code reads |
a file, or - and a pipe on stdin |
inline pem or derBase64, or a path inside the PKINATIVE_MCP_OUTPUT_DIR sandbox |
| Writing an artefact | — (the bytes are returned; the engine writes no file) | --output, exclusive unless --overwrite (an atomic rename); --dry-run writes nothing |
outputMode: 'file' inside the sandbox, never overwritten, returned as a resource_link |
| A key outside the process (HSM, KMS, remote signer) | ExternalSigner |
— (a PKCS#8 or PKCS#12 file) | — (signer.key or signer.p12) |
| Passwords | your code passes them | --password-file, --password-stdin or PKINATIVE_PASSWORD; never on the command line |
the password input or PKINATIVE_MCP_PASSWORD, never both |
| A negative verdict | valid: false and reasons[] |
the report, then exit 1 (E_VERIFY_FAILED or E_CHECK_FAILED) |
valid: false in structuredContent, never isError |
| A reproducible instant | the at option |
--at |
the at input; PKINATIVE_MCP_AT for the whole server |
| Diagnostics and strict mode | onDiagnostic, strict |
diagnostics in the envelope, --strict, --quiet |
diagnostics[] and diagnosticCounts, strict: true |
| Capability preflight | canSign, canVerify, canDecrypt |
doctor |
describe_engine |
| Machine contract | docs/assets/api.json and the registries of docs/data/ |
schema (twelve subjects), --summary, --fields |
inputSchema and outputSchema in tools/list; verbosity, fields |
| Shell completion | — | completion (bash, zsh, fish, PowerShell) |
— |
| Configuration file | — (options per call) | .pkinativerc.json, --config, --no-config — presentation defaults only |
— (the nine PKINATIVE_MCP_* variables instead) |
| Response cache | — | — | PKINATIVE_MCP_CACHE_DIR (opt-in, SHA-256 keyed) |
| Transport | an in-process import |
one process per command: the report on stdout, the --json envelope on stderr |
stdio; Streamable HTTP on loopback, with an opt-in bearer token |
| Guided workflows | — (recipes/ in the repository) |
— (--help, docs/AGENT_CONTRACT.md) |
seven prompts (prompts/list) |
| AI-governance drafter | — (repository policy, npm run verify:issue) |
— (repository policy, .github/drafts/) |
draft_governance_issue; the governance_contract and draft_issue_workflow prompts |
| Exit codes | — | 0, 1, 2; 130 and 143 on SIGINT and SIGTERM | — (isError and JSON-RPC errors) |
| Runtimes | any with Web Crypto: Node.js, Deno, Bun, browsers, edge workers | Node.js | Node.js |
| Generating or exporting a key; fetching a CRL, an OCSP response or a timestamp | — | — | — |
Honest notes #
- No surface generates or exports a key, and none fetches. Web Crypto's
generateKeyandexportKeyare refused in every version (ADR 0001), and the engine opens no socket (ADR 0006): keys come from your HSM, KMS oropenssl genpkey, and CRLs, OCSP responses and timestamps are evidence you carry in. The CLI and the MCP server build the OCSP and timestamp requests; neither sends them. - Only the CLI and the MCP sandbox write files. The engine returns bytes. The CLI writes where you point it, exclusively unless
--overwrite; the MCP server writes only underPKINATIVE_MCP_OUTPUT_DIR, never overwrites, and never writes, lists or serves a key-bearing file. - A key in an HSM or a remote service reaches the library only. Both satellites sign with a PKCS#8 or PKCS#12 file, imported as a non-extractable Web Crypto key; an
ExternalSigneris a library call. - A satellite adds no PKI decision. A
PKI_*,PKI_REASON_*orPKI_DIAG_*code means the same on every surface. A CLIE_*class groups the codes for exit codes and scripts; an MCP wrapper code names only the boundary the server owns — validation, the sandbox, caps, secrets, output. - The table is the complete story for these capabilities, not a sample: where a cell is an em-dash, the surface lacks the capability rather than hiding it under another name.
You can switch later #
All three surfaces call the same engine, so what one writes the others read: a certificate create_certificate writes into the sandbox verifies under pkinative chain verify and parses with parseCertificate, and a verdict carries the same reason codes wherever it was reached. cert create --spec and the spec input of create_certificate take the same JSON, so a certificate description moves between the CLI and the MCP server unchanged — but each surface keeps its own defaults: for a self-signed certificate cert create adds an authorityKeyIdentifier and create_certificate does not, so the same spec gives different bytes. Compare verdicts across surfaces, not hashes. Starting on the "wrong" surface costs a call-site migration, not a re-authoring.
The CLI guide and the MCP guide document every command and tool; CLI use cases and MCP use cases put them to work.