Pushgate

Trust architecture and PKI

How the platform establishes cryptographic trust, and where your keys stay yours.

This page covers the trust model and the key flow, and nothing else. The question it answers is the one a security reviewer asks first: what holds the private keys, what can reach them, and what happens when something in the chain is wrong.

One key, and everything else derived from it

A deployment persists exactly one secret of cryptographic consequence: 32 bytes of root key material. The Root CA, the Fulcio intermediate CA, the timestamp authority leaf, the JWT signing key, and the session secrets are all derived from it in memory at startup using HKDF-SHA256 (RFC 5869), each with its own domain-separated label.

Nothing else is stored. There is no keystore to back up, and no file on a running node holds a usable CA private key.

One root, everything derived
Root key material
32 bytes — the only stored secret
Root CA
P-256 · self-signed · 20 yr
Fulcio CA
intermediate · 5 yr · EKU code signing
TSA leaf
RFC 3161 · 3 yr · EKU time stamping
JWT signing key
ES256 service tokens
Session secrets
cookies and sessions
The Root CA issues the Fulcio CA and TSA certificates, so every signing leaf chains leaf → Fulcio CA → Root CA.

The derivation is deterministic. The same root material produces byte-identical public keys on every replica and across every restart, which is what lets a signing certificate minted on one replica verify against the CA on another without any shared state between them. Certificate DER bytes do vary per boot, because ECDSA signing is hedged, so the issued bundle is persisted and keyed by root-key fingerprint. Trust is evaluated on public keys, and those are stable.

KeyRoleLifetimeConstraint
Root CASelf-signed trust anchor20 yearsMaxPathLen=1
Fulcio CAIssues short-lived signing leaves5 yearsMaxPathLen=0, EKU code signing
TSA leafRFC 3161 timestamps3 yearsEKU id-kp-timeStamping

All keys are ECDSA P-256 with SHA-256. Nothing negotiates an algorithm at runtime, so there is no downgrade path to misconfigure.

Where the root key lives

Two sources, chosen by deployment mode. The derivation tree above is identical in both.

On AWS

The root comes from a KMS HMAC key. At boot the service calls kms:GenerateMac over a fixed label and gets back 32 deterministic bytes. The KMS key material never leaves the HSM, and the key ARN is read from SSM Parameter Store rather than being baked into configuration. The keys are provisioned by a bootstrap step; the infrastructure stack imports them and never creates them, so no deployment can quietly mint a new root.

Standalone

The root is an ECDSA P-256 PEM file on disk at a configured path, mode 0600. The operator generates it with openssl. The server never generates one on its own: if the path is set and the file is missing, boot fails with instructions rather than silently creating a new root, because silent regeneration would invalidate every derived credential on a restart after a volume failure.

The tradeoff, stated plainly. The HMAC-derived root has a property worth knowing before you find it. Any principal holding kms:GenerateMac on that key can recover the 32-byte root offline and derive every downstream key. The mitigations are that workload identity (IRSA) scopes the permission to a single service account, CloudTrail records every call, the recovered material grants platform credentials only and no cloud or tenant data access, and rotating the KMS key invalidates all derived material at once.

Running on AWS

Every API replica calls the same KMS key at boot and receives the same 32 bytes, so each one derives the same chain independently. Replicas hold no key material between them and coordinate nothing to agree on trust: a certificate minted on one replica verifies against the CA on any other. Adding a replica requires no key distribution step, and losing one loses nothing. The KMS key material itself never leaves the HSM.

Running standalone

The same binary, with every dependency embedded: the CA, the timestamp authority, the evidence store, identity, workflows, and the web UI. Storage is SQLite in WAL mode and a local blob directory. There is no outbound network dependency at all.

Standalone — one binary, no outbound dependency
Browser or CI client
platform binary — single process
Embedded services
Fulcio CA
TSA (RFC 3161)
Evidence store
Identity
Workflows
Web UI
Local state directory
root-key.pem
P-256 · mode 0600
SQLite
WAL mode
Blob directory
The root key is read once at boot; every key is derived from it in memory.

This is the mode that matters for a restricted or disconnected environment, where a dependency on a public certificate authority or a hosted timestamp authority would be a blocker on its own. Neither is in the path.

