Home โ€บ Guides โ€บ Errors, diagnostics and reasons

Errors, diagnostics and reasons

Every failure has a stable code, and every code has a cause and a remedy. Branch on the code; the message is for people. The machine-readable registries are docs/data/errors.json, docs/data/diagnostics.json and docs/data/reasons.json.

Three vocabularies, three questions #

They are separate because they answer different questions, and one registry answering two of them is one registry answering neither well.

Vocabulary The question About How it travels
PkiErrorCode "Is this the structure it claims to be, and is the API being used correctly?" โ€” no the input, or the call thrown
PkiDiagnosticCode "It is that structure, but it deviates from a profile." one object emitted (onDiagnostic, strict, console.warn)
PkiReasonCode "The input is well formed, and the judgement you asked for is no โ€” here is why." a relation returned, in a report

The rule this establishes, and that every composed operation follows:

Primitives return and throw. Compositions report. Exactly one layer converts, and it is the only place in src/ that turns a PkiError into a reason. Anywhere else a PkiError is caught, it is to throw it again under a more precise class, or to diagnose and drop a value no verdict depends on โ€” and every such catch passes what it caught through one guard that throws anything that is not a PkiError on, as the bug it is.

Two consequences worth knowing before you write a catch:

Unlike PkiErrorCode, frozen whole since 0.8, the reason vocabulary is grow-only: every name is frozen with the export surface, and the set of ways a chain can be rejected grows with the standards. Adding a reason is semver-minor; removing or renaming one is major (ADR 0018).

The error classes #

Class Raised for Extra fields
PkiError Usage faults: a wrong argument, an invalid option, strict: true escalating a diagnostic code
PkiEncodingError X.690, OBJECT IDENTIFIER and RFC 7468 syntax code, offset
PkiCertificateError The RFC 5280 certificate structure code, path, offset
PkiLimitError A configured limit exceeded, or an invalid limits override code, limit, configured, observed
PkiCryptoError A verification could not be performed โ€” never one that failed code, algorithm
PkiCmsError Well-formed DER that is not the RFC 5652 CMS or RFC 3161 timestamp structure it was read as code, path, offset
PkiKeyError A PKCS#8 or PKCS#12 structure pkinative cannot read, or a password scheme or MAC it refuses by policy โ€” never a wrong password code, path, offset

Every subclass extends PkiError, so error instanceof PkiError catches them all โ€” also for an error thrown by the other build (the CommonJS copy beside the ES module one) or by another realm, through a brand every copy shares (ADR 0021) โ€” and every message starts with pkinative: . The error vocabulary is frozen under semantic versioning: within the 1.x line no code is renamed, removed or moved to another class, and a new code is semver-minor (docs/data/errors.frozen.json is the snapshot, ADR 0012 the decision). Diagnostic codes are additions-only: none is ever renamed or removed, while a diagnostic's severity and wording may change in a minor release.

Each class types its own code, so narrowing the error narrows the codes: PkiBaseErrorCode on PkiError, and PkiEncodingErrorCode, PkiCertificateErrorCode, PkiLimitErrorCode, PkiCryptoErrorCode, PkiCmsErrorCode and PkiKeyErrorCode on the six subclasses. PkiErrorCode is the union of all seven โ€” the type to write when you store or pass a code without caring which class raised it.

The line PkiCryptoError draws #

verifyCertificateSignature returns a boolean, and throws only when the question could not be put. A signature that does not check out is false. So is one whose bytes are malformed, one whose issuer key is of the wrong family entirely, and one whose two signatureAlgorithm fields disagree โ€” every case where the answer is knowably "no". Failing closed is deliberate: a caller who forgets a catch gets "not verified", which is the safe reading, instead of an exception some layer above may swallow into a success path.

A PkiCryptoError means something else, and it is worth reacting to differently: pkinative did not decide. Retrying elsewhere may give a different answer, and treating it as a rejection would blame a certificate for the runtime's limits.

One code of that class is the exception, and it is on the decrypting side, where there is no boolean to return: PKI_CRYPTO_DECRYPTION_FAILED says the host tried to decrypt a key or a SafeContents and could not. It stays a PkiCryptoError rather than a PkiKeyError because only the host can find it out: the file was readable, and AES-CBC, which carries no authentication tag, cannot say whether the password was wrong, the bytes altered, or the key not of the algorithm it was unwrapped as.

Error codes #

Usage #

ASN.1 and OIDs #

PEM #

Certificates #

Limits #

Verification that could not run #

CMS and timestamps #

verifySignedData and verifyTimeStampToken throw none of these for the message or token they judge: there, they are PKI_REASON_INPUT_MALFORMED with the code in errorCode. The one exception is verifyTimeStampToken's request, which is your own bytes, so a malformed one is thrown.

Private keys and PKCS#12 #

openPkcs12 throws none of these for the file it opens: they are its PKI_REASON_PKCS12_* reasons, or PKI_REASON_INPUT_MALFORMED for a file it cannot read at all.

Diagnostics #

A diagnostic is a non-fatal conformance concern: { code, severity, message, standard, path, offset }. Receive them with onDiagnostic, read them on certificate.diagnostics, or refuse them all with strict: true.

