Home β€Ί Guides β€Ί Conformance

Conformance

pkinative is held to corpora it did not write and to parsers it does not share code with. A blocking gate runs the built package over x509-limbo and Wycheproof, pinned by commit and SHA-256, and compares serial, validity, CA flag and fingerprint with OpenSSL.

The corpora #

Corpus What it holds Pinned at
x509-limbo 9 793 x509-limbo test cases of certificate validation, from RFC 5280, the CA/Browser Forum, webpki, BetterTLS and pathological inputs commit 118721335e67
Project Wycheproof 1 530 Wycheproof ECDSA vectors on P-256, P-384 and P-521, including deliberately mis-encoded signatures commit 3fa63dd0344a
NIST PKITS 223 path-validation tests of the US Federal PKI β€” 405 certificates and 173 revocation lists, with certificate policies, policy mapping, key rollover and indirect CRLs SHA-256 592f66030d2e of PKITS_data.zip
RFC 5280 Not a corpus of cases but the text L5 is held to: 182 requirement sentences of Β§4.1 and Β§4.2, each accounted for SHA-256 a2f2628c0a83 of rfc5280.txt
RFC 5652, RFC 3161, RFC 6960, RFC 7292, RFC 7468 The texts of CMS, timestamps, OCSP, PKCS #12 and PEM, read the same way: 132 requirement sentences, each held by a named test or excluded with its reason SHA-256 dbd209ae7844, 39fd17644ff2, 7e63ffa1ea2c, 168ce6749ac3, 0b2c3c2087cc of each rfc<NNNN>.txt

npm run conformance:fetch downloads them into test-output/corpora/ and refuses any file whose SHA-256 differs from .github/checksums/; nothing is committed. PKITS is distributed as a ZIP and nothing else, so it is pinned by the archive's own digest and by a per-file list of what was extracted from it β€” one digest fixes what NIST published, the other fixes what this project's ZIP reader made of it. scripts/lib/corpora.ts holds the pins, and the verify-docs rule corpus-pin-parity keeps the checksums, THIRD-PARTY-NOTICES.md and docs/assets/ecosystem.json in step with them.

The levels #

scripts/validate-certs.ts runs the package from dist/ β€” what users get, not the sources:

The write direction #

Everything above points one way: bytes someone else produced, read by pkinative. For what pkinative writes, the arrow points outward, and that direction has no corpus β€” nobody publishes a set of certificates a library is supposed to have written. The only oracle available is the tools themselves.

npm run interop (scripts/run-interop.ts) hands two sets of artefacts to every foreign tool it can find, and requires agreement on facts that have exactly one right answer β€” a serial number as an integer, a name attribute, the DNS names, whether a chain or a self-signature or a SignedData verifies, the digest and nonce a request carries β€” never on rendered text.

The readers, and what each is asked:

Tool Reads
openssl certificates (DER and PEM), the chain with -x509_strict, the TLS purpose and the host name, CSR self-signatures, every SignedData with cms -verify and the content it returns, the OCSP request with -req_text, the TimeStampReqs with ts -query -text
windows-cryptoapi every certificate, through CryptoAPI under Windows PowerShell 5.1
dotnet certificates (DER and PEM), the chain through X509Chain with a custom root and the server-authentication policy β€” on Windows, CryptoAPI's chain engine β€” CSRs through LoadSigningRequest, every SignedData through SignedCms.CheckSignature, the TimeStampReqs through Rfc3161TimestampRequest
gnutls-certtool certificates, the chain with the host name and the purpose, CSR self-signatures
go-x509 certificates, the signature and the whole path as a TLS client builds it, CSR self-signatures
python-cryptography certificates, each leaf's signature by its issuer, the Web PKI verifier on the baseline-shaped leaves, CSRs, the certificates a SignedData carries, the OCSP request
java-keytool certificates and the PKIX path through the JDK, CSRs through keytool -printcertreq
gpgsm every SignedData, with the CA in its trust list
zlint, pkilint every certificate created: zlint with the RFC sources everywhere and the CA/Browser Forum, Mozilla, Chrome and Apple sources on what is shaped for the Web PKI in a family it admits; pkilint's lint_pkix_cert on each and lint_pkix_signer_signee_cert_chain on each leaf with its CA

