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.jsonanddocs/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 aPkiErrorinto a reason. Anywhere else aPkiErroris 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 aPkiErroron, as the bug it is.
Two consequences worth knowing before you write a catch:
- A reason message never starts with
pkinative:. That prefix marks what is thrown, and keeping it exclusive is what lets you tell an exception from a verdict in a log.reason-parityrefuses it. - A reason never duplicates an error code.
PKI_REASON_INPUT_MALFORMEDcarries in itserrorCodefield thePkiErrorCodethat would have been thrown, so a report can promise never to throw for a malformed input without copying 57 error codes into a second vocabulary โ which would then have to be frozen too. The reason registry wraps the error registry; it never mirrors it.
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 #
PKI_INVALID_INPUTโ the input has the wrong type: not aUint8Array, not a string, not a parsed certificate.PKI_INVALID_OPTIONโ an option fails validation, e.g.encodingRules: 'cer'.PKI_API_MISUSEโ a caller contract is violated, e.g. DER-encoding an indefinite-length node, or reading an implicitly tagged value without saying its type.PKI_STRICT_DIAGNOSTICโstrict: truerefused an input because of its first diagnostic; the message carries that diagnostic's code.PKI_INTERNALโ an internal invariant broke: a pkinative bug, please report it.
ASN.1 and OIDs #
PKI_ASN1_TRUNCATEDโ a header or value runs past the end of the input (CWE-125).PKI_ASN1_LENGTH_OVERFLOWโ a declared length overruns its enclosing value (CWE-130).PKI_ASN1_TAG_INVALID,PKI_ASN1_LENGTH_INVALIDโ a malformed tag or length octet.PKI_ASN1_LENGTH_NON_MINIMAL,PKI_ASN1_INDEFINITE_LENGTH_FORBIDDEN,PKI_ASN1_CONSTRUCTED_STRING_FORBIDDENโ a BER-only construct under DER (CWE-436); decode withencodingRules: 'ber'if the input is BER.PKI_ASN1_CONSTRUCTED_FORM_INVALID,PKI_ASN1_EOC_UNEXPECTED,PKI_ASN1_UNEXPECTED_TAGโ the wrong form or type where the structure requires another.PKI_ASN1_TRAILING_DATAโ bytes after the outermost value.PKI_ASN1_BOOLEAN_INVALID,PKI_ASN1_INTEGER_INVALID,PKI_ASN1_INTEGER_UNREPRESENTABLE,PKI_ASN1_BIT_STRING_INVALID,PKI_ASN1_NULL_INVALID,PKI_ASN1_STRING_INVALID,PKI_ASN1_TIME_INVALIDโ a value that does not match its type.PKI_ASN1_VALUE_OUT_OF_RANGEโ an encoder argument the type cannot represent.PKI_OID_INVALIDโ an OBJECT IDENTIFIER that is empty, non-minimal or malformed.
PEM #
PKI_PEM_NO_BLOCK,PKI_PEM_UNTERMINATEDโ no block, or a BEGIN line without its END.PKI_PEM_LABEL_INVALID,PKI_PEM_LABEL_MISMATCHโ a label outside the RFC 7468 grammar, or an END label that differs.PKI_PEM_UNEXPECTED_LABELโ a block whose label is not the one requested withlabel, such as a private key in a certificate bundle.PKI_PEM_BASE64_INVALID,PKI_PEM_HEADERS_FORBIDDENโ the body is not canonical base64, or strict mode met RFC 1421 headers.
Certificates #
PKI_X509_STRUCTURE_INVALIDโ the DER is not the structure the reader expects: a Certificate forparseCertificate, a CertificationRequest forparseCertificationRequest. Check that the input is not the other one, a CRL or a key; each has its own reader.PKI_X509_VERSION_INVALID,PKI_X509_NAME_INVALID,PKI_X509_VALIDITY_INVALID,PKI_X509_SPKI_INVALID,PKI_X509_UNIQUE_ID_INVALIDโ a malformed field.PKI_X509_EXTENSIONS_EMPTY,PKI_X509_EXTENSION_DUPLICATEโ an empty extensions field, or one extension twice (CWE-694).PKI_X509_EXTENSION_MALFORMEDโ a recognised extension whose value does not match its definition; parse withdecodeExtensions: falseto keep every extension raw.PKI_X509_GENERAL_NAME_INVALIDโ a malformed GeneralName: an unknown tag, a bad iPAddress length, a non-ASCII IA5String name.
Limits #
PKI_LIMIT_EXCEEDEDโ a bound ofPkiLimitswas exceeded; raise it only for trusted input (CWE-400).PKI_LIMIT_INVALIDโoptions.limitsitself is invalid.
Verification that could not run #
PKI_CRYPTO_UNAVAILABLEโ this runtime exposes nocrypto.subtlewithimportKeyandverify, or, to open a password-protected key, withderiveKey,unwrapKeyanddecrypttoo. CallcanVerify(),canSign()orcanDecrypt()first; a page on plain HTTP has none (CWE-693).PKI_CRYPTO_ALGORITHM_UNSUPPORTEDโ the signature algorithm is outside the set pkinative verifies, or its RSASSA-PSS parameters name something Web Crypto cannot express. MD2 and MD5 are here on purpose: Web Crypto implements neither, so the honest answer names the algorithm instead of a verification that did not happen (CWE-757).PKI_CRYPTO_KEY_UNSUPPORTEDโ the issuer's key is of the right family but on a curve Web Crypto does not verify, or the host refused to import it. Web Crypto does ECDSA only on P-256, P-384 and P-521; Ed448 and, on several runtimes still, Ed25519 are absent โ for a certificate signature as for a CMS signer, which since 1.0.0 is verified wherever the host has Ed448 (ADR 0022). Anid-RSASSA-PSSpublic key is no longer this error: it is handed to the host underrsaEncryptiononce the signature has been held to the key's parameters (RFC 4055), so what it signed verifies on every runtime; only anid-RSASSA-PSSprivate key under PBES2 still depends on the host, since a shrouded key is unwrapped there and never re-wrapped here (CWE-757).PKI_CRYPTO_ALGORITHM_REFUSEDโ the signature is over SHA-1. Web Crypto would compute it, and pkinative refuses to treat it as evidence: SHA-1 collisions have been practical since 2017, so a SHA-1 signature does not bind the bytes it covers, andtruewould be a false assurance (RFC 9155 ยง3). Get the certificate reissued under SHA-256 or better. To examine a historical artefact rather than rely on it, pass{ allowSha1: true }and treat the answer as an archival fact, not an authentication decision. A certificate's own SHA-1 fingerprint is unaffected: that is public data, not a signature (CWE-327).
CMS and timestamps #
PKI_CMS_STRUCTURE_INVALIDโ the value does not match the RFC 5652 or RFC 3161 module;pathnames the field. The usual cause is aSignedDatapassed without itsContentInfowrapper, or a PDF/Contentspassed with its zero padding and withoutallowTrailingData(CWE-1286).PKI_CMS_CONTENT_TYPE_UNEXPECTEDโ the wrong kind of CMS content: enveloped or encrypted data handed toparseSignedData, which pkinative never decrypts, or aSignedDataover anything but aTSTInfohanded toparseTimeStampToken(CWE-843).PKI_CMS_VERSION_UNSUPPORTEDโ aSignedData,TSTInfoorTimeStampReqversion the syntax does not define. A defined version that disagrees with the content is onlyPKI_DIAG_CMS_VERSION_MISMATCH(CWE-1286).PKI_CMS_CONTENT_NOT_OCTET_STRINGโ PKCS #7 content that is not an OCTET STRING, such as Authenticode, whose digest rule differs from RFC 5652's (CWE-843).
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 #
PKI_KEY_STRUCTURE_INVALIDโ well-formed DER that does not match the RFC 5958 PKCS#8 or RFC 7292 PKCS#12 module: a field missing or out of order, PBES2 parameters that contradict each other, an initialisation vector that is not 16 octets. The usual cause is a PEM block decoded from the wrong label โPRIVATE KEYfor PKCS#8,ENCRYPTED PRIVATE KEYfor its encrypted form โ or a.p12read as text (CWE-1286).PKI_KEY_VERSION_UNSUPPORTEDโ aPrivateKeyInfoversion other than 0 or 1, or a PFX version other than 3; re-export the file from the tool that owns the key (CWE-1286).PKI_KEY_ENCRYPTION_UNSUPPORTEDโ a key or a SafeContents protected by anything but PBES2 with PBKDF2 and AES-CBC: an RFC 7292 Appendix C scheme such aspbeWithSHAAnd3-KeyTripleDES-CBC, PBES1, PBES2 with another cipher, or public-key privacy mode. A policy, not a gap, and the message names the scheme and the conversion:openssl pkcs12 -in legacy.p12 -legacy -aes256 -out bundle.pem, thenopenssl pkcs12 -export -in bundle.pem -pbmac1_pbkdf2 -out modern.p12(OpenSSL 3.4 or later), oropenssl pkcs8 -topk8 -v2 aes-256-cbc -v2prf hmacWithSHA256for a key alone (CWE-327).PKI_KEY_MAC_UNSUPPORTEDโverifyPkcs12Macasked to check a MAC keyed with the RFC 7292 Appendix B KDF, which is every MAC OpenSSL wrote before 3.4, orparsePkcs12given public-key integrity mode. Re-export withopenssl pkcs12 -export -pbmac1_pbkdf2;openPkcs12reports such a file asPKI_REASON_PKCS12_INTEGRITY_UNVERIFIEDand takesallowUnverifiedIntegrityfor the decision (CWE-327).PKI_CRYPTO_DECRYPTION_FAILEDโ aPkiCryptoError: Web Crypto could not decrypt a PBES2 payload or unwrap a key with the key derived from the password. Check the password first โ a string is encoded as UTF-8; pass the exact octets as aUint8Arrayfor a file written with another encoding โ then that the algorithm named fits the key. A file that fails with both right has been altered (CWE-354).
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.
Serial number and algorithms โ
PKI_DIAG_SERIAL_TOO_LONG,PKI_DIAG_SERIAL_NOT_POSITIVE,PKI_DIAG_SIGNATURE_ALGORITHM_MISMATCH,PKI_DIAG_RSA_PARAMETERS_NOT_NULL.Version and encoding โ
PKI_DIAG_EXTENSIONS_REQUIRE_V3,PKI_DIAG_UNIQUE_ID_REQUIRES_V2,PKI_DIAG_DEFAULT_ENCODED,PKI_DIAG_STRING_SIGNATURE(a BMPString or UniversalString starting with the U+FEFF signature X.690 ยง8.23 forbids),PKI_DIAG_STRING_ESCAPE_SEQUENCE(an ISO/IEC 2022 escape or shift control inside an ISO/IEC 10646 string type).Time โ
PKI_DIAG_GENERALIZED_TIME_BEFORE_2050,PKI_DIAG_GENERALIZED_TIME_FRACTION,PKI_DIAG_VALIDITY_INVERTED.Names โ
PKI_DIAG_EMPTY_ISSUER,PKI_DIAG_EMPTY_SUBJECT_SAN_NOT_CRITICAL,PKI_DIAG_SAN_EMPTY,PKI_DIAG_RDN_SET_NOT_SORTED,PKI_DIAG_PRINTABLE_STRING_CHARSET,PKI_DIAG_TELETEX_AS_LATIN1,PKI_DIAG_DNS_NAME_NOT_PREFERRED_SYNTAX,PKI_DIAG_GENERAL_NAME_CONTROL_CHARACTER(a NUL, another C0 control or DEL in a dNSName, rfc822Name or URI: kept and compared literally, but a consumer that stops at NUL reads another name โ the CVE-2009-2408 class),PKI_DIAG_COMMON_NAME_NOT_IN_SAN,PKI_DIAG_NAME_ATTRIBUTE_STRING_TYPE(an attribute encoded outside the syntax RFC 5280 Appendix A.1 gives it, such as a UTF8String countryName),PKI_DIAG_COUNTRY_NAME_SIZE(a countryName that is not two characters),PKI_DIAG_NAME_ATTRIBUTE_TOO_LONG(a value past the RFC 5280 Appendix A.1 upper bound of its attribute โ 64 characters for a commonName, 255 for an emailAddress โ read as it is; informational),PKI_DIAG_NAME_COUNTRY_UNKNOWN(a two-letter countryName that is neither an assigned ISO 3166-1 alpha-2 code nor a user-assigned one; informational).Extensions โ
PKI_DIAG_UNKNOWN_CRITICAL_EXTENSION,PKI_DIAG_PATHLEN_WITHOUT_CA,PKI_DIAG_KEY_USAGE_EMPTY,PKI_DIAG_NAMED_BITS_TRAILING_ZERO,PKI_DIAG_NAME_CONSTRAINTS_NOT_CRITICAL,PKI_DIAG_NAME_CONSTRAINTS_IN_END_ENTITY,PKI_DIAG_BASIC_CONSTRAINTS_NOT_CRITICAL,PKI_DIAG_POLICY_CONSTRAINTS_NOT_CRITICAL,PKI_DIAG_KEY_CERT_SIGN_WITHOUT_CA,PKI_DIAG_AKI_ISSUER_SERIAL_UNPAIRED,PKI_DIAG_POLICY_DUPLICATE,PKI_DIAG_POLICY_CONSTRAINTS_EMPTY,PKI_DIAG_SUBJECT_DIRECTORY_ATTRIBUTES_CRITICAL(RFC 5280 ยง4.2.1.8 says non-critical).The six added in 0.5.0 share one shape: each is a sentence RFC 5280 or the CA/Browser Forum addresses to the issuing CA, and none of them refuses a chain. pkinative reads and enforces the extension whatever the profile says about its criticality, so refusing would make it stricter than the standards ask of a verifier while accepting exactly the same set of chains;
strict: trueis where that choice belongs. Each answers a case of the x509-limbo corpus that expects a refusal, and the reason it stays a diagnostic is written beside that case inscripts/data/limbo-score.json.PKI_DIAG_COMMON_NAME_NOT_IN_SANis the first to cite CA/Browser Forum Baseline Requirements rather than an RFC, because no RFC says it โ and it fires only for acommonNamethat could be matched as a host, since reportingCN=Example Issuing CAwould be reporting the ordinary shape of every organisational subject.Key identifiers โ
PKI_DIAG_AKI_MISSING(a v3 certificate naming another subject as its issuer without anauthorityKeyIdentifier: path building falls back to names, which costs exploration, not correctness) andPKI_DIAG_SKI_MISSING(a CA certificate without asubjectKeyIdentifier, so the certificates it issues cannot name its key).computeKeyIdentifierwithencodeAuthorityKeyIdentifierandencodeSubjectKeyIdentifierwrites both.The rest of the RFC 5280 ยง4.1โยง4.2 profile โ one code per requirement sentence a single certificate can be checked against, each held to an independent reading by its L5 clause; a MUST is a
warning, a SHOULD aninfo, and none changes what is decoded or refuses a chain.- Criticality:
PKI_DIAG_AKI_CRITICAL,PKI_DIAG_SKI_CRITICAL,PKI_DIAG_INHIBIT_ANY_POLICY_NOT_CRITICAL,PKI_DIAG_FRESHEST_CRL_CRITICAL,PKI_DIAG_AIA_CRITICAL,PKI_DIAG_SIA_CRITICAL(MUST);PKI_DIAG_KEY_USAGE_NOT_CRITICAL,PKI_DIAG_POLICY_MAPPINGS_NOT_CRITICAL,PKI_DIAG_SAN_CRITICAL(critical beside a non-empty subject),PKI_DIAG_ISSUER_ALT_NAME_CRITICAL,PKI_DIAG_EKU_ANY_CRITICAL(critical while listing anyExtendedKeyUsage),PKI_DIAG_CRL_DISTRIBUTION_POINTS_CRITICAL(SHOULD). - Presence:
PKI_DIAG_UNIQUE_ID_PRESENT(a unique identifier at any version, besidePKI_DIAG_UNIQUE_ID_REQUIRES_V2for the v1 case),PKI_DIAG_SKI_MISSING_END_ENTITY(an end-entity certificate without asubjectKeyIdentifier: RFC 5280 recommends one, the CA/Browser Forum now advises against it in subscriber certificates, so it is aninfoto weigh against the profile you issue under). - Policies:
PKI_DIAG_ANY_POLICY_QUALIFIER,PKI_DIAG_NOTICE_REF_USED,PKI_DIAG_EXPLICIT_TEXT_STRING_TYPE(a VisibleString or BMPString explicitText โ the one fact two sentences of ยง4.2.1.4 state, so one code),PKI_DIAG_EXPLICIT_TEXT_CONTROL_CHARACTER,PKI_DIAG_EXPLICIT_TEXT_NOT_NFC,PKI_DIAG_POLICY_MAPPING_NOT_ASSERTED(an issuerDomainPolicy the certificate's own certificatePolicies does not list). - Alternative names, in
subjectAltNameandissuerAltName(ยง4.2.1.7 encodes the latter as ยง4.2.1.6 does):PKI_DIAG_ALT_NAME_URI_INVALID(not an RFC 3986 URI, a relative reference included),PKI_DIAG_ALT_NAME_URI_SCHEME_MISSING,PKI_DIAG_ALT_NAME_URI_HOST_INVALID(an authority whose host is neither a fully qualified domain name nor an IP address),PKI_DIAG_ALT_NAME_GENERAL_NAME_EMPTY. The URI is still kept and compared literally; the grammar is the onesrc/core/uri.tsgives name constraints too. - Name constraints:
PKI_DIAG_NAME_CONSTRAINTS_MIN_MAX(a non-zero minimum or any maximum, which path validation never lets cover a name),PKI_DIAG_NAME_CONSTRAINTS_URI_NOT_FQDN. - Where to fetch:
PKI_DIAG_DISTRIBUTION_POINT_WITHOUT_NAME,PKI_DIAG_DISTRIBUTION_POINT_LDAP_URI_INCOMPLETE(no<dn>or not one<attrdesc>, RFC 4516 ยง2),PKI_DIAG_DISTRIBUTION_POINT_NO_HTTP_OR_LDAP_URI,PKI_DIAG_DISTRIBUTION_POINT_RELATIVE_NAME,PKI_DIAG_DISTRIBUTION_POINT_RELATIVE_NAME_AMBIGUOUS, forcRLDistributionPointsandfreshestCRLalike;PKI_DIAG_INFO_ACCESS_LDAP_URI_INCOMPLETE(anid-ad-caIssuersorid-ad-caRepositoryLDAP URI without<dn>or<attributes>: one sentence in ยง4.2.2.1 and ยง4.2.2.2, one code, the path names the extension),PKI_DIAG_CA_ISSUERS_NO_HTTP_OR_LDAP_URI,PKI_DIAG_CA_REPOSITORY_NO_HTTP_OR_LDAP_URI.
strict: truerefuses the firstwarningโ a MUST the input broke โ and still reports everyinfo: a SHOULD the input did not follow, such as the subjectKeyIdentifier nine thousand x509-limbo end-entity certificates omit, is advice to the issuer, not a reason to refuse the certificate.- Criticality:
CMS โ
PKI_DIAG_CMS_VERSION_MISMATCH,PKI_DIAG_CMS_SET_NOT_SORTED,PKI_DIAG_CMS_SIGNED_ATTRIBUTES_NOT_DER,PKI_DIAG_CMS_DIGEST_ALGORITHM_NOT_LISTED, onSignedData.diagnostics. None of them touches a signed octet โ the signed attributes are verified over the bytes as they were signed, never over a re-sorted set โ so none changes a verdict; they tell you which other verifiers may refuse the message. The same holds for what the message says against RFC 5652 ยง5.2, ยง11.3 and ยง11.4:PKI_DIAG_CMS_CERTS_ONLY_CONTENT(a signer-less SignedData whose eContentType is not id-data, or which carries an eContent),PKI_DIAG_CMS_SIGNING_TIME_NOT_UTCandPKI_DIAG_CMS_SIGNING_TIME_FRACTION(a signingTime in GeneralizedTime for 1950โ2049, or with fractional seconds โ the instant is read as written),PKI_DIAG_CMS_COUNTERSIGNATURE_CONTENT_TYPE,PKI_DIAG_CMS_COUNTERSIGNATURE_NO_MESSAGE_DIGESTandPKI_DIAG_CMS_COUNTERSIGNATURE_EMPTY(a countersignature's own signed attributes, looked into as far as their types; the countersignature stays carried, not verified).Timestamps โ
PKI_DIAG_TSP_CERTREQ_UNMET,PKI_DIAG_TSP_CERTS_UNREQUESTED: the token's certificates field against the request'scertReq(RFC 3161 ยง2.4.1), decided only whereverifyTimeStampTokenholds both, and reported through itsonDiagnostic. The verdict is the same either way: the TSA is found among the token's certificates and the ones you pass.OCSP โ
PKI_DIAG_OCSP_CERTS_EMPTY(a certs field present and empty) andPKI_DIAG_OCSP_VERSION_NOT_V1(an explicit ResponseData version that is not the INTEGER 0; a v1 written out isPKI_DIAG_DEFAULT_ENCODED), onOcspResponse.diagnostics;PKI_DIAG_OCSP_RESPONDER_ID_MISMATCHandPKI_DIAG_OCSP_NOCHECK_CRITICAL, whencheckOcspStatusis given the certificate that signed (itssigner) and the responderID names neither its subject nor its key hash, or itsid-pkix-ocsp-nocheckis critical;PKI_DIAG_OCSP_SINGLE_RESPONSE_UNREQUESTED, the answers about certificates nobody asked about beside the one that was.verifyCertificateChainpasses the signer it established and forwards every one of them, with what the lists and responses say about themselves, to its ownonDiagnosticโ and says nothing without one.Revocation lists โ
PKI_DIAG_CRL_EXTENSION_MALFORMED: acRLNumberordeltaCRLIndicatorthat is not a DER INTEGER, an entryreasonCodeRFC 5280 does not assign, or aninvalidityDatethat is not a GeneralizedTime. The value is ignored, because none of them decides whether a serial is revoked: a revocation with an unreadable reason is still a revocation, and a delta whose base number cannot be read is never paired with a base.Keys โ
PKI_DIAG_KEY_KDF_ITERATIONS_LOW: a PBKDF2 iteration count, for a key, a SafeContents or a PBMAC1 MAC, below the 1 000 RFC 8018 recommends. The file still opens; the count is what a stolen copy costs to brute-force. A count abovemaxKdfIterationsis not a diagnostic butPKI_LIMIT_EXCEEDED, because the host would run it where nothing can interrupt it.Public keys โ
PKI_DIAG_SPKI_RSA_EXPONENT_WEAK: an RSA public exponent below 3 or even, outside RFC 8017 ยง3.1. The certificate still parses; no signature under that key is ever checked, since under e = 1 a message is its own signature (PKI_REASON_SIGNATURE_NOT_CHECKEDwithPKI_CRYPTO_KEY_UNSUPPORTEDat verification).PKI_DIAG_SPKI_EC_PARAMETERS_INVALID: an EC key whose parameters are NULL (implicitCurve) or explicit (specifiedCurve), which RFC 5480 ยง2.1.1 forbids, or a namedCurve other than P-256, P-384 and P-521. The key decodes withcurveundefined; no signature under it is checked, for the same reason code.Accepted tolerances โ
PKI_DIAG_BER_CONSTRUCT_ACCEPTED,PKI_DIAG_PEM_LAX_ACCEPTED: the input used a construct you allowed withencodingRules: 'ber'ormode: 'lax'.
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.
- The input, wrapped โ
PKI_REASON_INPUT_MALFORMED, carrying inerrorCodethePkiErrorCodethat would have been thrown. - The certificate alone โ
PKI_REASON_NOT_YET_VALID,PKI_REASON_EXPIRED,PKI_REASON_UNKNOWN_CRITICAL_EXTENSION. - The link to the issuer โ
PKI_REASON_ISSUER_NOT_FOUND,PKI_REASON_SIGNATURE_INVALID,PKI_REASON_SIGNATURE_NOT_CHECKED. - The chain โ
PKI_REASON_NO_TRUST_ANCHOR,PKI_REASON_NOT_A_CA,PKI_REASON_PATH_TOO_LONG,PKI_REASON_PATH_LOOPS. - What a CA above it forbade โ
PKI_REASON_NAME_NOT_PERMITTED,PKI_REASON_NAME_EXCLUDED,PKI_REASON_NO_VALID_POLICY,PKI_REASON_POLICY_MAPPING_INVALID. - Revocation โ
PKI_REASON_REVOKED(carrying the date, because a signature made before it may still be good),PKI_REASON_REVOCATION_STALE,PKI_REASON_REVOCATION_WRONG_ISSUER,PKI_REASON_REVOCATION_UNKNOWN,PKI_REASON_REVOCATION_MISMATCH,PKI_REASON_REVOCATION_OUT_OF_SCOPE(the right CA, but a list whoseissuingDistributionPointexcludes this certificate โ its silence is not evidence) andPKI_REASON_REVOCATION_PARTIAL(a list withonlySomeReasons, which rules out those reasons and no others;verifyCertificateChainadds up the lists you supply and drops it once the union is complete). - The questions ยง6 does not ask โ
PKI_REASON_NAME_MISMATCH(fromcheckServerName: the chain is sound and the certificate is for somebody else) andPKI_REASON_PURPOSE_NOT_PERMITTED(fromcheckExtendedKeyUsage: the chain is sound and the certificate is for something else). - A signed message (from
verifySignedData) โPKI_REASON_CMS_NO_SIGNERS(a certificate bundle is not a signature),PKI_REASON_CMS_SIGNER_NOT_FOUND,PKI_REASON_CMS_CONTENT_MISSING(absent content is never empty content),PKI_REASON_CMS_DIGEST_MISMATCH,PKI_REASON_CMS_ATTRIBUTE_INVALID,PKI_REASON_CMS_ALGORITHM_MISMATCH(the shape of an algorithm substitution),PKI_REASON_CMS_SIGNING_CERTIFICATE_MISMATCH(the signature presented under another certificate for the same key). - A timestamp (from
verifyTimeStampToken, and underunsignedAttrs.timeStampToken[i]fromverifySignedData) โPKI_REASON_TSP_IMPRINT_MISMATCH(the token stamps something else),PKI_REASON_TSP_REQUEST_MISMATCH(a nonce or policy the request did not ask for โ what a replay looks like; send a new request rather than retrying),PKI_REASON_TSP_TOKEN_INVALID, andPKI_REASON_TSP_NOT_GRANTED(a response whose status is neither granted nor grantedWithMods, so it carries no token; the message carries the status, the TSA's text and its failure codes). - A PKCS#12 (from
openPkcs12) โPKI_REASON_PKCS12_INTEGRITY_UNVERIFIED(the MAC cannot be checked here, or there is none: the contents are reported andvalidis false unless you passallowUnverifiedIntegrity),PKI_REASON_PKCS12_MAC_MISMATCH(a PBMAC1 that does not match โ most likely the password โ after which nothing is decrypted),PKI_REASON_PKCS12_ENCRYPTION_UNSUPPORTED(a scheme pkinative refuses, or one this runtime's Web Crypto lacks, such as AES-192 in several browsers),PKI_REASON_PKCS12_DECRYPTION_FAILED,PKI_REASON_PKCS12_KEY_UNMATCHED(no certificate shares the key'slocalKeyId, so its algorithm is unknown โdecryptPrivateKeywith a named algorithm opens it),PKI_REASON_PKCS12_KEY_UNSUPPORTED(a key Web Crypto does not sign with, such as DSA or X25519) andPKI_REASON_PKCS12_RSA_SCHEME_UNSPECIFIED(an RSA key whose scheme you did not name inrsaAlgorithm: pkinative does not guess one, so the key stays shut until you do). - Your own limits โ
PKI_REASON_LIMIT_EXCEEDED, withlimitnaming the bound that stopped the search.
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.