docs/data/diagnostics.json gives each one's cause, remedy and the clause it cites.

Reasons #

A reason is why a judgement came out negative: { code, message, standard, path }, plus errorCode or limit where they apply. It is returned inside a report, never thrown and never printed โ€” and several are returned together whenever several apply, because a certificate can be expired and outside a name constraint, and a report that stopped at the first would hide the work still to do.

PKI_REASON_SIGNATURE_NOT_CHECKED is not a rejection. A runtime with no Web Crypto, or one that refuses Ed448 โ€” Bun and Chromium today, for a certificate signature as for a CMS signer โ€” says nothing about whether a signature is good. Treating it as PKI_REASON_SIGNATURE_INVALID turns "ask me elsewhere" into "this certificate is bad", which is the most expensive confusion available in this vocabulary โ€” the same distinction PkiCryptoError draws on the throwing side.

PKI_REASON_REVOCATION_UNKNOWN is not "not revoked" either, for the same reason: a missing or unsigned list is an absence of evidence, and reporting it as a clean answer would make the soft-fail decision on your behalf, invisibly.

docs/data/reasons.json gives each one's cause, remedy and the clause it cites. Which of them a real chain actually earns is measured rather than asserted: every case of the x509-limbo corpus is scored on each release, and a reviewed subset is pinned on its reason code rather than on the pass/fail boolean โ€” because rejected for the wrong reason is a defect no count can see.

Where the codes surface #

The three vocabularies are the engine's, and they travel unchanged through every surface built on it: pkinative-cli and pkinative-mcp add a class of their own for the failures the engine does not own โ€” usage, files, the sandbox โ€” but never rename, merge or drop a PKI_* code.

Question Library CLI MCP server
The coarse class The PkiError subclass thrown (PkiEncodingError, PkiCertificateError, โ€ฆ) error.code in the --json envelope: one of 13 E_* classes, E_USAGE exit 2, the others exit 1 _meta.error.data.errorClass on a PKI_* code (usage, input, parse, security, limit, unsupported, crypto, password, check, internal); the wrapper's own codes carry none
The exact cause err.code, a PKI_* code error.pkiCode, the same code verbatim; E_IO, E_NOT_FOUND and E_VERIFY_FAILED are the CLI's own and carry none _meta.error.code โ€” the same PKI_* code verbatim, or one of the server's 21 wrapper codes (VALIDATION_ERROR, SECURITY_VIOLATION, SECRET_REQUIRED, โ€ฆ)
Structured detail The subclass's fields (offset, path, limit, configured, observed, algorithm) error.detail, with detail.flag naming the --max-* flag of a limit _meta.error.data (path, offset, algorithm, limit, configured, observed, env)
The remedy The registry's remedy, in the message error.remedy: the CLI flag that lifts the refusal (--ber, --allow-sha1, --max-depth <value>) data.remedy: the MCP input that lifts it (encodingRules:'ber', allowSha1:true, limits.<limit>)
A negative verdict A report with valid: false and reasons[] โ€” never thrown The report on stdout and exit 1, the envelope's error.code E_VERIFY_FAILED or E_CHECK_FAILED, error.reasons the PKI_REASON_* list valid: false and reasons[] in structuredContent โ€” never isError
Diagnostics onDiagnostic, or console.warn diagnostics[] in the envelope, one warning PKI_DIAG_โ€ฆ line each on stderr; --strict escalates the first warning to E_CHECK_FAILED with PKI_STRICT_DIAGNOSTIC diagnostics[] and diagnosticCounts in every engine tool's result; strict: true escalates to PKI_STRICT_DIAGNOSTIC
Explain a code docs/data/errors.json, reasons.json, diagnostics.json and this guide pkinative explain <code>, and pkinative explain --list The explain_code tool, with list: true

One failure, three surfaces โ€” a private key passed where a certificate is expected. The library's decodePem with { label: 'CERTIFICATE' } throws a PkiEncodingError whose code is PKI_PEM_UNEXPECTED_LABEL; the CLI's envelope and the server's tool result (its content text omitted) carry the same code โ€” output of pkinative-cli 1.0.0 and pkinative-mcp 1.0.0:

{"ok":false,"command":"cert inspect","error":{"code":"E_INPUT","message":"leaf.key.pem holds no CERTIFICATE or X509 CERTIFICATE or TRUSTED CERTIFICATE PEM block (labels present: PRIVATE KEY).","pkiCode":"PKI_PEM_UNEXPECTED_LABEL","remedy":"pkinative pem decode <file> (lists the labels present)"},"diagnostics":[]}
{"isError":true,"_meta":{"error":{"code":"PKI_PEM_UNEXPECTED_LABEL","message":"certificate (fixtures/leaf.key.pem) holds no CERTIFICATE or X509 CERTIFICATE or TRUSTED CERTIFICATE PEM block (labels present: PRIVATE KEY).","data":{"errorClass":"input","path":"certificate","remedy":"convert_pem mode:'decode' (lists the labels present)"}}}}

Branch on the code, on every surface: the class says which kind of failure it is, the code says exactly which, and the message is for people. The CLI guide maps the 57 error codes to the 13 classes one by one, and the MCP guide lists the 21 wrapper codes.