What decides whether the result means anything (scripts/lib/interop-judge.ts, every rule of it proved to fire by tests/tools/interop.test.ts):

The matrix runs blocking inside the conformance job, whose three contexts the ruleset already requires, and in the release gate, both with --require-all per platform: a tool REQUIRED_TOOLS names for the runner fails the job when it is missing. On Linux that is openssl and java-keytool, which the image carries, and gnutls-certtool, gpgsm, python-cryptography, go-x509, zlint and pkilint, which the workflow installs pinned; on Windows openssl, windows-cryptoapi and dotnet, which the image carries; on macOS openssl, the system build. Windows CryptoAPI runs on Windows only. Every other implemented tool runs where the image carries it and where it has been proved (TOOL_PLATFORMS), and a Windows workstation reaches the Linux readers through WSL. Windows certutil, macOS security, and dotnet on Linux and macOS (dotnet-linux, dotnet-macos β€” it is proved on Windows only) are declared in scripts/lib/interop.ts with the reason each is pending, and interop-matrix-declared holds the declarations to ROADMAP.md, THIRD-PARTY-NOTICES.md, this page and the workflow, so a gap cannot quietly disappear.

The first run of the extended set found one defect: the RSASSA-PSS AlgorithmIdentifier was written without the NULL after each digest OID, which the CA/Browser Forum baseline requirements fix byte for byte β€” zlint and pyca's Web PKI verifier refused every RSASSA-PSS certificate pkinative wrote, while the whole gate had been green, because no foreign tool had ever been shown one.

Key containers: the arrow turned back #

pkinative reads PKCS#8 and PKCS#12 and never writes either β€” it may not encrypt or wrap a key, by policy β€” so for key containers the same runner points the other way: each implemented tool writes, pkinative reads (scripts/lib/interop-keys.ts, the KEYS lines of npm run interop). Every case is declared in KEY_CONTAINER_CASES of scripts/lib/interop.ts, the runner refuses to evaluate one that is not, and interop-matrix-declared holds that list to this page in both directions. Three things are compared on every file, none of them decided by pkinative alone:

