Code signing & timestamping — prove an artifact is genuine, and prove when
What it is
Code signing is putting a verifiable signature on a software artifact — a binary, a container image, an SBOM — so anyone can confirm it came from you and wasn't tampered with. Timestamping is getting a trusted third party to attest when something was signed, so the signature stays verifiable even after the signing certificate expires. trstctl provides both: a governed code-signing service and an RFC 3161 timestamping authority (TSA).
Why it exists
Software supply-chain attacks slip malicious artifacts into a trusted pipeline. Signing every artifact and verifying signatures before you run them closes that door. But signing has two hazards: the key is extremely valuable, so it must never sit in a build script, and signatures normally become unverifiable once the certificate expires, so long-lived artifacts "rot." trstctl addresses both — keys stay in an HSM/the isolated signer, the signing path composes with the live policy and distinct-approver gates when enabled, and the TSA supplies the timestamps that give signatures long-term validity.
How it works
The code-signing service (F50)
The service signs the digest (hash) of an artifact, never the artifact itself, so it works for anything from a 4 KB manifest to a 4 GB image. Two modes:
- Key-based signing. The API seals the complete command with tenant- and
operation-bound AAD and projects it as a
codesign.commandoutbox row (eventcodesign.commanded) in one PostgreSQL transaction. The bounded outbox worker opens it and, whenca.policy.enabledand/orca.policy.require_approvalis configured, evaluatesMaySign(tenant, principal, key, digest)through the same OPA evaluator and distinct-approver store the served lifecycle gate uses; a denial is audited (codesign.refused) and signs nothing. On approval, the key resolves to a signer handle — persistent and purpose-constrained in production, or an explicitly configured ephemeral resolver for eval/test — and the digest is signed through the signer boundary, where the private key never appears in the request, response, logs, or API process memory. The isolated signer journals the operation before replying, so a crash after signing replays the same bytes instead of signing twice, and the result returns as an immutablecodesign.signedevent.codesign.completedthen atomically queues transparency-log publication (transparency.rekorby default); the request thread only polls its own operation, never calling the signer or Rekor inline or holding a database transaction while waiting. - Keyless signing (Sigstore/Fulcio style). Instead of a long-lived key, the caller
presents a verified attestation — for example, a CI job's
OIDC identity — sealed inside the command, never plaintext in the event log,
PostgreSQL, or outbox. The outbox worker verifies that proof through the configured
Fulcio-style attestor, creates a deterministic ephemeral signer handle, and signs the
digest. The bound Fulcio SAN/issuer come from the verified attestation, never
caller-supplied strings: a claim that contradicts the attestation is refused
(
codesign.keyless.refused), and no verified attestation is rejected outright. Completion atomically queues both the Rekor bundle and a durablecodesign.cleanupcommand; the handle derives deterministically from the operation ID even if a crash loses the in-memory value first. Cleanup is idempotent and recordscodesign.ephemeral.destroyed, so a crash cannot strand an ephemeral handle.
Verification (Verify, VerifyKeyless) routes through the same signer boundary, with
each tenant's data isolated at the database layer and digests/signatures held as
wipeable, zeroed []byte buffers, never strings.
The timestamping authority (F51)
A TSA answers one question with a signed token: here is a hash, certify the time right
now. trstctl's TSA (RFC 3161) builds a TSTInfo record — policy, hash algorithm, the
submitted hash, a monotonic serial, and the generation time — and signs it through the
signer boundary, kept in the isolated signing service rather than the API process.
Each issuance is recorded as an immutable tsa.timestamp.issued event.
The payoff is long-term validity (LTV): a VerifyLongTermValidity check confirms the
signature and that the timestamp falls within the signing certificate's validity
window, so you can prove an artifact was signed while the certificate was still good,
even years after it has expired — what keeps a five-year-old signed release
verifiable.
In the console
Open Software Trust → Software signing. The /codesign screen offers key-backed
and keyless (Fulcio) signing: it submits only
the artifact digest and renders the returned signature receipt, so private keys and
artifact bytes never enter the browser. See The web console.
For timestamping, open Certificates → Protocols, then Set up and operate
methods. Timestamp authority readiness checks the exact running process before
you send an artifact hash. It confirms the tenant-bound /tsa mount, active protocol
profile, stable timestamping-only certificate, isolated signer connection, immutable
audit path, and bounded responder capacity. The check is deliberately effect-free: it issues no
timestamp, reads no certificate file, contacts no signer or network, and performs no
write. A failed gate names the safe repair and leaves Check again available.
Readiness is not interoperability proof. The OpenSSL query, HTTP POST, and verify commands in the same Protocols workspace are the real wire test. OpenSSL hashes the artifact locally; trstctl receives only the hash inside the RFC 3161 request. The artifact and its private signing key never enter the console or timestamp service.
Use it
The code-signing API requires the shipped code_signing configuration, which
tenant-binds signer handles and Fulcio-style attestors and pins the Rekor log public
key. Sign exactly one 32-byte SHA-256 artifact digest through the REST API or CLI —
trstctl signs those exact bytes and never hashes the digest again. The authenticated
token subject becomes the signer principal; the request body carries no trusted
principal field. See Configuration for a complete
production example.
Key-based signing:
cat > code-sign.json <<'JSON'
{
"key_id": "release-key",
"artifact_type": "oci-image",
"digest": "4EW4IfBBkDngEwN3v+ChO06PV2er4tF7nEVmFev3x1g="
}
JSON
trstctl-cli --idempotency-key release-sign-2026-06-25 code-signing sign -f code-sign.json
Keyless/Sigstore signing:
cat > code-sign-keyless.json <<'JSON'
{
"artifact_type": "oci-image",
"digest": "4EW4IfBBkDngEwN3v+ChO06PV2er4tF7nEVmFev3x1g=",
"identity_method": "github_oidc",
"identity_payload": "eyJqd3QiOiJleGFtcGxlIn0="
}
JSON
trstctl-cli --idempotency-key release-keyless-2026-06-25 code-signing keyless -f code-sign-keyless.json
Both responses return algorithm, signature, public_key_der, artifact_type, and
transparency_destination (base64 JSON bytes for the key fields); key-based responses
also include key_id, keyless responses the verified fulcio_san/fulcio_issuer. The
outbox worker submits an official Rekor v1 HashedRekord, confirms it binds the digest,
signature, and public key, and checks the signed-entry timestamp against the
operator-pinned log key before acknowledging delivery — on an idempotent 409 it
instead follows the same-origin Rekor Location and re-verifies the existing entry.
Idempotency-Key binds the canonical command: a replay returns the original response
byte-for-byte without signing again, and reusing the key for a different digest, key,
artifact type, principal, or identity proof is rejected. A terminal worker failure
projects an immutable codesign.failed fact before the outbox dead-letters it, even if
the caller disconnected; failure values are closed-form codes, never an upstream error
string that could echo identity material.
When ca.policy.require_approval is enabled, the first denied response is 403 with
a non-secret approval_required:codesign:<sha256> resource. A distinct approver
records {"action":"sign"} for it through
POST /api/v1/identities/{resource}/approvals; the requester resubmits the same
principal/key/digest with a new Idempotency-Key. The resource binds the tenant-scoped
principal, key (or keyless identity), and exact digest, so it cannot authorize another
caller or artifact — the approval route requires certs:issue, the signing request
keys:write.
For long-term validity, timestamp the returned signature through the TSA:
openssl ts -query -data signature.bin -sha256 -cert -out signature.tsq
curl -sS -H 'Content-Type: application/timestamp-query' \
--data-binary @signature.tsq \
https://trstctl.example.com/tsa \
-o signature.tsr
An authenticated automation can review the same zero-effect runtime gates with
POST /api/v1/protocols/tsa/qualification using certs:read. Its response contains
only readiness booleans, the public endpoint and policy OID, bounded explanations,
and recovery steps. It never returns tenant IDs, certificate bytes or paths, signer
handles, request bodies, tokens, or keys. Timestamp issuance remains exclusively at
the public protocol endpoint POST /tsa.
The equivalent headless operator command is:
trstctl protocols tsa qualify
It calls that same read-only qualification surface. It sends no request body or mutation headers and cannot issue a timestamp; use the OpenSSL flow above for the real RFC 3161 proof.
Preview, execute, recover, prove
The console and headless client now use the same two-step safety contract:
- Preview validates one exact SHA-256 digest, artifact type, tenant, caller, managed-key ID or keyless identity proof, approval posture, isolated-signer algorithm, and Rekor destination. The server returns a keyed request fingerprint plus a configuration fingerprint. It does not resolve a live signer, attest the identity proof, append an event, reserve idempotency, write SQL, or contact Fulcio/Rekor. The keyless proof is never returned in the plan.
- Execute sends that fingerprint with one retained
Idempotency-Key. The server recomputes readiness and the keyed fingerprint before a command can exist. A changed digest, caller, signer, proof, or runtime configuration returns409and signs nothing. - Recover retries an uncertain response with the unchanged request and the same idempotency key. A new command requires a new preview. The recent-outcomes ledger shows durable signing and transparency failures without opening the sealed command.
- Prove verifies the returned signature against the exact input digest and public
key, then confirms the operation is
completedand Rekor state isverified.
Managed-key automation:
trstctl-cli code-signing preview -f code-sign.json
trstctl-cli --idempotency-key release-sign-1 code-signing sign -f reviewed-code-sign.json
Keyless automation uses code-signing keyless-preview followed by
code-signing keyless. Put the returned request_fingerprint in the execution
body as preview_fingerprint; never put an OIDC proof in logs or evidence.
Pitfalls & limits
- Serving status: code signing is reviewed at
POST /api/v1/code-signing/previewandPOST /api/v1/code-signing/keyless/preview, then served atPOST /api/v1/code-signing/signandPOST /api/v1/code-signing/keyless, with matchingtrstctl-cli code-signing preview,code-signing keyless-preview,code-signing sign, andcode-signing keylesscommands. The shipped binary builds the service fromcode_signing, fails closed with501while that configuration is disabled, and fails closed at startup if an enabled configuration lacks an isolated signer, tenant-bound keys/attestors, or pinned Rekor log trust. Mutations requireIdempotency-Keyandkeys:write; policy and distinct-approver enforcement engage whenca.policy.enabled/ca.policy.require_approvalare set, otherwise RBAC plus signer purpose constraints are the authorization boundary. The TSA is served at/tsawhenprotocols.tsa.enabledplusprotocols.tsa.tenant_idare set, returningapplication/timestamp-replyTimeStampRespbodies. The console readiness check is a preview, not a synthetic timestamp: only a real request to/tsacalls the signer and emitstsa.timestamp.issued. - Wire formats differ by surface: the TSA emits a real RFC 3161
TimeStampToken— a CMSSignedDataover a DERTSTInfo— wrapped in the requiredTimeStampRespenvelope for stock verifiers (openssl ts -verify, DSS/ESS validators). Code signing itself returns trstctl's JSON signature receipt and queues Rekor publication through outbox: the payload holds digest, signature, public key, key id or Fulcio identity, and no private material or artifact bytes. For byte-level cosign bundle interchange, validate that encoding separately. - Keys belong in the signer: the code-signing resolver uses persistent,
purpose-constrained keys inside the isolated
trstctl-signer, so signing keys never live in a build agent. The separately served HSM/KMS and CA custody paths are not currently a code-signing key resolver — don't call a configured handle hardware-backed unless that bridge is added and independently proven. - Keyless still needs a real attestation: it's only as strong as the OIDC identity you verify. The served keyless path derives the signed SAN/issuer from that attestation and refuses a conflicting SAN/issuer, or none at all.
Reference
- Code signing:
POST /api/v1/code-signing/preview,POST /api/v1/code-signing/keyless/preview,POST /api/v1/code-signing/sign,POST /api/v1/code-signing/keyless,trstctl-cli code-signing preview,trstctl-cli code-signing keyless-preview,trstctl-cli code-signing sign,trstctl-cli code-signing keyless,Service.Sign,Service.SignKeyless,Verify,VerifyKeyless. - Timestamping:
Authority.Timestamp,Verify,VerifyLongTermValidity(RFC 3161). - Events:
codesign.commanded,codesign.completed,codesign.failed,codesign.ephemeral.destroyed,codesign.signed,codesign.refused,codesign.keyless.signed,attestation.verified,tsa.timestamp.issued; worker execution usescodesign.command/codesign.cleanup, and Rekor publication uses thetransparency.rekoroutbox destination. - Related: the signing key lives behind the separate, isolated signing service, never in the API process; the supply-chain story is in Supply chain.
See also
Issuance & certificate authorities (HSM-backed keys) · Workload identity (the attestation behind keyless signing) · Supply chain · Signing-service design · glossary: HSM/KMS, attestation, fingerprint
Covers: F50, F51