Platform PKI & trust
This page explains the public-key infrastructure behind the TestifySec platform —
the certificate authorities cilock signs and verifies against, how your client
discovers them, and the one mistake that breaks supply-chain verification in a way
the error messages don't make obvious: trusting the wrong platform's roots when
the Common Names happen to match.
If you only need to connect and run, start with Connect to the platform. This page is the reference for why that works and what you are trusting. For the conceptual threat model (what CI/lock does and does not protect against), see Trust model; for signer identities, see Signing & identity.
One root, two CAs
A TestifySec platform derives its entire PKI from a single root key. The root key HKDF-derives (RFC 5869) three things, in memory, at startup:
- a self-signed Root CA (the trust anchor),
- a Fulcio CA (intermediate) that mints the short-lived signing leaves, and
- a TSA leaf that signs RFC 3161 timestamps.
Because derivation is deterministic, every replica of the platform derives the same CA public keys — so a signature minted on one replica verifies on any other. The hosted platform runs three replicas behind one URL; you never see this, but it's why keyless signing is reliable under load.
Choose an explicit verification trust posture. CI/lock can use operator-supplied roots, policy trust embedded in a particular release, or platform discovery with the applicable pinning rules. Discovery alone is not independent confirmation that a platform is the authority you intended.
Discovery: where trust comes from
Every platform publishes one unauthenticated document:
curl -fsSL "$PLATFORM_URL/.well-known/judge-configuration" | jq .{
"archivista_url": "https://platform.testifysec.com/archivista",
"fulcio_grpc_addr": "[::]:5554",
"graphql_url": "https://platform.testifysec.com/query",
"tsa_url": "https://platform.testifysec.com/api/v1/timestamp",
"signing": {
"assurance_level": "aal1",
"fulcio_oidc_issuer": "https://platform.testifysec.com/fulcio/oidc",
"fulcio_url": "https://platform.testifysec.com",
"oidc_audience": "sigstore",
"trust_bundle_pem": "-----BEGIN CERTIFICATE-----…", // Fulcio CA + Root CA
"trust_bundle_url": "https://platform.testifysec.com/api/v1/fulcio/trustbundle",
"tsa_cert_chain_url": "https://platform.testifysec.com/api/v1/timestamp/certchain"
}
}The field that matters most is signing.trust_bundle_pem — it inlines the
Fulcio CA and the Root CA, so one fetch tells your client both where to sign and
what to trust. tsa_cert_chain_url serves the TSA leaf + Root CA, used to validate
timestamps (and to validate a keyless signature after its short-lived leaf expires).
$PLATFORM_URL is https://platform.testifysec.com for the hosted platform, or your
own host for a self-hosted / --standalone instance.
Signing and uploading use separate authority
This is the single most useful distinction to internalize:
| What you're doing | What it proves | Authority |
|---|---|---|
| Sign (Fulcio + TSA) | the certificate principal signed these exact bytes at this time | CI workflow OIDC (GitHub Actions, GitLab.com, Buildkite, CircleCI); an enrolled agent's own SPIFFE identity (cilock enroll agent); or a person's cilock login credential, whose login must have reached AAL2 unless the tenant opted out |
| Upload (Archivista) | this evidence belongs to this tenant/subject | A separate purpose-scoped tenant credential |
Signing is keyless. On GitHub Actions, with id-token: write, the runner mints an
ambient OIDC token; the platform Fulcio exchanges it for a leaf that lives ~10 minutes —
long enough to sign, too short to be worth stealing. The TSA timestamps the
signature so it stays verifiable long after the leaf expires. A workflow OIDC
request is a workload principal. The platform Fulcio also accepts GitLab.com,
Buildkite and CircleCI workload identities, and cilock run --platform-url fetches the
job's token on those too. On Kubernetes, sign keyless against public Sigstore, and on
other CI sign with a key. See the
support matrix for the level each environment reaches.
On a workstation, an agent signs as itself: cilock enroll agent mints a
time-bound agent principal with its own SPIFFE ID after a person approves it at AAL2
(passkey or second factor), and cilock run then signs as that agent, never as the person. A person's
own cilock login credential mints a token at the assurance level its login reached.
By default the tenant requires AAL2 (a passkey) for that exchange; a tenant that opts
out still lets the local client exchange a stored API credential non-interactively for an AAL1 token
naming its creator's email. Either way the level belongs to the login, not to each
signature: it does not prove a person was present when a given signature was made,
and agents must not use that path. The platform's explicit, server-observed human/agent ceremony remains a target
for binding a person's signature to the exact bytes signed.
Uploading binds evidence to your tenant, so it needs a different credential.
cilock login supplies a human tenant session;
cilock login --workflow-identity exchanges exact workload identity in CI.
Neither path changes the principal already bound into the signing certificate.
Verifying
With an appropriate platform session and trust configuration, cilock verify can derive policy-signer trust from discovery. Pinnable sessions retain the adopted trust pin; changed roots are refused until a verified rotation is explicitly accepted. A session that cannot retain a pin requires an explicit trust decision or supplied roots. Review the installed release and its configured trust posture before relying on defaults:
cilock verify ./myapp -p policy.json --platform-url "$PLATFORM_URL" --enable-archivistaOffline (no session, air-gapped), you supply trust yourself:
cilock verify ./myapp -p policy.json \
--policy-ca-roots roots.pem \
-a attestation-1.json -a attestation-2.jsonUnder the hood, verify chains each signing leaf to the Fulcio CA and then to the Root CA, and validates the RFC 3161 timestamp against the TSA chain — which is what lets a years-old signature still verify after its leaf has long expired.
⚠️ Same CN, different key = wrong platform
This is the failure that looks like a bug in cilock but is actually a trust
misconfiguration, and it is easy to hit because every TestifySec platform
derives its certificates from the same code — so they all share the same Common
Names:
- Root CA CN:
TestifySec Platform Root CA - Fulcio CA CN:
TestifySec Platform Fulcio CA - TSA CN:
TestifySec Platform TSA
Production and staging are different platforms with identical CNs but different keys. A Common Name tells you a certificate's role; it tells you nothing about which platform issued it. Only the key does.
Symptom. Verification fails with verifiers=0 on every collection, and any
timestamp check fails with x509: ECDSA verification failure — against a Root CA
whose name matches what you expected.
Cause. Something on the verify side trusts platform A while the evidence was signed by platform B. The classic case: a release policy that embeds staging roots while the binaries were production-signed. A prod leaf cannot chain to a staging Root CA, no matter how identical the names are.
How to confirm it. Compare the Root CA key fingerprint from each side. They must match.
# Root CA public-key fingerprint (SPKI) from a platform's discovery
curl -fsSL "$PLATFORM_URL/.well-known/judge-configuration" \
| jq -r '.signing.trust_bundle_pem' \
| awk 'BEGIN{n=0} /BEGIN CERT/{n++} {if(n==2) print}' \
| openssl x509 -noout -pubkey \
| openssl pkey -pubin -outform DER \
| openssl dgst -sha256 | cut -c1-8If the fingerprint from the platform you signed against differs from the Root CA
embedded in your policy (or passed via --policy-ca-roots), you are trusting the
wrong platform. Re-fetch trust from the same $PLATFORM_URL you signed against,
and re-sign any signed policy against that platform.
Rule of thumb: when two roots have the same CN, compare their keys (SPKI), never their names. Same name + different key = different platform = verification fails closed.
See also
- Connect to the platform —
cilock login,cilock use,cilock doctor,cilock trust. - Trust model — what CI/lock protects against (and what it doesn't).
- Signing & identity — signer providers and how functionaries are matched.
- Timestamping — verifying expired-certificate signatures.
- Policy schema —
roots,timestampauthorities, functionaries, Rego. - CLI reference —
cilock login,cilock verify,cilock signflags.
Reference generated from the product documentation. Match commands and support details to your installed release.