Case Written by pkinative reads
openssl:pkcs12-pbmac1 openssl pkcs12 -export -pbmac1_pbkdf2, EC P-256, RSA 2048 and Ed25519: PBES2 with PBKDF2/HMAC-SHA-256 and AES-256-CBC on the key and the certificates, an RFC 9579 PBMAC1 MAC valid, integrity verified, the key signs
openssl:pkcs12-default openssl pkcs12 -export, the same three keys: the same PBES2 encryption under an HMAC-SHA-256 MAC keyed with RFC 7292 Appendix B (OpenSSL 3.0 and later; the tool's own -info decides, so an older default is held to the legacy expectation) PKI_REASON_PKCS12_INTEGRITY_UNVERIFIED alone; valid with allowUnverifiedIntegrity, the key signs
openssl:pkcs12-legacy openssl pkcs12 -export -legacy: the certificates under pbeWithSHAAnd40BitRC2-CBC, the key under pbeWithSHAAnd3-KeyTripleDES-CBC, an HMAC-SHA-1 Appendix B MAC reported, never thrown: the integrity reason and PKI_REASON_PKCS12_ENCRYPTION_UNSUPPORTED naming each scheme
openssl:pkcs12-wrong-password the PBMAC1 file, read under another password PKI_REASON_PKCS12_MAC_MISMATCH, nothing decrypted
openssl:pkcs12-converted the conversion SECURITY.md documents: openssl pkcs12 -in legacy.p12 -legacy -aes256 -out bundle.pem, then openssl pkcs12 -export -in bundle.pem -pbmac1_pbkdf2 -out modern.p12 valid, integrity verified, the key signs; when the conversion fails, a failure β€” unless that openssl cannot run the second command on an encrypted PEM key it wrote itself with pkey -aes256, and then a skip naming it
openssl:pkcs8-pbes2 openssl pkcs8 -topk8 -v2 aes-256-cbc -v2prf hmacWithSHA256, the three keys decryptPrivateKey returns a key that signs
openssl:pkcs8-pbes1 openssl pkcs8 -topk8 -v1 PBE-SHA1-3DES decryptPrivateKey throws PKI_KEY_ENCRYPTION_UNSUPPORTED naming the scheme
windows-cryptoapi:pfx-export X509Certificate2.Export(X509ContentType.Pkcs12, password) from Windows PowerShell, on a certificate made in memory by CertificateRequest.CreateSelfSigned (no store touched), EC P-256 and RSA 2048: the key shrouded and the certificates encrypted, both under pbeWithSHAAnd3-KeyTripleDES-CBC, an HMAC-SHA-1 Appendix B MAC, 2000 iterations, DER throughout reported, never thrown: the integrity reason and 3DES named twice
windows-cryptoapi:pfx-pbes2 X509Certificate2.ExportPkcs12(Pkcs12ExportPbeParameters.Pbes2Aes256Sha256, password) from PowerShell 7 on .NET 9 or later: PBES2 with PBKDF2/HMAC-SHA-256 and AES-256-CBC, an HMAC-SHA-256 Appendix B MAC β€” .NET writes no PBMAC1 β€” 2000 iterations, DER throughout PKI_REASON_PKCS12_INTEGRITY_UNVERIFIED alone; valid with allowUnverifiedIntegrity, the key signs

The first run found one defect, in the report rather than the reading: a key shrouded under a PKCS#12 scheme, in a file whose certificates sit in contents the same refusal keeps shut β€” exactly what -legacy and a default Windows export write β€” was reported as PKI_REASON_PKCS12_KEY_UNMATCHED, whose remedy sends the caller after a certificate that could not have helped. It is now reported under its own scheme. Windows wrote DER at every level parsePkcs12 reads; encodingRules: 'ber' was not needed for any of it.

Signed structures: what the tools sign, pkinative verifies #

The same arrow for everything else pkinative verifies and never writes in the tool's dialect: CMS SignedData, RFC 3161 tokens, OCSP responses and CRLs (scripts/lib/interop-reads.ts, the READS lines). Each tool makes its own keys, its own CA and its own signers, signs, revokes and stamps β€” so the tool is the oracle: what it signed verifies, what it revoked is revoked, what it stamped is that imprint. Every case is declared in READ_CASES of scripts/lib/interop.ts with the verdict pkinative must reach, and interop-matrix-declared holds that list to the table below in both directions. pkinative is handed only the DER the tool wrote; PEM is unwrapped with Node's base64, never with pkinative's own decoder. A case the installed build cannot write is a skip when the tool is not the reference build, and a failure when it is.

Case Written by pkinative decides
openssl:cms-rsa openssl cms -sign -nodetach, RSA 2048 with PKCS #1 v1.5 verifySignedData valid
openssl:cms-rsa-pss openssl cms -sign -keyopt rsa_padding_mode:pss, detached valid
openssl:cms-ecdsa openssl cms -sign -nodetach -md sha384, ECDSA P-384 valid
openssl:cms-ed25519 openssl cms -sign -md sha512, Ed25519, detached valid
openssl:cms-ber-stream openssl cms -sign -stream -nodetach: indefinite-length BER refused under the default DER rules with a message naming encodingRules: 'ber'; valid under them
openssl:cms-tampered the attached SignedData with one content octet changed afterwards not valid: PKI_REASON_CMS_DIGEST_MISMATCH
openssl:tsp-reply openssl ts -reply to an openssl ts -query -cert, an RSA TSA whose timeStamping EKU is critical verifyTimeStampToken valid against the request and the data; PKI_REASON_TSP_IMPRINT_MISMATCH against other data
openssl:ocsp-good openssl ocsp -index, answered by a delegated responder (id-kp-OCSPSigning, ocspNoCheck) verifyCertificateChain valid with requireRevocation
openssl:ocsp-revoked the same responder, after openssl ca -revoke -crl_reason keyCompromise PKI_REASON_REVOKED
openssl:crl-base openssl ca -gencrl after that revocation the revoked certificate PKI_REASON_REVOKED, another valid
openssl:crl-delta openssl crl -gendelta -sha256 between that CRL and the next, after a second revocation with the base alone the second certificate is valid; with the base and the delta, PKI_REASON_REVOKED
openssl:csr openssl req -new for every key the matrix issues a certificate to (RSA 2048, ECDSA P-256 and P-384, Ed25519) parseCertificationRequest with zero diagnostics and the subject the tool was given; verifyCertificationRequest valid
gnutls-certtool:crl certtool --generate-crl over an ECDSA CA and one certificate it issued the listed certificate PKI_REASON_REVOKED, the other valid
gpgsm:cms-detached gpgsm --detach-sign, with an RSA key imported from PKCS #12 valid
gpgsm:cms-attached gpgsm --sign: indefinite-length BER valid with encodingRules: 'ber'
dotnet:cms-rsa SignedCms.ComputeSignature, RSA PKCS #1 v1.5, detached, IssuerAndSerialNumber valid
dotnet:cms-rsa-ski the same, attached, SubjectKeyIdentifier, SHA-512 valid
dotnet:cms-rsa-pss the same with RSASignaturePadding.Pss valid
dotnet:cms-ecdsa the same, ECDSA P-256 valid

Writing the delta case found something worth knowing about the tool rather than the library: openssl crl -gendelta signs with SHA-1 unless it is given a digest, and pkinative does not take a SHA-1 signature as evidence, so a delta written that way is not applied β€” the certificate it revokes stays valid. The case passes -sha256.

What is not claimed yet #

The 20 PKITS Β§4.8 certificate-policy tests are skipped, at L7 and β€” for the 20 messages their end entities signed β€” at L8, and they are the only tests of either corpus that are. Their expected result depends on the user-initial-policy-set a validator is given; PKITS answers per setting in its prose, the archive states none of them, and a runner that guessed would be scoring its own guess. A reviewed transcription belongs in scripts/data/pkits-score.json, where the 8 deviations already live.

And the score is a measurement, not a promise. It moves when the corpus is re-pinned, which is why the 1.0 freeze does not name it: what gets frozen is the decision surface β€” which certificate pkinative refuses and with which code β€” because that is a property of this library rather than of somebody else's test suite. The deviations file is where the two meet, and it is meant to shrink β€” it has, twice, and each time because an entry said plainly what was missing. The key-identifier entries said createCertificate had to be able to emit subjectKeyIdentifier and authorityKeyIdentifier before their absence could be diagnosed, or everything this library builds would trip its own reader; computeKeyIdentifier closed that, and those certificates are now reported rather than passed over in silence. The entries remain, because a missing key identifier is a hint and not a verdict β€” but they now record a decision instead of a gap.

Where it runs #

.github/workflows/conformance.yml runs the gate on every push to main and every pull request into it, weekly, and on demand, on Linux, Windows and macOS β€” three required checks, conformance, conformance-windows and conformance-macos. Three platforms is not redundancy: the command-line tool the L3 level cross-checks against differs by runner (OpenSSL, the Git for Windows build, LibreSSL), and so do line endings, path separators and the default shell. The release gate (npx tsx scripts/gate.ts --publish --require-all) runs it too. Locally:

npm run build
npm run conformance:fetch
npx tsx scripts/validate-certs.ts --require-all

Fixtures #

Six public certificates are committed as known-answer fixtures, each with its source, retrieval date and SHA-256 in tests/fixtures/PROVENANCE.md: ISRG Root X1 and X2, Let's Encrypt E7 and R12, the letsencrypt.org end-entity certificate, and the RFC 8410 Β§10.2 example. Their expected values were read with OpenSSL, and every one is also run through the node:crypto differential.