How a client learns what to trust

Clients do not ship pinned roots. They fetch one unauthenticated document and take both the endpoints and the trust anchors from it:

GET ${PLATFORM}/.well-known/judge-configuration

The response carries the Fulcio and TSA endpoints, the evidence store URL, and a trust_bundle_pem that inlines the Fulcio CA together with the Root CA. One fetch establishes where to sign and what to trust. A separate URL serves the TSA certificate chain, which is what a verifier needs to validate a timestamp, and to validate a signature after the leaf that made it has expired.

The practical effect is that rotating a chain does not require redistributing anything to clients. They read the current bundle on the next verification.

Signing and uploading are different acts

These get conflated, and separating them is deliberate.

ActWhat it provesAuthentication
SignThis identity produced this evidence at this timeNone. An ambient workload OIDC token is sufficient.
UploadThis evidence belongs to this tenant and productA session that carries the tenant binding.

Signing is keyless and needs no login. The build job exchanges an ambient OIDC token for a certificate valid for roughly ten minutes, signs, gets the signature timestamped, and throws the private key away. Uploading is what binds evidence to an owner, so that is where a session is required.

Keyless signing, step by step
  1. 1Build jobRequests an ambient workload token from the OIDC provider — a short-lived JWT carrying the job’s identity in its claims.
  2. 2Fulcio CAExchanges that token for a signing certificate: a leaf valid for about ten minutes, chaining to the Fulcio CA and then the Root CA.
  3. 3Build jobBuilds a DSSE envelope over the in-toto predicate and signs it with the ephemeral private key.
  4. 4TSATimestamps the signature (RFC 3161), which keeps it verifiable after the short-lived certificate expires.
  5. 5Build jobDiscards the private key.
  6. 6Evidence storeReceives the signed envelope — the upload, not the signing, is the session-authenticated step.
Nothing long-lived was issued, so nothing long-lived can be stolen.

No long-lived signing credential is ever issued, so there is nothing to steal, and nothing to find in a build image six months from now. The RFC 3161 timestamp is what keeps the signature verifiable after the certificate that made it has expired.

Verifying

Verification runs the same trust chain in reverse. The verifier fetches the discovery document for the trust bundle and the TSA chain URL, retrieves the attestations for the artifact digest from the evidence store, chains each signing leaf to the Fulcio CA and then to the Root CA, and validates the RFC 3161 timestamp against the TSA chain.

Then it evaluates the policy, which is where the real decision lives: which steps are required, which identities are allowed to sign each one, and what rules the evidence has to satisfy. A signature that is cryptographically valid but produced by the wrong identity, or missing a required step, fails. The result is reported per step, with reasons.

Where your keys stay yours

The platform's own PKI signs attestations and timestamps. It does not need to be the thing that signs your artifacts. The attestation signer is pluggable, and the choice is yours per pipeline:

SignerKey custody
Fulcio (default)Ephemeral key plus OIDC identity. Nothing persists.
AWS KMSAsymmetric KMS key. Private key stays in the HSM.
Azure Key Vault / GCP KMSSame model on the respective cloud HSM.
HashiCorp Vault TransitVault holds and uses the key.
SPIFFESVID from the Workload API, for mesh environments.
FilePEM on disk. Intended for disconnected environments.

A PKCS #11 signer, for direct integration with a customer-operated HSM, is planned and not yet shipped.

Every signature can be countersigned

Two signatures, and they do different jobs. Your key produces the authoritative signature. The platform countersigns with Fulcio for identity and timestamps with the TSA.

Every signature can be countersigned
Artifact or policy to be signed
DSSE envelope — two signatures
Authoritative signature
your key — customer HSM, KMS, or Vault; satisfies your assurance controls
Identity countersignature
Fulcio leaf — its SAN names the pipeline that produced the artifact
The TSA timestamps both (RFC 3161)
Verifiable as: who authorised it, which pipeline produced it, and when

The countersignature is not redundancy. A Fulcio leaf's subject alternative name carries the identity of the pipeline that produced the artifact, and because that key is ephemeral and issued against an ambient workload token rather than a stored secret, a build cannot mint provenance claiming to be a different builder. That is what evidences the builder identity requirement for SLSA Build Level 3. The RFC 3161 timestamp establishes when, and keeps the whole envelope verifiable after the ephemeral certificate expires.

