CLI use cases
Four pipelines built from the pkinative-cli samples: a CI gate on a chain and its expiry, revocation checked offline, a signed and timestamped release, and a PKCS#12 audit. Every command below is one a sample runs, or a flag the CLI guide documents.
pkinative-cli 1.0.0 ships 57 samples in its repository's samples/ directory, at least one for every invocation but doctor, each as a .sh and a .ps1 script that its CI runs and pins. A sample runs from the repository root against the test PKI in tests/fixtures/pki (F below), the inputs in samples/inputs (S) and an output directory (O), and calls ${PKINATIVE:-node dist/cli.cjs} โ with the CLI installed, PKINATIVE=pkinative. The cases below quote those command lines, then show the output the 1.0.0 binary prints for them; every flag is documented in the CLI guide.
F=tests/fixtures/pki S=samples/inputs O=test-output/samples
Case 1 โ A CI gate on a certificate chain and its expiry #
A deployment should not ship a certificate that does not chain to the roots it is meant to chain to, that names another host, or that expires before the next release. chain verify answers all three in one call, and its process contract โ the verdict on stdout, one envelope on stderr, exit 0, 1 or 2 โ is what a CI step branches on.
The building blocks, from samples/chain/:
# samples/chain/chain-verify.sh โ one-call verdict with revocation (expected exit 0)
pkinative chain verify "$F"/leaf.crt.pem --untrusted "$F"/inter.crt.pem --trust "$F"/root.crt.pem --host example.test --crl "$F"/inter.crl.pem --ocsp "$F"/leaf.ocsp.der --require-revocation --at 2027-01-01T00:00:00Z
# samples/chain/chain-verify-revoked.sh โ a revoked leaf fails the verdict (expected exit 1)
pkinative chain verify "$F"/revoked.crt.pem --untrusted "$F"/inter.crt.pem --trust "$F"/root.crt.pem --crl "$F"/inter.crl.pem --at 2027-01-01T00:00:00Z -q
# samples/chain/chain-build.sh โ find a path to a trust anchor, signatures not checked (expected exit 0)
pkinative chain build "$F"/leaf.crt.pem --untrusted "$F"/inter.crt.pem --trust "$F"/root.crt.pem --at 2027-01-01T00:00:00Z --json --summary
Add --json --summary and the first sample's verdict is two lines, stdout then stderr:
{"valid":true,"reasons":[],"path":["CN=example.test,O=pkinative-cli test,C=FR","CN=Test Intermediate CA,O=pkinative-cli test,C=FR","CN=Test Root CA,O=pkinative-cli test,C=FR"],"explored":2,"signatureVerifications":2}
{"ok":true,"command":"chain verify","valid":true,"pathLength":3,"signaturesChecked":true,"diagnostics":[]}
Expiry is a question of --at. The verdict is computed at the instant --at names โ ISO 8601, epoch milliseconds or now โ so an expiry gate is the same command asked about the future. Moving the first sample's --at past the leaf's notAfter (2046-01-01) gives exit 1, E_VERIFY_FAILED, one PKI_REASON_EXPIRED per certificate of the path, and two PKI_REASON_REVOCATION_UNKNOWN on the OCSP response, whose delegated responder certificate has expired too:
{"valid":false,"reasons":["PKI_REASON_EXPIRED","PKI_REASON_EXPIRED","PKI_REASON_EXPIRED","PKI_REASON_REVOCATION_UNKNOWN","PKI_REASON_REVOCATION_UNKNOWN"],"path":["CN=example.test,O=pkinative-cli test,C=FR","CN=Test Intermediate CA,O=pkinative-cli test,C=FR","CN=Test Root CA,O=pkinative-cli test,C=FR"],"explored":3,"signatureVerifications":2}
A CI step built on those pieces โ verify the chain as it will stand 30 days from now, and fail with the reason codes when it does not (GNU date and jq assumed on the runner):
AT="$(date -u -d '+30 days' +%Y-%m-%dT%H:%M:%SZ)"
pkinative chain verify server.pem --untrusted intermediate.pem --trust root.pem \
--host www.example.com --purpose serverAuth --at "$AT" \
--json --summary > verdict.json 2> envelope.json
case $? in
0) echo "chain valid on $AT" ;;
1) echo "chain refused: $(jq -r '.reasons | join(", ")' verdict.json)"; jq -c .error envelope.json; exit 1 ;;
2) echo "invocation error: $(jq -r .error.message envelope.json)"; exit 2 ;;
esac
A name mismatch is caught by the same call: on the sample chain, --host other.test exits 1 with PKI_REASON_NAME_MISMATCH, whose message lists the names the certificate does hold. Exit 2 is never a verdict โ an unknown flag, a missing --trust, both --host and --ip โ and a gate should fail the build on it rather than retry.
Limits. chain verify judges the files it is given: it never fetches an intermediate or a CRL, so a CI job must carry them (Case 2). chain build exits 0 whenever a path is found, because it checks no signature โ its valid is always false with PKI_REASON_SIGNATURE_NOT_CHECKED and its envelope says signaturesChecked: false โ so it is a diagnostic, never a gate.
Case 2 โ Offline and air-gapped revocation checking #
Revocation is where a verifier is most tempted to reach the network, and where an air-gapped signing host cannot. The CLI never fetches anything: it writes the OCSP request you send, and judges the CRL and the OCSP response you carry back.
The building blocks, from samples/ocsp/ and samples/crl/:
# samples/ocsp/ocsp-request.sh โ build an OCSP request (expected exit 0)
pkinative ocsp request --cert "$F"/leaf.crt.pem --issuer "$F"/inter.crt.pem -o "$O"/ocsp-req.der
# samples/crl/crl-verify-signature.sh โ verify a CRL signature (expected exit 0)
pkinative crl verify-signature "$F"/inter.crl.der --issuer "$F"/inter.crt.pem
# samples/crl/crl-find.sh โ look a serial up in a CRL (expected exit 0)
pkinative crl find "$F"/inter.crl.der --serial 1002
# samples/crl/crl-check.sh โ decide a status from a CRL (expected exit 0)
pkinative crl check "$F"/inter.crl.der --cert "$F"/leaf.crt.pem --issuer "$F"/inter.crt.pem --at 2027-01-01T00:00:00Z
# samples/ocsp/ocsp-verify-signature.sh โ verify the responder signature (expected exit 0)
pkinative ocsp verify-signature "$F"/leaf.ocsp.der
# samples/ocsp/ocsp-check.sh โ decide a status from an OCSP response (expected exit 1: revoked)
pkinative ocsp check "$F"/revoked.ocsp.der --cert "$F"/revoked.crt.pem --issuer "$F"/inter.crt.pem --at 2027-01-01T00:00:00Z -q
The workflow splits across the gap:
# Connected side: write the request with a random RFC 8954 nonce, send it, fetch the CRL.
pkinative ocsp request --cert leaf.pem --issuer ca.pem --nonce random -o req.der --json
# stderr: {"ok":true,"command":"ocsp request","nonce":"<32 hex digits>","bytes":106,"encoding":"der","output":"req.der","diagnostics":[]}
curl --data-binary @req.der -H 'Content-Type: application/ocsp-request' -o resp.der "$OCSP_URL"
curl -o ca.crl "$CRL_URL"
# Air-gapped side: judge the evidence you carried, at a pinned instant.
pkinative crl check ca.crl --cert leaf.pem --issuer ca.pem --at "$AT" --json --summary
pkinative ocsp check resp.der --cert leaf.pem --issuer ca.pem --nonce "$NONCE" --require-nonce --at "$AT" --json --summary
pkinative chain verify leaf.pem --untrusted ca.pem --trust root.pem --crl ca.crl --ocsp resp.der \
--ocsp-nonce "$NONCE" --require-ocsp-nonce --require-revocation --at "$AT" --json --summary
The URLs come from the certificate itself: cert inspect --extension crlDistributionPoints and --extension authorityInfoAccess. ocsp check recomputes the CertID with the responder's hash, finds the signer among --responder, the embedded certificates and the issuer, and accepts it only if it is the CA or a delegate the CA issued with the OCSPSigning purpose (RFC 6960 ยง4.2.2.2) โ unless you say --responder-trusted. crl check without --issuer counts the list's signature as unchecked, and the verdict fails.
On the samples' revoked leaf, both checks are negative verdicts โ exit 1, E_CHECK_FAILED, the reason with the revocation date:
{"valid":false,"reasons":[{"code":"PKI_REASON_REVOKED","message":"the certificate was revoked on 2026-06-01T00:00:00.000Z (reason: keyCompromise); a signature made before that instant may still be good, which is why the date is here","standard":"RFC 5280 ยง5.1","path":"ocsp"}],"status":"revoked","signatureVerified":true,"responderAuthorized":true}
and on the good leaf, ocsp check answers {"valid":true,"reasons":[],"status":"good","signatureVerified":true,"responderAuthorized":true} with exit 0.
Limits. --require-revocation turns an unknown status into a failure; without it, a chain whose revocation evidence is missing can still verify, which is a soft-fail decision you make explicitly. A CRL past its nextUpdate is stale: crl check accepts one only within --stale-tolerance <ms>, and chain verify, which takes no tolerance, reports PKI_REASON_REVOCATION_STALE; an OCSP thisUpdate in the future is refused by ocsp check beyond --future-tolerance (60000 ms by default) โ on an air-gapped host, pin --at rather than trust its clock.
Case 3 โ CMS signature and RFC 3161 timestamp in a release pipeline #
A release artefact signed with a detached CMS signature can be verified long after the signing certificate expires โ if a timestamp proves the signature existed while the certificate was valid.
The building blocks, from samples/cms/ and samples/tsp/ (cms-sign.sh signs with the CA that samples/cert/cert-create-ca.sh writes into $O):
# samples/cert/cert-create-ca.sh โ issue a self-signed Ed25519 CA (expected exit 0)
pkinative cert create --spec "$S"/ca.json --key "$F"/ed25519.key.pem -o "$O"/ca.pem
# samples/cms/cms-sign.sh โ sign detached; Ed25519 is deterministic (expected exit 0)
pkinative cms sign --content "$F"/content.txt --cert "$O"/ca.pem --key "$F"/ed25519.key.pem --detached --signing-time 2027-01-01T00:00:00Z -o "$O"/content.p7s
# samples/cms/cms-verify.sh โ verify an attached signature (expected exit 0)
pkinative cms verify "$F"/attached.p7s --trust "$F"/root.crt.pem --at 2027-01-01T00:00:00Z --json --summary
# samples/tsp/tsp-request.sh โ build a timestamp request (expected exit 0)
pkinative tsp request --data "$F"/content.txt --nonce 42 -o "$O"/req.tsq
# samples/tsp/tsp-verify.sh โ verify a timestamp against its request and data (expected exit 0)
pkinative tsp verify --response "$F"/content.tsr --request "$F"/content.tsq --data "$F"/content.txt --trust "$F"/root.crt.pem --at 2027-01-01T00:00:00Z -q
# samples/cms/cms-add-timestamp.sh โ refuse a token that stamps the content, not the signature value (expected exit 1)
pkinative cms add-timestamp "$F"/attached.p7s --token "$F"/content.tsr -o "$O"/stamped.p7s
cms sign verifies the new signature under --cert before it writes a byte, and its envelope reports what it wrote: {"ok":true,"command":"cms sign","detached":true,"bytes":980,"encoding":"der","output":"โฆ/content.p7s","diagnostics":[]}. The last sample is the one that matters for a pipeline: a counter timestamp stamps the signer's signature value, and a token over anything else โ here, over the content โ is refused before anything is written, with the recipe as remedy:
{"ok":false,"command":"cms add-timestamp","error":{"code":"E_INPUT","message":"The token does not stamp signer 0's signature value: its imprint is over something else (PKI_REASON_TSP_IMPRINT_MISMATCH).","remedy":"request the token over the signature value: cms inspect --fields signerInfos.signature, hash it, tsp request --digest <hex> --hash <alg>","reasons":[{"code":"PKI_REASON_TSP_IMPRINT_MISMATCH","message":"the token stamps a different hash from the one expected; it may be a perfectly good timestamp, of something else","standard":"RFC 3161 ยง2.4.2","path":"token.tstInfo.messageImprint"}]},"diagnostics":[{"code":"PKI_DIAG_CMS_SET_NOT_SORTED","severity":"warning","message":"this SET OF is not in DER order; nothing signed depends on it, but a strict DER reader will refuse the message","standard":"ITU-T X.690 ยง11.6","path":"content.certificates","offset":235}]}
The pipeline that follows that remedy (jq and xxd assumed on the runner; the TSA call is yours):
# 1. Sign the release artefact, detached.
pkinative cms sign --content release.tgz --cert signer.pem --chain intermediate.pem \
--key signer.key --password-file signer.pass --detached -o release.p7s
# 2. Timestamp the signature value, not the artefact.
pkinative cms inspect release.p7s --json --fields signerInfos.signature \
| jq -r '.signerInfos[0].signature' | xxd -r -p > signature.bin
pkinative tsp request --data signature.bin --nonce random -o stamp.tsq
curl --data-binary @stamp.tsq -H 'Content-Type: application/timestamp-query' -o stamp.tsr https://tsa.example.test/
# 3. Attach the token โ refused unless it stamps that signature value โ and verify at the stamped time.
pkinative cms add-timestamp release.p7s --token stamp.tsr -o release-stamped.p7s
pkinative cms verify release-stamped.p7s --content release.tgz --trust root.pem --at-timestamp --json --summary
cms inspect --fields signerInfos.signature prints the signature value as hex โ {"signerInfos":[{"signature":"3046022100f22eโฆ"}]} on the samples' attached message โ and tsp request --data hashes the bytes you give it (--hash, SHA-256 by default). cms verify --at-timestamp validates the signer at its verified timestamp instead of the wall clock. An ECDSA or RSA-PSS signature differs on each run; the signing sample uses Ed25519, which is deterministic, so its output can be pinned.
Limits. The CLI never posts to a TSA. tsp verify needs to be told what was stamped โ --request, --data or --digest โ and refuses (exit 2) when it is not, because a token verified without what it stamps proves nothing. A TSA certificate whose timeStamping EKU is not critical is accepted only under --allow-noncritical-eku.
Case 4 โ A PKCS#12 audit with --password-file #
A .p12 arriving from a partner or an old export should be described before anyone types its password, and opened without its key ever reaching a terminal or a log.
The building blocks, from samples/p12/ and samples/key/:
# samples/p12/p12-inspect.sh โ describe a PKCS#12 file, no password (expected exit 0)
pkinative p12 inspect "$F"/leaf.p12
# samples/p12/p12-verify-mac.sh โ verify the PBMAC1 integrity (expected exit 0)
PKINATIVE_PASSWORD=test-only-password pkinative p12 verify-mac "$F"/leaf.p12
# samples/p12/p12-bags.sh โ list the bags, decrypted (expected exit 0)
PKINATIVE_PASSWORD=test-only-password pkinative p12 bags "$F"/leaf.p12
# samples/p12/p12-open.sh โ open it and export its certificates (expected exit 0)
PKINATIVE_PASSWORD=test-only-password pkinative p12 open "$F"/leaf.p12 --certs-out "$O"/bundle.pem -q
# samples/key/key-inspect.sh โ describe an encrypted key, no password needed (expected exit 0)
pkinative key inspect "$F"/leaf.key.enc.pem --json
# samples/key/key-check.sh โ prove an encrypted key decrypts (expected exit 0)
PKINATIVE_PASSWORD=test-only-password pkinative key check "$F"/leaf.key.enc.pem --key-type ec-p256 --json
The samples pass the password through PKINATIVE_PASSWORD. An audit script on a shared host should prefer --password-file, which reads the first line of a file you protect, and never the command line โ --password is refused before any command runs:
umask 077 && printf '%s\n' "$P12_PASSWORD" > p12.pass
pkinative p12 inspect partner.p12 --json --fields mac.kind,mac.iterations,contents.path,contents.encryption.scheme
pkinative p12 verify-mac partner.p12 --password-file p12.pass --json --summary
pkinative p12 bags partner.p12 --password-file p12.pass --json
pkinative p12 open partner.p12 --password-file p12.pass --certs-out partner-chain.pem --json --summary
On the samples' leaf.p12, p12 inspect, p12 verify-mac and p12 open say what protects the file, whether the password is right, and what it holds โ its key imported as a non-extractable Web Crypto key, without a key byte anywhere:
{"mac":{"kind":"pbmac1","iterations":2048},"contents":[{"path":"authSafe[0]","encryption":{"scheme":"PBES2 (PBKDF2 with HMAC-SHA-256, AES-256-CBC)"}},{"path":"authSafe[1]"}]}
{"valid":true}
{"valid":true,"integrity":"verified","keys":[{"path":"authSafe[1].bags[0]","friendlyName":"test leaf","certificate":"CN=example.test,O=pkinative-cli test,C=FR","signingKey":{"algorithm":{"name":"ECDSA","namedCurve":"P-256","hash":"SHA-256"},"key":{"type":"private","algorithm":"ECDSA","extractable":false,"usages":["sign"]}}}],"certificates":[{"subject":"CN=example.test,O=pkinative-cli test,C=FR","serialNumber":"1001"},{"subject":"CN=Test Intermediate CA,O=pkinative-cli test,C=FR","serialNumber":"02"}],"crls":0,"reasons":[]}
The failures an audit meets are classes, each with its remedy (exit codes in brackets):
{"ok":false,"command":"p12 verify-mac","error":{"code":"E_PASSWORD","message":"The PKCS#12 MAC does not match: the password is wrong, or the file was altered.","remedy":"--password-file <file> | --password-stdin | PKINATIVE_PASSWORD (check the password)"},"diagnostics":[]}
{"ok":false,"command":"p12 open","error":{"code":"E_USAGE","message":"--password is refused: a password on the command line is visible to every process on the machine.","remedy":"--password-file <file> | --password-stdin | PKINATIVE_PASSWORD"},"diagnostics":[]}
A wrong password is E_PASSWORD [1]; a password on argv is E_USAGE [2]. A legacy file โ RC2 or 3DES, the RFC 7292 Appendix B MAC โ is described by p12 inspect (mac.kind is pkcs12-kdf, the scheme is named) and refused by p12 open with E_SECURITY [1], the reasons PKI_REASON_PKCS12_INTEGRITY_UNVERIFIED and PKI_REASON_PKCS12_ENCRYPTION_UNSUPPORTED, and the one-time OpenSSL 3.4 conversion as remedy. A file whose MAC cannot be checked opens only under --allow-unverified-integrity, for a file whose origin you trust; an RSA key opens only with --rsa-scheme pkcs1|pss.
Limits. No key or p12 report contains a key byte: they are allow-list views, and keys are imported non-extractable. The CLI writes no PKCS#12 or PKCS#8 โ a converted file comes from OpenSSL โ and --certs-out writes the certificates and CRLs only, and only from a file that opened.
See also #
- CLI guide โ every command, flag, exit code and error class.
- MCP use cases โ the same workflows driven by an AI assistant.
- Use cases โ the library in applications.
- Errors, diagnostics and reasons โ every
PKI_*code these cases print. - The CLI's samples and agent contract.