pkinative-mcp
MCP server for PKI — inspect and verify X.509 certificates, CSRs, CRLs, OCSP responses, CMS SignedData and RFC 3161 time-stamps; build and validate certificate chains (RFC 5280 path validation, name and purpose checks, offline CRL and OCSP revocation); create certificates, CSRs, CMS signatures, OCSP and time-stamp requests; open PKCS#8 and PKCS#12 without ever returning a key byte; decode ASN.1, PEM and OIDs — 34 tools and 7 prompts on the pkinative engine (zero-dependency, pure TypeScript, Web Crypto for every signature; OpenSSL 3 interoperability validated in CI), for Claude Desktop, Cursor, ChatGPT and any Model Context Protocol client. No network, ever.
✨ Features
pkinative-mcp exposes 34 tools to any MCP host, in six groups. The kind says how the result is shaped: a read tool returns facts (structuredContent, projectable with verbosity / fields); a verdict tool returns valid + reasons[] and is never isError for a negative verdict; an artefact tool delivers DER or PEM once, inline or into the sandbox. The last column names the refusal codes an agent branches on (the full table is in docs/AGENT_CONTRACT.md).
Encodings
Certificates and requests
Paths, CRLs and OCSP
CMS and time-stamps
Private keys and PKCS#12
Meta
What every tool guarantees:
- 🌐 No network, ever — no CRL / OCSP / AIA / TSA fetch, no telemetry, no GitHub, no URL from any argument, no operator-configurable endpoint. Revocation evidence and time-stamp tokens are inputs;
create_ocsp_requestandcreate_timestamp_requestproduce bytes you transport. Every tool advertisesopenWorldHint: false. - 🔑 No key byte leaves — signing keys are non-extractable Web Crypto handles; every key / PKCS#12 report is an allow-list view; key-bearing files (
.key .p8 .p12 .pfx) are never written and never served as resources;convert_pem,decode_asn1andencode_asn1refuse private-key material. There is no key generation and no key export. - 🧭 Verdicts are reports, errors are refusals — a negative verdict is
valid: false+reasons[]with the engine'sPKI_REASON_*codes (43 of them);isErroris reserved for an input the engine refuses (one of the 57PKI_*codes, passed through verbatim withdata.errorClassand a remedy) or a wrapper refusal (21 codes, each with a remedy). Branch on the code, never on the message. - 📐 Doctrine, surfaced not hidden — SHA-1 only with
allowSha1: true; RSA needs an explicitrsaScheme('pkcs1'|'pss', no default); PBES2 and PBMAC1 only (a legacy RC2 / 3DES PKCS#12 is refused with the OpenSSL 3.4 conversion remedy); DSA and ML-DSA are parsed, never verified; diagnostics (97PKI_DIAG_*codes) are not errors unlessstrict: true; the engine's 22 limits raise per call and never above the operator ceilings. - 🪙 Token-frugal — read and verdict tools accept
verbosity: 'summary'andfields: [...]; artefacts arrive once as an embeddedresourceblock or aresource_link, never duplicated intostructuredContent. - 🔁 Reproducible on request — pin
at,notBefore,signingTime,serialNumberandnonce(or let the operator pin the instant withPKINATIVE_MCP_AT) and an artefact or a verdict is a pure function of its inputs;structuredContent.sha256is the proof (docs/guides/REPRODUCIBLE.md). - 🧪 OpenSSL 3 is the judge — every artefact the server writes is read back by OpenSSL 3.x in CI (23 conformant artefacts, 5 refused canaries, 12 command families), blocking on Linux and Windows.
All artefact-producing tools support two output modes and three encodings:
base64(default) — the artefact is returned once as an embeddedresourcecontent block (PEM astext, DER asblob, with its MIME type:application/pkix-cert,application/pkcs10,application/pkcs7-signature,application/pkix-crl,application/ocsp-request,application/timestamp-query, …);structuredContentcarries{ mode, encoding, sizeBytes, sha256, summary, diagnostics }— withencoding: 'hex'the bytes land instructuredContent.hexinstead (inline only, ≤ 64 KiB).file— the artefact is written inside the sandbox configured byPKINATIVE_MCP_OUTPUT_DIR(exclusive create, never overwritten →OUTPUT_EXISTS; ≤ 1 GiB) and the result carries aresource_link. File I/O is disabled unless this variable is set; absolute paths, traversal, NUL bytes, a key-bearing or unknown extension and a symlink or junction whose real path leaves the sandbox are all rejected. The same sandbox serves everypathinput, so acreate_csr → create_certificate → verify_certificate_chainchain never re-sends the bytes.
Token-frugal reads. The read and verdict tools (and describe_engine, explain_code) accept two optional inputs:
verbosity: 'summary'— a compact scalar-only verdict. E.g.verify_certificate_chain→{ valid, reasonCodes, pathLength, evaluatedAt, revocationEvidence, diagnosticCount };inspect_certificate→ subject, issuer, serial, validity, key, extension kinds.fields: ['a', 'b.c']— projects the structured result to named dot-paths (≤ 16; arrays map over elements); composes afterverbosity. Every path that matched nothing — nested paths included — is omitted and reported in_meta.unmatchedFields(with_meta.availableFields); a path holding a__proto__,constructororprototypesegment isVALIDATION_ERROR.
Smallest "can I trust this server certificate?" probe: { "leaf": {…}, "trustAnchors": [{…}], "at": "2027-01-01T00:00:00Z", "serverName": { "host": "example.test" }, "verbosity": "summary", "fields": ["valid", "reasonCodes"] } on verify_certificate_chain.
The server also ships seven MCP prompts — governance_contract, draft_issue_workflow, verify_before_trust, issue_certificate, sign_and_timestamp, offline_revocation, open_keys_safely — and exposes every non-secret file in the sandbox as a pkinative://output/{+path} resource.
Why pkinative?
pkinative-mcp inherits every guarantee of the underlying engine:
- Zero runtime dependencies in the engine — pure TypeScript, no native bindings; Web Crypto for every signature, so no secret-dependent arithmetic runs in JavaScript (this server adds only the MCP SDK and zod: three runtime dependencies in total).
- RFC 5280 path validation (building, §6.1 validation, name constraints, policies), RFC 6125 names, RFC 6960 OCSP, RFC 5652 CMS, RFC 3161 time-stamps, RFC 2986 CSRs, RFC 5958 / 8018 PKCS#8 with PBES2, RFC 7292 / 9579 PKCS#12 with PBMAC1, strict X.690 DER with opt-in BER.
- 22 named, CWE-tagged limits on every parser loop — raised only by explicit input, never silently.
- A frozen vocabulary — 57
PKI_*error codes, 43PKI_REASON_*verdict reasons and 97PKI_DIAG_*diagnostics, vendored at the v1.0.0 tag and passed through verbatim. - What it will NOT do: no key generation, no key export, no writer for PKCS#8 / PKCS#12 / CRL / OCSP response / time-stamp response, no DSA / ML-DSA verification, no network access, ever.
🚀 Installation
# Run directly with npx (recommended for MCP clients)
npx -y pkinative-mcp
# Or install globally
npm install -g pkinative-mcp
pkinative-mcp
Requirements: Node.js ≥ 22 (the engine's floor is ^22.22.2 || ^24.14.1 || >=25.8.2; describe_engine.capabilities.nodeFloorSatisfied tells you). From source: git clone https://github.com/Nizoka/pkinative-mcp.git && cd pkinative-mcp && npm ci --ignore-scripts && npm run build && node dist/cli.js.
⚙️ Configuration
Claude Desktop
Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):
{
"mcpServers": {
"pkinative": {
"command": "npx",
"args": ["-y", "pkinative-mcp"],
"env": {
"PKINATIVE_MCP_OUTPUT_DIR": "/Users/you/Documents/mcp-pki"
}
}
}
}
Claude Code
.mcp.json at the root of the project (the repository ships one that drives the built server from dist/cli.js):
{
"mcpServers": {
"pkinative": {
"type": "stdio",
"command": "npx",
"args": ["-y", "pkinative-mcp"],
"env": { "PKINATIVE_MCP_OUTPUT_DIR": "${CLAUDE_PROJECT_DIR}/.mcp-pki" }
}
}
}
Cursor / Continue / Zed / Windsurf / Cline / Roo Code
Any MCP-compatible client that supports stdio servers will work. Use the same command + args + env triple. Example for Cursor (~/.cursor/mcp.json):
{
"mcpServers": {
"pkinative": {
"command": "npx",
"args": ["-y", "pkinative-mcp"],
"env": { "PKINATIVE_MCP_OUTPUT_DIR": "/Users/you/Documents/mcp-pki" }
}
}
}
Windsurf / Cline / Roo Code use the same shape inside their respective MCP config files.
VS Code
.vscode/mcp.json:
{
"servers": {
"pkinative": {
"type": "stdio",
"command": "npx",
"args": ["-y", "pkinative-mcp"],
"env": { "PKINATIVE_MCP_OUTPUT_DIR": "${workspaceFolder}/.mcp-pki" }
}
}
}
🌐 Supported AI Ecosystem & Clients
pkinative-mcp is designed for MCP-native environments and works with clients that support MCP over stdio or Streamable HTTP: Claude Desktop, Claude Code, Cursor, Continue, Zed, Windsurf, Cline, Roo Code, VS Code, ChatGPT (Streamable HTTP), the MCP Inspector, and any client built on the MCP TypeScript / Python SDKs. Because the server never opens a socket of its own, it is also safe to run inside air-gapped or egress-restricted hosts — which is where PKI material usually lives.
It is the third server of the "native" family, after pdfnative-mcp (PDF) and zipnative-mcp (ZIP): the same thin-wrapper architecture, the same sandbox, cache, projection and governance conventions, the same human-in-the-loop rules. The same engine is on the command line as pkinative-cli.
🔌 MCP protocol compliance
The server is built on the MCP TypeScript SDK v2 (@modelcontextprotocol/server) and speaks MCP 2026-07-28:
- Stateless serving —
server/discoverreplaces the session handshake; every result carriesresultTypeand the_metaserverInfoenvelope. Over HTTP, 2026-07-28 clients sendMcp-Method/Mcp-Nameheaders with eachPOST /mcp. - Cache hints —
tools/listandprompts/listarepublicwith a 24 httlMs,server/discoverispublicfor 1 h, andresources/list/resources/templates/list/resources/readareprivatewithttlMs: 0(sandbox files are per-host user data). - Protocol errors —
tools/callwith an unknown tool name is a JSON-RPC error (-32602,[UNKNOWN_TOOL] Unknown tool: …) rather than anisErrorresult, and an unknown resource URI is-32602(Invalid params), as the specification requires;isError: trueis reserved for execution failures. - Automatic legacy fallback — a client that opens with
initializeis served through the SDK's legacy path on both stdio and HTTP for every revision the SDK negotiates: 2024-10-07, 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25 (legacy) next to 2026-07-28 (modern). Nothing changes for existing hosts. - HTTP —
POST /mcponly;GET/DELETE /mcpanswer 405 (no SSE resumability; the server is stateless). Bound to127.0.0.1with aHost/Originguard (foreign values → 403);PKINATIVE_MCP_HTTP_TOKENadds an opt-in bearer-token gate (401+WWW-Authenticatewithout it); bodies above 256 MiB → 413. - stdio — the frame cap is 256 MiB so a multi-MiB base64 CMS message or PKCS#12 fits in one
tools/call; as in every SDK release, a request sent beforeinitializeis dropped without a reply on the legacy path. - Output schemas — every
structuredContentvalidates against the tool'soutputSchema(a 2026-07-28 MUST), includingverbosity: 'summary'andfieldsprojections: the read and verdict tools declare projectable schemas (all properties optional,additionalProperties: falsekept). Input schemas carry no$schemakeyword and no$refby policy (some hosts forwardinputSchemato function-calling APIs that reject unknown keywords).serverInfocarrieswebsiteUrl; the resource template ispkinative://output/{+path}.
The tools/call payload (content, structuredContent, isError) is identical between the 2026-07-28 path and the legacy path; tests/http-modern.test.ts asserts it, and tests/schema-conformance.test.ts validates structuredContent with the SDK's JSON Schema 2020-12 validator.
Compatibility matrix — every negotiated revision, on both transports, with the test that proves it:
Environment variables
All nine are declared in server.json, read once at boot; an invalid value (a PKINATIVE_MCP_PORT outside 1–65535, a relative PKINATIVE_MCP_OUTPUT_DIR, a malformed instant or ceiling) refuses to start — never a silent fallback. Only the presence of a secret is logged.
The operator ceilings. The engine's 22 named PkiLimits (maxInputBytes, maxDepth, maxNodes, maxIntegerBytes, maxOidBytes, maxBerSegments, maxPemBlocks, maxExtensions, maxGeneralNames, maxNameAttributes, maxPolicies, maxChainLength, maxPolicyNodes, maxRevokedCertificates, maxOcspSingleResponses, maxPathsExplored, maxSignerInfos, maxAttributes, maxCmsCertificatesAndCrls, maxKdfIterations, maxPkcs12Bags, maxPkcs12KdfIterations) can be raised per call through the limits fragment for trusted input only. Three of them are bounded by the three operator ceilings above: a value above the ceiling is refused with LIMIT_CEILING_EXCEEDED and data.env names the variable an operator would raise. There is no "unbounded" and no ceiling a tool argument can move. describe_engine reports the defaults, the ceilings and every cap in force.
🛠 Tool reference
Every PKI object input is an object with exactly one of pem (RFC 7468 text), derBase64 (the DER as base64, exactly once — no data: URI, not PEM) or path (relative, inside the sandbox; .pem .crt .cer .der .csr .p10 .crl .p7s .p7b .p7c .p7m .cms .p12 .pfx .key .p8 .pub .ocsp .ors .orq .tsq .tsr .tst .bin .asn1). Every engine-touching tool takes the PARSE fragment (encodingRules, pemMode, strict, limits); the read and verdict tools take verbosity and fields; the artefact tools take outputMode, outputPath and encoding. The fragments are described in Shared input fragments. Error results: content[0].text = "<tool> failed [CODE]: message", isError: true, _meta.error = { code, message, data? }. Full input / output schemas are advertised by tools/list; the consumer contract — catalogue, decision tree, recipes and the complete error table — is docs/AGENT_CONTRACT.md. Every example below is an executable call from examples/ or a tool's _meta.examples (the fixtures/… paths are the TEST-ONLY PKI the examples runner copies into the sandbox; test-only-password is their published password).
convert_pem
The RFC 7468 layer (catalogue). Inputs: mode (decode | encode), input (PKI_INPUT; encode takes exactly one DER value), label, extractIndex, PARSE, PROJECTION, OUTPUT (encoding der / pem / hex). Output: blocks[] (index, label, sizeBytes, offset, sha256), count, extracted, and the artefact when a block is extracted or DER is wrapped. Codes: SECURITY_VIOLATION (a private-key block, or key material anywhere in the input — a key behind a prefix or among other blocks included — under any label), NOT_FOUND (an index past the last block), VALIDATION_ERROR, PKI_PEM_UNEXPECTED_LABEL, PKI_PEM_*.
{ "mode": "decode", "input": { "path": "fixtures/leaf.crt.pem" }, "label": "CERTIFICATE", "extractIndex": 0, "encoding": "hex", "fields": ["extracted", "artefact"] }
resolve_oid
Object identifiers in one call. Inputs: mode (name | validate | list | encode | decode), oids[] (≤ 1000), filter, offset / limit (200 default, 2000 max), form (content | tlv), relative, hex or input (a PKI_INPUT — pem / derBase64 / path — as the decode source), PARSE, PROJECTION. Output: per mode — oids[] with names or validity, entries[] + total / hasMore, or one oid + name. encode answers hex per OID, never an artefact: a PEM-wrapped OID is encode_asn1 { spec: { type: 'oid', value }, encoding: 'pem', label }. Codes: VALIDATION_ERROR (a member outside its mode, relative without form: 'tlv', both hex and input), PKI_OID_INVALID (encode / decode; validate reports it instead), PKI_ASN1_*.
{ "mode": "name", "oids": ["1.2.840.10045.4.3.2", "2.5.29.17", "1.3.6.1.5.5.7.3.1", "1.2.3.4"] }
compute_fingerprint
The digest of one object. Inputs: input (PKI_INPUT, any label or DER; a bundle is refused), mode (fingerprint | key-id | shake256), algorithm, webcrypto, outputLength (≤ 1 MiB), separator, letterCase, PARSE, PROJECTION. Output: hex, formatted, algorithm, sizeBytes, label, from (where the public key came from in key-id). Codes: VALIDATION_ERROR (an algorithm outside its mode, SHA-384 / 512 for a key identifier), PKI_X509_* / PKI_ASN1_* (key-id on an object without a public key).
{ "input": { "path": "fixtures/leaf.csr.pem" }, "mode": "key-id", "fields": ["formatted", "from"] }
decode_asn1
The X.690 node tree. Inputs: input (PKI_INPUT — never a private key or a PKCS#12), sequence, nodePath ('0.2.1'), read (boolean | integer | small-integer | enumerated | null | bit-string | octet-string | oid | relative-oid | string | time), stringType, timeType, reencode, label (the PEM label of a reencode artefact when the input had none or a sub-node is selected), allowTrailingData, maxPreviewBytes (default 32), PARSE, PROJECTION, OUTPUT. Output: nodes[] (path, offset, lengths, tag, constructed, children, preview, oid + oidName), nodeCount, truncated (past 20 000 nodes), value (the typed read), reencode.identical, artefact. Codes: SECURITY_VIOLATION, NOT_FOUND (a missing nodePath step, naming the range), PKI_ASN1_UNEXPECTED_TAG (a wrong reader), PKI_API_MISUSE (an IMPLICIT string / time without stringType / timeType), PKI_ASN1_*, ENGINE_ERROR (the call stack exhausted inside a raised limits.maxDepth — the message names the remedy: lower it).
{ "input": { "path": "fixtures/leaf.crt.der" }, "nodePath": "0.1", "read": "integer", "verbosity": "summary" }
encode_asn1
A JSON node spec to DER. Inputs: spec (one node { type, … }), label (for encoding: 'pem'), PARSE, OUTPUT (encoding der / hex / pem; default der). Output: the artefact (sizeBytes, sha256, hex or the resource) and summary (root tag, node count). Codes: VALIDATION_ERROR naming the JSON path ($.children[1].value …), SECURITY_VIOLATION (a private-key label, or a value that parses as a key or a PKCS#12), PKI_LIMIT_EXCEEDED (nesting past limits.maxDepth), PKI_ASN1_*, PKI_OID_INVALID.
{ "spec": { "type": "sequence", "children": [{ "type": "oid", "value": "1.2.840.10045.4.3.2" }] }, "encoding": "hex" }
inspect_certificate
The RFC 5280 report. Inputs: certificate (PKI_INPUT), extension (one of the 19 kinds), rawExtensions, issuer (PKI_INPUT) or checkSelfSignature, requireAlgorithmMatch, fingerprints[], allowSha1, PARSE, PROJECTION. Output: version, serialNumber, signatureAlgorithm, issuer / subject (+ issuerString / subjectString), validity, subjectPublicKeyInfo, extensions[], selfIssued, fingerprints, signature (valid / algorithm, when checked), diagnostics[]. Codes: NOT_FOUND (an extension the certificate lacks), PKI_PEM_UNEXPECTED_LABEL (a key given as a certificate — refused by label before any byte is parsed), PKI_X509_*, PKI_CRYPTO_ALGORITHM_REFUSED (SHA-1 without allowSha1).
{ "certificate": { "path": "fixtures/leaf.crt.pem" }, "issuer": { "path": "fixtures/inter.crt.pem" }, "fingerprints": ["SHA-256", "SHA-1"], "fields": ["signature", "extensions.kind", "subjectString", "fingerprints"] }
create_certificate
Issue a certificate. Inputs: spec (serialNumber — integer, hex string or 'random', subject, issuer, notBefore, notAfter / validityDays, extensions — basicConstraints, keyUsage, extendedKeyUsage, subjectAltName, issuerAltName, SKI / AKI, raw), issuerCertificate (PKI_INPUT; omitted = self-signed), subjectPublicKey (a certificate, a CSR or a PUBLIC KEY), signer (SIGNER), allowSha1, PARSE, OUTPUT (default pem). Output: the artefact and summary (serialNumber, randomSerial, subject, issuer, selfSigned, signatureAlgorithm, notBefore / notAfter with notBeforeSource, the extension kinds, signatureVerified, publicKeyFrom). Codes: KEY_MISMATCH (the new certificate did not verify against the issuer or itself), RSA_SCHEME_REQUIRED, KEY_TYPE_REQUIRED, SECRET_REQUIRED, UNSUPPORTED_KEY, VALIDATION_ERROR (an unknown spec member, SHA-1 without allowSha1, notAfter before notBefore).
{
"spec": {
"serialNumber": 1,
"subject": { "C": "FR", "O": "Acme", "CN": "Acme Test CA" },
"notBefore": "2027-01-01T00:00:00Z",
"validityDays": 3650,
"extensions": { "basicConstraints": { "ca": true, "pathLen": 0 }, "keyUsage": ["keyCertSign", "cRLSign"] }
},
"signer": { "key": { "path": "fixtures/ed25519.key.pem" } }
}
encode_x509_structure
One building block to DER. Inputs: structure (name | name-attribute | validity | spki | algorithm-identifier | attribute | extension | extensions | basic-constraints | key-usage | extended-key-usage | subject-alt-name | subject-key-identifier | authority-key-identifier | signature-algorithm), spec (a closed object), signer (signature-algorithm only — the key is imported non-extractable and only its algorithm is read), label, allowSha1, PARSE, OUTPUT (default hex). Output: the artefact (hex by default) and summary. Codes: VALIDATION_ERROR naming spec <member> (an unknown or missing member is never dropped silently), KEY_TYPE_REQUIRED, SECRET_REQUIRED, RSA_SCHEME_REQUIRED.
{ "structure": "subject-alt-name", "spec": { "dns": ["svc.example.test", "*.wild.example.test"], "ip": ["192.0.2.1"] } }
decode_extension
One extnValue by OID. Inputs: oid, valueHex or value (PKI_INPUT — the DER inside the OCTET STRING), critical, PARSE, PROJECTION. Output: kind, oid, name, critical, sizeBytes, decoded (the kind's members), diagnostics[] (the criticality check). Codes: PKI_X509_EXTENSION_MALFORMED (remedy: decode_asn1), VALIDATION_ERROR.
{ "oid": "2.5.29.19", "valueHex": "30030101ff", "critical": true }
check_certificate_name
The RFC 6125 identity verdict. Inputs: certificate form — certificate (PKI_INPUT) + host or ip, chain[] (PKI_BUNDLE, for the CN fallback), allowCommonNameFallback (default false), allowWildcards (default true); match form — presented + reference; PARSE, PROJECTION. Output: form, valid, reasons[], reference, subject / presented, match. Codes: PKI_REASON_NAME_MISMATCH and friends in reasons[] (a verdict, never isError); VALIDATION_ERROR (both forms at once).
{ "certificate": { "path": "fixtures/leaf.crt.pem" }, "host": "a.wild.example.test", "verbosity": "summary" }
check_certificate_purpose
The RFC 5280 §4.2.1.12 purpose verdict. Inputs: certificate, chain[] (leaf's issuer first), purpose (serverAuth | clientAuth | codeSigning | emailProtection | timeStamping | ocspSigning | any | a dotted OID), restrictIssuers (default true), requireExplicitPurpose (default false), PARSE, PROJECTION. Output: valid, reasons[], purposeOid, purposeName, subject, pathLength. Codes: PKI_REASON_PURPOSE_NOT_PERMITTED (with the path[i] element) in reasons[]; VALIDATION_ERROR (an unknown purpose name).
{ "certificate": { "path": "fixtures/leaf.crt.pem" }, "purpose": "codeSigning" }
inspect_csr
The RFC 2986 report. Inputs: request (PKI_INPUT), rawExtensions, verify (default true — the proof of possession, one Web Crypto verification), allowSha1, PARSE, PROJECTION. Output: subject (+ subjectString), subjectPublicKeyInfo, attributes[], extensions[], challengePassword (present or not, never its text), signatureAlgorithm, signature (valid, reasons[]). Codes: PKI_REASON_SIGNATURE_NOT_CHECKED in signature.reasons[] (SHA-1 without allowSha1), PKI_X509_*, PKI_PEM_UNEXPECTED_LABEL.
{ "request": { "path": "fixtures/leaf.csr.pem" }, "verbosity": "summary" }
create_csr
A PKCS#10 request. Inputs: spec (subject, extensions — subjectAltName, keyUsage, extendedKeyUsage, basicConstraints, subjectKeyIdentifier, raw; the issuer's extensions are refused), subjectPublicKey (required for an encrypted PKCS#8 key), signer (SIGNER), allowSha1, PARSE, OUTPUT (default pem). Output: the artefact and summary (subject, signatureAlgorithm, the requested extension kinds, signatureVerified, publicKeyFrom). Codes: KEY_MISMATCH (the key does not match the public key named), RSA_SCHEME_REQUIRED, SECRET_REQUIRED, KEY_TYPE_REQUIRED, VALIDATION_ERROR.
{
"spec": {
"subject": { "C": "FR", "O": "Acme", "CN": "svc.example.test" },
"extensions": { "subjectAltName": { "dns": ["svc.example.test", "www.svc.example.test"] }, "keyUsage": ["digitalSignature"], "extendedKeyUsage": ["serverAuth"] }
},
"signer": { "key": { "path": "fixtures/leaf.key.pem" } }
}
verify_certificate_chain
The RFC 5280 §6 verdict. Inputs: leaf (PKI_INPUT), TRUST (trustAnchors[], candidates[], crls[], ocspResponses[], at, requireRevocation, purposes[], policy), serverName (host | ip), ocspNonce, requireOcspNonce, allowSha1, PARSE, PROJECTION. Output: valid, reasons[], path[] (leaf first up to the anchor), pathLength, explored, signatureVerifications[], evaluatedAt + evaluatedAtSource, revocationEvidence. Codes: PKI_REASON_* in reasons[] — NO_TRUST_ANCHOR, EXPIRED, NOT_YET_VALID, REVOKED, REVOCATION_UNKNOWN, REVOCATION_STALE, NAME_MISMATCH, PURPOSE_NOT_PERMITTED, … (a verdict, never isError); VALIDATION_ERROR, PKI_* for inputs the engine refuses.
{
"leaf": { "path": "fixtures/revoked.crt.pem" },
"candidates": [{ "path": "fixtures/inter.crt.pem" }],
"trustAnchors": [{ "path": "fixtures/root.crt.pem" }],
"crls": [{ "path": "fixtures/inter.crl.pem" }],
"ocspResponses": [{ "path": "fixtures/revoked.ocsp.der" }],
"requireRevocation": true,
"at": "2027-01-01T00:00:00Z",
"fields": ["valid", "reasons.code", "revocationEvidence", "pathLength"]
}
build_certificate_path
Path discovery, signatures not checked. Inputs: leaf, candidates[], trustAnchors[], at, purposes[], policy, PARSE, PROJECTION. Output: found, structuralReasons[], path[], pathLength, explored, valid (always false), reasons[], signaturesChecked: false. Codes: PKI_REASON_SIGNATURE_NOT_CHECKED (always present), PKI_REASON_NO_TRUST_ANCHOR (in structuralReasons[] when no path reached an anchor).
{ "leaf": { "path": "fixtures/leaf.crt.pem" }, "candidates": [{ "path": "fixtures/inter.crt.pem" }], "trustAnchors": [{ "path": "fixtures/root.crt.pem" }], "at": "2027-01-01T00:00:00Z", "verbosity": "summary" }
validate_certificate_path
RFC 5280 §6.1 on an ordered path. Inputs: path[] (PKI_BUNDLE, leaf first), trustAnchors[], at, checkSignatures (default true), policy, allowSha1, PARSE, PROJECTION. Output: valid, reasons[], failingReasons[], path[], signaturesChecked, signatures[] (one verdict per link), evaluatedAt. Codes: PKI_REASON_SIGNATURE_INVALID, PKI_REASON_SIGNATURE_NOT_CHECKED (checkSignatures: false), VALIDATION_ERROR.
{ "path": [{ "path": "fixtures/leaf.crt.pem" }, { "path": "fixtures/inter.crt.pem" }], "trustAnchors": [{ "path": "fixtures/root.crt.pem" }], "at": "2027-01-01T00:00:00Z" }
inspect_crl
The RFC 5280 §5 report. Inputs: crl (PKI_INPUT), find (serialHex | certificate), issuer (PKI_INPUT, one signature check), allowSha1, PARSE, PROJECTION. Output: issuer (+ issuerString), signatureAlgorithm, thisUpdate / nextUpdate, crlNumber, isDelta, baseCrlNumber, issuingDistributionPoint, entryCount, extensions[], entry (revoked, revocationDate, reason, invalidityDate), signature. Codes: VALIDATION_ERROR, PKI_X509_*, PKI_CRYPTO_ALGORITHM_REFUSED.
{ "crl": { "path": "fixtures/inter.crl.der" }, "find": { "certificate": { "path": "fixtures/revoked.crt.pem" } }, "fields": ["entry", "issuerString", "crlNumber", "nextUpdate.iso"] }
check_crl_revocation
One certificate against one CRL. Inputs: certificate, crl, delta, issuer (the CRL issuer — without it the list is not accepted as evidence: valid: false with PKI_REASON_REVOCATION_UNKNOWN, a verdict), at, staleTolerance (ms), allowSha1, PARSE, PROJECTION. Output: valid, reasons[], signatureVerified, deltaSignatureVerified, crlNumber, isDelta, deltaCrlNumber, evaluatedAt. A delta whose signature does not verify against issuer is not applied: valid: false with PKI_REASON_REVOCATION_UNKNOWN on delta. Codes: PKI_REASON_REVOKED (with the reason), PKI_REASON_REVOCATION_UNKNOWN and the freshness / scope reasons in reasons[]; VALIDATION_ERROR (a delta without a deltaCRLIndicator).
{ "certificate": { "path": "fixtures/ed25519.crt.pem" }, "crl": { "path": "fixtures/ed25519.crl.der" }, "delta": { "path": "fixtures/ed25519-delta.crl.der" }, "issuer": { "path": "fixtures/ed25519.crt.pem" }, "at": "2027-01-01T00:00:00Z" }
create_ocsp_request
The RFC 6960 request you POST yourself. Inputs: certificate, issuer, hashAlgorithm (SHA-1 default | SHA-256), nonce ('random' or 1–32 bytes of hex), certIdOnly, label (the PEM label for encoding: 'pem'), PARSE, OUTPUT (default der, .orq). Output: the artefact and summary (certIdHex, nonceHex, randomNonce, serial). Codes: VALIDATION_ERROR.
{ "certificate": { "path": "fixtures/leaf.crt.pem" }, "issuer": { "path": "fixtures/inter.crt.pem" }, "hashAlgorithm": "SHA-256", "nonce": "random", "encoding": "hex" }
inspect_ocsp_response
The RFC 6960 report. Inputs: response (PKI_INPUT), responder (PKI_INPUT, one signature check; omitted = the embedded certificates are tried), PARSE, PROJECTION. Output: status, basicResponse (responder id, producedAt, responses[] with CertID / status / updates, extensions, certificates, signature algorithm), noncePresent, signature (valid, source). Codes: PKI_ASN1_*, PKI_X509_*, VALIDATION_ERROR.
{ "response": { "path": "fixtures/revoked.ocsp.der" }, "responder": { "path": "fixtures/ocsp.crt.pem" }, "fields": ["signature", "basicResponse.responses.status", "basicResponse.responses.certId.serialNumber", "noncePresent"] }
check_ocsp_status
One response for one certificate. Inputs: response, certificate, issuer, responder, responderTrusted, hashAlgorithm, nonce (hex), requireNonce, at, staleTolerance, futureTolerance, allowSha1, PARSE, PROJECTION. Output: valid, reasons[], status (good | revoked | unknown | null), signatureVerified, responderAuthorized, responder, responderSource, hashAlgorithm, certIdHex, evaluatedAt. Codes: PKI_REASON_REVOKED, the freshness, nonce and authorisation reasons in reasons[] (a non-successful response status is a negative verdict with status: null); VALIDATION_ERROR.
{ "response": { "path": "fixtures/leaf.ocsp.der" }, "certificate": { "path": "fixtures/leaf.crt.pem" }, "issuer": { "path": "fixtures/inter.crt.pem" }, "nonce": "1538349c433373ed058c291909df5b3e", "requireNonce": true, "at": "2027-01-01T00:00:00Z", "verbosity": "summary" }
sign_cms
An RFC 5652 SignedData. Inputs: content (CONTENT_INPUT) or contentDigest (HEX, detached only), detached, contentType (OID), certificate, chain[], crls[], sid (issuerAndSerialNumber default | subjectKeyIdentifier), signingTime (an instant, 'now', 'none'), signingCertificateV2, algorithmProtection, signedAttributes[] / unsignedAttributes[] (DER Attributes as hex), signer (SIGNER), allowSha1, PARSE, OUTPUT (default der, .p7s). Output: the artefact and summary (detached, contentType, signerSubject / signerSerial, sid, digest and signature algorithms, signingTime with signingTimeSource, certificatesEmbedded, crlsEmbedded, attribute counts, signatureVerified). Codes: KEY_MISMATCH (the new signature did not verify against the signer certificate), RSA_SCHEME_REQUIRED, SECRET_REQUIRED, KEY_TYPE_REQUIRED, VALIDATION_ERROR (contentDigest without detached, certificate missing with signer.key), SECURITY_VIOLATION (a content.path naming a key-bearing extension or holding key material — the content a tool reads from the sandbox is never a key), PKI_API_MISUSE (a repeated or builder-owned attribute type), ENGINE_ERROR.
{
"content": { "path": "fixtures/content.txt" },
"detached": true,
"certificate": { "path": "fixtures/leaf.crt.pem" },
"chain": [{ "path": "fixtures/inter.crt.pem" }],
"signingTime": "2027-01-01T00:00:00Z",
"signer": { "key": { "path": "fixtures/leaf.key.pem" } }
}
verify_cms
The RFC 5652 verdict. Inputs: signedData (PKI_INPUT), content (CONTENT_INPUT) or contentDigest (detached signatures), TRUST (trustAnchors[], candidates[], crls[], ocspResponses[], at, requireRevocation, purposes[]), atTimeStamp, requireSigningCertificate, requireAlgorithmProtection, allowTrailingData, allowSha1, PARSE, PROJECTION. Output: valid, reasons[], signers[] (per signer: valid, intact, reasons[], chain, time-stamps, revocation), signerCount, signatureVerifications[], evaluatedAt, atTimeStamp. Codes: PKI_REASON_* in reasons[] (NO_TRUST_ANCHOR, SIGNATURE_INVALID, REVOKED, TSP_*, …); PKI_API_MISUSE (content given for an attached message — it carries its own), VALIDATION_ERROR (both content and contentDigest), PKI_ASN1_TRAILING_DATA (the zero padding of a PDF /Contents without allowTrailingData).
{ "signedData": { "path": "fixtures/attached.p7s" }, "trustAnchors": [{ "path": "fixtures/root.crt.pem" }], "crls": [{ "path": "fixtures/inter.crl.pem" }], "ocspResponses": [{ "path": "fixtures/leaf.ocsp.der" }], "requireRevocation": true, "purposes": ["emailProtection"], "at": "2027-01-01T00:00:00Z" }
inspect_cms
The RFC 5652 report. Inputs: signedData, allowTrailingData, signerCertificate (one key check), signerIndex, content (for a signer without signed attributes), allowSha1, PARSE, PROJECTION. Output: version, contentType, contentLength (null = detached), digestAlgorithms[], certificates[], crlCount, ocspResponseCount, signerInfos[] (sid, algorithms, signing time as claimed, attributes, time-stamp count, signature as hex), signerSignature. Codes: NOT_FOUND (a signerIndex past the last signer, when signerCertificate is given — the index names the signer that check applies to), PKI_ASN1_TRAILING_DATA, PKI_*.
{ "signedData": { "path": "fixtures/detached.p7s" }, "signerCertificate": { "path": "fixtures/leaf.crt.pem" }, "content": { "path": "fixtures/content.txt" }, "fields": ["signerInfos.signature", "signerInfos.sid", "signerSignature"] }
amend_cms
The one legitimate edit. Inputs: signedData, signerIndex, operation (add-unsigned-attribute | add-timestamp), attribute (HEX), token (PKI_INPUT — a .tst or a whole .tsr), allowSha1, PARSE, OUTPUT (default der). Output: the artefact and summary. Codes: VALIDATION_ERROR with data.detail PKI_REASON_TSP_IMPRINT_MISMATCH (a token over anything but the signer's signature value — the remedy names the inspect_cms → create_timestamp_request → amend_cms sequence), NOT_FOUND, PKI_API_MISUSE (a signed-only attribute type), PKI_*.
{ "signedData": { "path": "fixtures/attached.p7s" }, "operation": "add-unsigned-attribute", "attribute": "300d06032a0304310606042a030405" }
create_timestamp_request
The RFC 3161 request you POST yourself. Inputs: data (CONTENT_INPUT) or digestHex (HEX), hashAlgorithm (SHA-256 default | SHA-384 | SHA-512), nonce ('random', decimal or 0x hex), policy (OID), certReq (default true), label (the PEM label for encoding: 'pem'), PARSE, OUTPUT (default der, .tsq). Output: the artefact and summary (imprintHex, hashAlgorithm, nonce, randomNonce, policy, certReq). Codes: VALIDATION_ERROR (a digest whose length does not match the algorithm), SECURITY_VIOLATION (a data.path naming a key-bearing extension or holding key material).
{ "data": { "path": "fixtures/content.txt" }, "nonce": "random" }
inspect_timestamp
The RFC 3161 report. Inputs: input (PKI_INPUT), as (response | token | tstinfo), PARSE, PROJECTION. Output: as, status (statusStrings, failInfo), token (CMS envelope: signer count, certificates), tstInfo (policy, messageImprint, serialNumber, genTime, accuracy, ordering, nonce, tsa, extensions). Codes: PKI_* (the bytes are none of the three structures), PKI_STRICT_DIAGNOSTIC (with strict: true — OpenSSL tokens carry PKI_DIAG_CMS_SET_NOT_SORTED).
{ "input": { "path": "fixtures/content.tst" }, "as": "token", "fields": ["token.tstInfo.genTime.iso", "token.tstInfo.messageImprint", "token.certificates.subject"] }
verify_timestamp
The RFC 3161 verdict. Inputs: token or response, request (the .tsq you sent — imprint, nonce and policy all checked), data (CONTENT_INPUT) or digestHex, TRUST (trustAnchors[], candidates[], crls[], ocspResponses[], at, requireRevocation), allowNonCriticalTimeStampingEku, allowSha1, PARSE, PROJECTION. Output: valid, reasons[], genTime, earliest / latest (accuracy applied), tsa, chain ({ valid, path[], reasons[] }), signatureVerifications[], evaluatedAt. Codes: PKI_REASON_TSP_IMPRINT_MISMATCH, PKI_REASON_TSP_NOT_GRANTED, PKI_REASON_TSP_REQUEST_MISMATCH, PKI_REASON_TSP_TOKEN_INVALID and the chain reasons in reasons[]; VALIDATION_ERROR (nothing stamped was given — a token verified without what it stamps proves nothing).
{ "response": { "path": "fixtures/content.tsr" }, "request": { "path": "fixtures/content.tsq" }, "data": { "path": "fixtures/content.txt" }, "trustAnchors": [{ "path": "fixtures/root.crt.pem" }], "candidates": [{ "path": "fixtures/inter.crt.pem" }], "at": "2027-01-01T00:00:00Z" }
inspect_private_key
A PKCS#8 key described, no key byte. Inputs: key (PKI_INPUT), tryImport, password (or PKINATIVE_MCP_PASSWORD), keyType (ec-p256 | ec-p384 | ec-p521 | ed25519 | ed448 | rsa | rsa-pss), hash, rsaScheme, saltLength, allowSha1, PARSE, PROJECTION. Output: format (pkcs8 | encrypted-pkcs8), kind, curve, algorithm + algorithmName, attributes, publicKey (embedded or not), encryption (scheme, PBKDF2 iterations, PRF, AES key size, salt length), encryptedBytes, imported (handle type, algorithm, usages). Codes: SECRET_REQUIRED, KEY_TYPE_REQUIRED, RSA_SCHEME_REQUIRED, PKI_CRYPTO_DECRYPTION_FAILED (a wrong password), PKI_KEY_ENCRYPTION_UNSUPPORTED (a legacy cipher, with the openssl pkcs8 remedy), PKI_* (a PUBLIC KEY or a non-PKCS#8 DER).
{ "key": { "path": "fixtures/leaf.key.enc.pem" }, "tryImport": true, "password": "test-only-password", "keyType": "ec-p256", "fields": ["imported", "encryption.scheme"] }
inspect_pkcs12
A PKCS#12 described, no key byte. Inputs: pkcs12 (PKI_INPUT; BER with encodingRules: 'ber' for a Windows-written PFX), password (optional), PARSE, PROJECTION. Output: version, mac (kind pbmac1 | pkcs12-kdf | null, parameters), contents[] (path, encryption scheme and parameters, bags of the plain ones), bagCount, macVerified (with a password; false is a report only when no SafeContents is encrypted — otherwise a wrong password is PKI_CRYPTO_DECRYPTION_FAILED before the MAC is reported), opened[] (allow-list views: certBags by subject / issuer / serial, key bags by algorithm, curve and encryption parameters, crlBags by size). A set PKINATIVE_MCP_PASSWORD counts as the password: describing a file without one needs the variable unset. Codes: PKI_CRYPTO_DECRYPTION_FAILED (a wrong password on an encrypted SafeContents), PKI_KEY_MAC_UNSUPPORTED, PKI_KEY_ENCRYPTION_UNSUPPORTED (each with the OpenSSL 3.4 conversion remedy).
{ "pkcs12": { "path": "fixtures/legacy.p12" }, "fields": ["mac.kind", "contents.path", "contents.encryption.scheme"] }
open_pkcs12
A PKCS#12 opened in one call. Inputs: pkcs12, password (or PKINATIVE_MCP_PASSWORD), allowUnverifiedIntegrity, rsaScheme (pkcs1 | pss), hash, exportCertificates, PARSE, PROJECTION, OUTPUT (pem only — a bundle has no DER form). Output: valid, integrity (verified | unverified | mismatch), reasons[], keys[] (algorithm, handle type, usages — never a byte), certificates[], keyCount / certificateCount / crlCount, exportSkipped, artefact (the PEM bundle). Codes: SECRET_REQUIRED (no password from either channel); in reasons[]: PKI_REASON_PKCS12_MAC_MISMATCH (a wrong password or an altered file — nothing is decrypted), PKI_REASON_PKCS12_ENCRYPTION_UNSUPPORTED, PKI_REASON_PKCS12_INTEGRITY_UNVERIFIED, PKI_REASON_PKCS12_RSA_SCHEME_UNSPECIFIED; MISSING_OUTPUT_PATH, OUTPUT_TOO_LARGE.
{ "pkcs12": { "path": "fixtures/leaf.p12" }, "password": "test-only-password", "exportCertificates": true }
describe_engine
The offline preflight. Inputs: PROJECTION only. Output: server (name, version, toolApiVersion, protocol, toolCount), engine (name, version, nodeFloor), capabilities (canVerify, canSign, canDecrypt, node, nodeFloorSatisfied, platform), randomness, defaultLimits, ceilings, caps, sandbox (enabled, cacheEnabled, pinnedInstant, the extension allow-lists), network: 'none', registries (the 57 / 43 / 97 codes, the wrapper codes, purposes, key-usage bits, the OID registry count), refusals (the doctrine), unexposed. Codes: VALIDATION_ERROR.
{ "fields": ["defaultLimits", "ceilings", "sandbox", "capabilities"] }
explain_code
Any code, explained. Inputs: code, or list: true with kind (error | reason | diagnostic | wrapper), PROJECTION. Output: code, kind, summary, standard, severity, errorClass, engineClass, cwe, since, remedy — or codes[] + count. Codes: NOT_FOUND (an unknown code).
{ "code": "PKI_REASON_PKCS12_RSA_SCHEME_UNSPECIFIED" }
draft_governance_issue
Draft a governance-compliant GitHub issue locally for a human to review and submit — for pkinative (engine behaviour: parsing, verification, limits, refusals), pkinative-cli or pkinative-mcp (wrapper behaviour: schemas, sandbox, transport). The server never contacts GitHub — it has no network code path at all; it returns the draft Markdown plus a machine-readable compliance report. Present both to the user, then STOP: they review and submit under their own identity. Inputs: title, summary, issueType (bug | feature | security | docs | performance), targetRepo, reproduction ({ command, result }), expectedBehavior, actualBehavior, affectedPackages[], duplicateSearchPerformed (must be true), outputMode (inline | file) + outputPath (a relative .md in the sandbox). Output: draftMarkdown, warnings[], compliance (the nine keys of .github/AGENT_RULES.md). Codes: GOVERNANCE_VIOLATION (a runtime dependency proposed, an embedded private key or password, no reproduction, duplicateSearchPerformed: false), MISSING_OUTPUT_PATH, INVALID_EXTENSION, OUTPUT_EXISTS, SECURITY_VIOLATION.
{
"title": "parseCertificateList refuses a delta CRL with an empty revokedCertificates list",
"summary": "A delta CRL (deltaCRLIndicator present) whose revokedCertificates SEQUENCE is absent is refused with PKI_X509_STRUCTURE_INVALID although RFC 5280 makes the field OPTIONAL.",
"issueType": "bug",
"targetRepo": "pkinative",
"reproduction": { "command": "inspect_crl { crl: { path: \"fixtures/empty-delta.crl\" } }", "result": "PKI_X509_STRUCTURE_INVALID at revokedCertificates" },
"expectedBehavior": "The delta CRL parses with entryCount 0.",
"affectedPackages": ["pkinative", "pkinative-mcp"],
"duplicateSearchPerformed": true
}
See the governance_contract and draft_issue_workflow prompts and docs/guides/AI_GOVERNANCE.md for the full human-in-the-loop contract.
🧩 Shared input fragments
The same ten fragments appear across the catalogue; a tool's description names the ones it carries. Every object schema is closed (additionalProperties: false, Zod .strict() at every depth): an unknown or misspelt key is VALIDATION_ERROR naming the path, never silently dropped.
Every result also carries diagnostics[] (de-duplicated by code and path, capped at 200 with diagnosticsTruncated) and diagnosticCounts; the password input, when a tool has one, is a plain string (≤ 4096 characters) that is never echoed, logged or cached.
🔐 Security model
pkinative-mcp runs inside the host process and exposes a stdio MCP server (or a loopback-only HTTP endpoint). It treats every certificate, request, list, response, message, token, key and PKCS#12 as untrusted input and performs no I/O outside the configured sandbox other than the opt-in response cache under PKINATIVE_MCP_CACHE_DIR.
- Secrets: two channels, never a third. A password reaches the server through the per-call
passwordinput or the operator-sidePKINATIVE_MCP_PASSWORD— one source per call (both at once isVALIDATION_ERROR);SECRET_REQUIREDwhen neither is present. A password is never echoed in a result or an error text, never logged (only the presence of the variable is reported at boot), never written to a cache file, never served as a resource. Private keys are imported into non-extractable Web Crypto handles; every key and PKCS#12 report is an allow-list view built field by field (src/key-views.ts), and the test suite proves with a needle table computed from the fixtures that no key byte, scalar, JWK member, encrypted-key body or password is printed by any result, on stderr, in a cache file or on the stdio transcript (tests/secret-leak.test.ts). - The sandbox (
PKINATIVE_MCP_OUTPUT_DIR) is the one filesystem boundary for reads (everypathinput,resources/read) and writes (outputPath, governance drafts). Paths must be relative; absolute / drive / UNC paths,.., NUL bytes and non-allow-listed extensions are rejected; the real path of the file read and of the parent written must stay inside (a symlink or junction planted in the sandbox isSECURITY_VIOLATIONon both sides); files are opened exclusively (OUTPUT_EXISTS— never overwritten); a failed write removes the partial file. Key-bearing extensions (.key .p8 .p12 .pfx) are read by the key tools only, never written, never listed nor served byresources/*— andresources/list/resources/readrefuse key material by content as well, whatever the file name: a PEM key after a prefix, a key hidden among many blocks, a raw DER key behind junk or base64 key material inside a text file isSECURITY_VIOLATION. The same content test guards the generic encoders (convert_pem encodetakes exactly one DER value and refuses key material) and the content inputs (content.pathofsign_cms,data.pathofcreate_timestamp_request): what a tool reads from the sandbox as content is never a key. Unset ⇒ inline PEM / base64 only. - Caps — every byte and count bound is a constant in
src/caps.tsand advertised bydescribe_engine.caps:
- The cache gate (
isCacheable,src/server.ts) — withPKINATIVE_MCP_CACHE_DIRset, a result is served from the cache only when nothing about it depends on the environment, a secret, the clock or randomness: never apathinput (a path is a reference, not content), neveroutputMode: 'file', never a call carrying apasswordor asigner, never a'random'serial or nonce, never a'now'or unpinned instant, neverdescribe_engineordraft_governance_issue, and never any password-accepting tool whilePKINATIVE_MCP_PASSWORDis set (the operator password may have been used without any secret in the input). The key namespace carries the tool API, package and engine versions, so an engine upgrade never serves bytes the previous engine wrote. - Doctrine refusals (
describe_engine.refusals) — the lines the server will not cross, each with its remedy: no network, no key generation (bring keys from your HSM, KMS oropenssl genpkey), no key export, no writer for PKCS#8 / PKCS#12 / CRL / OCSP response / time-stamp response (use the issuing software or OpenSSL and verify the result here), SHA-1 refused by default, no default RSA scheme, legacy PKCS#12 and PBES1 keys refused (convert once with OpenSSL 3.4+:openssl pkcs12 -legacy …then-export -pbmac1_pbkdf2), DSA / ML-DSA parsed but never verified or signed, diagnostics are not errors, limits raise only explicitly and under the ceilings, randomness lives in the wrapper and is reported (randomSerial,randomNonce). - Inputs are validated against strict JSON Schemas + Zod at the boundary of every tool — unknown or misspelt keys at any depth are
VALIDATION_ERROR; base64 is sanity-checked (data:URIs, PEM where DER is expected, double-encoded or empty payloads refused with a remedy) before any parser runs; specs are built prototype-pollution-safe (tests/prototype-pollution.test.ts). - HTTP transport (
PKINATIVE_MCP_PORT) binds loopback only with a Host / Origin guard; it has no authentication unlessPKINATIVE_MCP_HTTP_TOKENis set (then401without a valid bearer token).
Network & egress: none
None, ever. The server has no network code path: no CRL, OCSP, AIA or TSA fetch, no telemetry, no GitHub, no update check, no URL from any tool argument, and — unlike its PDF sibling — no operator-configurable endpoint of any kind. describe_engine reports network: 'none' and every tool advertises openWorldHint: false. Revocation evidence and time-stamp tokens are inputs you fetched out of band; create_ocsp_request and create_timestamp_request produce the bytes you transport (application/ocsp-request, application/timestamp-query). The engine itself never opens a socket, never touches the filesystem and never evals.
See SECURITY.md for the responsible disclosure process and the threat model, and docs/guides/SECURITY.md for the defences with their CWE ids.
🧪 Conformance (OpenSSL 3)
OpenSSL 3.x is the external judge of this server — the role veraPDF plays for pdfnative-mcp: what the tools write, a foreign implementation must read and verify. npm run corpus:pki drives the built server (scripts/generate-pki-corpus.ts, corpus table in scripts/lib/pki-corpus.ts) under a pinned instant to write the interoperability corpus into test-output/pki/ with a manifest.json (status, SHA-256, the second digest of every deterministic entry): 23 conformant artefacts — self-signed CAs and leaves for every signature family (Ed25519, RSA PKCS#1 v1.5, RSA-PSS, ECDSA P-256), a pathLen-0 intermediate and a leaf under it, three CSRs, attached and detached CMS messages (the Ed25519 signer included), two time-stamp requests, two OCSP requests, PEM re-encodings of the DER fixtures — and 5 refused canaries that must be refused with their declared code (a SHA-1 signature, a legacy PKCS#12 signer, an RSA key without a scheme, a notAfter before notBefore, a URL member in a spec). A canary that writes bytes is an XPASS and fails the run.
npm run validate:pki (scripts/validate-pki.ts) then checks every artefact at two levels:
- Level 0 — engine round trip, in-process through
dist/server.js: every certificate inspected and verified to the corpus CA at the pinned instant (verify_certificate_chain