Where this may not be sufficient on its own. An ephemeral, software-held key satisfies SLSA. It does not satisfy control families that require cryptographic operations be performed in a validated module under your control, which is where NIST 800-53 SC-12 and SC-13 land, along with any FIPS 140-3 obligation flowing from them. If your control set carries those requirements, the Fulcio countersignature alone will not meet them. That is precisely why the authoritative signature is yours and comes from your own key custody. The two together give you an artifact that is attributable to a pipeline, bound to a moment in time, and signed by a key held to your own assurance standard. Neither signature alone does all three.

Note that key custody is two decisions, not one: the key that signs attestations and the key that signs policies may reasonably differ, and the policy signing key is the more sensitive of the two, since it defines what counts as acceptable evidence. If the platform operator held your policy signing key, it could redefine the policy that gates your pipeline — so you hold it.

What provenance does not give you

Worth being direct about, because overstating it is how these conversations go wrong. Provenance records a build's inputs, command, environment, and output digest, signed at build time and bound to the artifact digest, so substitution after the build is detectable. It does not detect a builder that was already compromised when it ran. A compromised builder can emit accurate-looking attestations about a malicious build.

What raises that bar is the policy, not the signature: requiring attestations from several independent steps so no single compromised step satisfies the policy alone, constraining which identities may sign each step, requiring ephemeral build environments, and verifying that materials resolve to approved sources.

A certificate authority's identity is its key, not its name

Operational warning. Every deployment derives its common names from the same code, so two entirely separate platforms produce CAs with identical subject strings and completely different keys. Common name equality means nothing. Only the key identifies a trust anchor: the SPKI fingerprint, or the subject and authority key identifiers.

Any policy that embeds trust roots should derive them from the discovery document of the same platform the artifacts were signed against, rather than having them copied in by hand. Hand-copied roots are how a policy ends up trusting one environment while the artifacts were signed by another, and the failure surfaces as zero valid verifiers and an ECDSA verification error against a certificate whose name looks correct.

Key rotation

There are two rotations, and they are deliberately different in cost. Both fall out of the derivation design rather than being bolted on.

Rotating the CA chain without touching the root

Each PKI derivation label carries a version prefix. Incrementing a single constant changes the labels, which changes the derived keys, which produces a new Root CA, Fulcio CA, and TSA leaf. The root key material is untouched, and no KMS or HSM operation is involved.

Because the old certificates are still derivable from the same root at the previous version, the platform can serve both during a dual-trust window. Attestations signed under the old chain keep verifying while newly issued leaves chain to the new one. That is what makes routine rotation survivable rather than a flag day, and it is the reason cryptoperiods can be set conservatively in the first place: Root CA 20 years, Fulcio CA 5, TSA leaf 3, per NIST SP 800-57 Part 1.

One asymmetry worth knowing. The JWT signing label carries no version prefix, for backward compatibility. A version bump rotates the CA chain but leaves the JWT signing key unchanged. Rotating that key requires changing the root key material itself. It is a deliberate exception, not an oversight, and it means "rotate the PKI" and "rotate all credentials" are two different operations.

Rotating the root

A new KMS HMAC key is provisioned, the ARN is updated, and the deployment rolls. Everything derived changes simultaneously: the entire CA chain, the JWT signing key, session secrets, and the internal service token. Existing tokens stop verifying and existing sessions are invalidated, so replicas have to move together rather than gradually.

The upside of that bluntness is containment. If the root is suspected compromised, one operation invalidates every credential derived from it, and there is no inventory of scattered keys to chase. The blast radius is knowable because the derivation tree is the complete list of what exists.

ScenarioActionBlast radius
Scheduled CA rotationIncrement the version constant and deploy.CA chain only. Dual-trust window keeps prior attestations verifiable.
Rotate all credentialsNew KMS key, update the ARN, coordinated rollout.Everything derived. Sessions and service tokens invalidated.
Suspected root compromiseRoot rotation. One operation.Total, and intentionally so. No key sprawl to hunt down.
Lost root, standaloneRestore from the offline encrypted backup.Unrecoverable without that backup. This is by design.