6.7.8 Signing and Verifying Artifacts
recommendedA successful signature check should answer a narrow question: did a trusted signing identity authorize this exact immutable subject? It should not merely confirm that some signature sits next to an artifact name. The release flow must therefore carry the subject digest across the build-to-signer boundary, keep signing credentials outside ordinary Bazel actions, and verify both the digest and the intended signer before promotion.1
Sign the subject, not its mutable name
6.7.4 From Build to Release Candidate establishes the input to signing: a technically
eligible release candidate whose outputs are identified by cryptographic digest and
whose required build evidence is complete. A tag such as stable, a version such as
2.4.0, and a filename such as server.tar help people find an artifact, but those
names can be reassigned. They are references to the subject, not the subject itself.
A signing request should carry an explicit, reviewable tuple:
subject:
media_type: application/vnd.example.release-archive
digest_algorithm: sha256
digest: 8b7...f21
signing_context:
release_channel: production
candidate_id: rc-1842
The exact wire format belongs to the selected signing system. The invariant is more
important than the format: the signer authorizes a digest. If the release channel or
candidate transition is meant to limit where that authorization can be replayed, bind
the context into the signed payload or require it through verifier policy for that
signed subject. Unsigned context stored only beside the request does not constrain
reuse. The verifier recomputes or otherwise obtains the digest of the bytes it will
consume, compares it with the signed subject, and evaluates the signer identity and
the required context. Looking up server:stable, verifying a signature for a
different digest, and then deploying whatever stable currently names breaks that
chain.
OCI images make the distinction concrete. rules_oci supports digest-pinned image
inputs and separates image construction from explicit registry operations. Its
signing and attestation APIs are developer preview, so their current rule shape is
evidence of one integration pattern, not a stable signing contract to standardize
across every artifact type.2 Distroless provides a production case: its published
images expose a Cosign verification path, while its internal Bazel release code keeps
image composition, signing, and pushing as distinguishable parts of the release
implementation.3
Put the signer after the cacheable build
The signing credential must not become an input, environment value, command-line argument, tool configuration file, or mounted secret of an ordinary cacheable action. If it does, the credential can cross into an execution request, remote worker, action log, cache object, or retained build evidence. It also couples a repeatable build to an authority-bearing side effect: a cache miss can now decide whether a release is signed.
Use the identity separation from 6.7.5 Build Identities and Secrets. The Bazel client and executors assemble the candidate. An eligibility service reconciles the result. A separate signer receives the immutable subject and only the evidence needed to authorize its signing request. The private key or equivalent signing authority remains behind that signer boundary. The returned signature or signed attestation is release evidence; it may be stored and distributed without giving the build access to the signing credential.
The existing identity and secret boundary contract makes the role split inspectable: the Bazel client, executor, provenance builder, signer, and publisher each have one allowed authority and explicitly deny adjacent ones. It is a deployment-neutral policy fixture, not a signer implementation; a real deployment must test its native IAM and retained surfaces. This separation is also a direct defense against user-defined build commands exfiltrating the signing key.1
This yields a useful retry boundary:
cacheable assembly -> eligibility -> signing -> verification -> publication
Rebuilding may reproduce or reuse the same candidate digest, but it does not sign or
publish anything merely because bazel build ran. A signing outage leaves an eligible
candidate unsigned rather than turning it into a failed build. A verification failure
leaves it unverified rather than causing an automatic rebuild under the same potentially
compromised path. 6.7.10 Publishing Artifacts Reliably then consumes the verified,
immutable handoff without confusing publication retries with build retries.
Signing an attestation follows the same boundary but authorizes a different object.
The signed payload contains claims about a subject; the verifier must check the
payload's signature and confirm that the payload names the digest being promoted.
Attaching an SPDX statement with Cosign, as the rules_oci example does, transports a
supplied statement. It does not generate that statement or prove that its component
inventory is complete.2 6.7.6 Build Provenance and Attestations covers which build claims can be
supported before they are signed.
Verification is a policy-bearing operation
Cryptographic validity is necessary but insufficient. A verifier needs an explicit trust input and an expected subject. The trust checks depend on the signing scheme:
- with a fixed key, configure the exact expected verification key or trust anchor;
- with keyless signing, require the certificate identity and its OIDC issuer, then validate that certificate path under a configured trust root.3
An identity and a trust root are not interchangeable alternatives. The identity says who the verifier is prepared to accept; the configured key or trust root establishes the authority under which the signature or certificate is authenticated.
At minimum, the verifier record should retain:
- the digest and digest algorithm of the consumed artifact;
- the signature and signed payload or envelope that were checked;
- the scheme-specific verification material, or an immutable retained locator for it;
- the authenticated signer claim and the configured key or trust root actually used;
- the intended release context, when the signature is scoped by one;
- the verifier and trust-configuration versions; and
- a resolved result that downstream promotion can consume.
For example, a current Sigstore bundle retains the signed message together with scheme-specific material such as the signing certificate and transparency-log or timestamp evidence. The verifier still applies its configured Sigstore trust root and expected certificate identity plus OIDC issuer; the bundle is evidence, not the policy.
This record prevents a green verify step from becoming an unexplained Boolean. It
also lets an incident responder answer whether a later trust-policy change affects a
past promotion. The build graph alone cannot answer that question: Bazel produced the
subject, but the signer and verifier establish a separate authority relationship.
Authenticating the signature, binding its payload to the artifact digest, and checking
the accepted signer or policy are distinct verifier steps.1
Use explicit outcomes. accepted means the subject, signature, signer, and required
context all agree under the named trust configuration. rejected means available
evidence contradicts one of those requirements. unknown means required signature or
trust evidence is missing, inaccessible, or cannot be evaluated. Both rejected and
unknown block promotion, but preserving the distinction tells an operator whether to
investigate hostile or contradictory evidence, or restore an unavailable verification
dependency. These are local workflow outcomes, not SLSA or Sigstore protocol states.
Choose one of two integration shapes deliberately:
- Detached release stage. The release orchestrator hands a digest-identified candidate to a signer and later to a verifier. This is the default for shared infrastructure because the credential and side-effect boundary is visible and can use a dedicated workload identity.
- Ruleset-provided runnable operation. A supported ruleset may offer an explicit
target invoked with
bazel runfor signing or attestation. Treat it as a release operation, not as proof that the signing key belongs in the action graph. Check the API's stability and credential path for the pinned ruleset version;rules_ocicurrently labels this surface as developer preview.2
The second shape can reduce integration code for one artifact ecosystem. It does not remove the need for separate identities, digest binding, negative tests, or an independent verification decision.
Test substitution, replay, and missing trust
A happy-path signature proves little about the system boundary. Start with one known candidate and preserve its digest, signature, signer identity, and verifier result. Then mutate one condition at a time:
| Test | Required outcome |
|---|---|
| Replace the artifact bytes but retain the filename or tag | Verification fails because the consumed digest no longer matches the signed subject. |
| Present a valid signature for another candidate | Verification fails the expected-subject check. |
| Present a valid signature from an untrusted identity | Verification rejects the signer even though the signature is mathematically valid. |
| Remove required trust evidence or make it unavailable to the verifier | Verification returns unknown, and promotion stops rather than silently accepting the artifact. |
| Replay an old signed candidate into a newer release context | Context or expected-candidate checks reject it, unless the workflow explicitly permits that reuse. |
| Reuse evidence from a signer whose authority was withdrawn | Current verifier policy rejects that signer and blocks new promotion. |
| Make signing unavailable after build eligibility | The candidate remains eligible and unsigned; no build result is rewritten. |
The first two cases distinguish byte identity from mutable naming. The third distinguishes cryptographic integrity from authorization. The replay case forces you to state whether a signature authorizes a subject everywhere, or only for a release channel, destination, or candidate transition. That is an organization policy choice, but the infrastructure must carry enough context to enforce the chosen answer.
The runnable
verifier contract tests
exercise accepted, rejected, and unknown across those mutations, including a
revoked signer. The
fixture verifier recomputes
the consumed digest and applies fixed-key or keyless policy. Its checked-in
signature_verified input deliberately stands in for a real cryptographic verifier:
the snippet proves the policy-state transitions and subject binding, not certificate
validation, key custody, transparency-log verification, or immutable evidence storage.
Bound a signer compromise
Withdrawing a compromised signer's authority stops future acceptance only after verifiers receive and enforce the changed trust state. The mechanism is system-specific: change the authorization or trust input that the verifier actually enforces, not merely an inventory entry beside it. That change does not retroactively repair artifacts, signatures, or attestations already produced under the compromised authority.1 Prepare a digest-indexed response:
- disable new signing and preserve signer, request, subject, and time evidence;
- enumerate every affected subject and release transition attributed to the signer;
- quarantine those subjects from new promotion while keeping unrelated builders and signers available;
- rebuild candidates through an independently trusted builder when build integrity is in doubt;
- sign the resulting digests with a replacement authority; and
- update verifier trust state and confirm that the compromised identity is rejected.
Do not automatically assume that signer compromise changed artifact bytes; signing and building are separate roles. But do not accept the old signature as evidence of authorization after its trust basis has been withdrawn. The retained subject and identity records determine which releases need investigation, rebuild, or re-signing.
Trace: A deployment job verifies a valid signature for digest A. It then
resolves the mutable tag production, which now points to digest B, and deploys
B. Which boundary failed?
Reveal
The verifier proved authorization for A, but the consumer never checked that the
bytes it promoted were A. The repair is not merely to verify the tag again. Resolve
the candidate to an immutable digest, verify that exact subject under the required
signer and context, and pass the same digest to publication and deployment.
Signing is a separate authority-bearing stage that consumes the immutable digest from an eligible Bazel-built candidate. Keep signing credentials outside cacheable actions, retain the signature or envelope and its scheme-specific verification material, and require downstream consumers to use the exact digest whose signature was accepted. A fixed-key policy names the expected key or trust anchor. A keyless policy requires the certificate identity and OIDC issuer under a configured trust root. A mutable tag or filename may locate a subject, but it cannot replace subject identity.
Prove the boundary with substitution, wrong-subject, untrusted-signer, missing-trust, and replay tests. If a signer is compromised, withdraw future authority, enumerate affected digests from retained evidence, rebuild where build integrity is uncertain, re-sign through an independent authority, and verify that the old identity is rejected.
Check your understanding · 4 questions
1.When may a verifier accept an older signature for the same artifact digest in a new release context?
Select one answer
2.Which controls keep signing separate from ordinary cacheable Bazel assembly?
Select all that apply
3.Match each verification or incident case to its required disposition:
Drag each answer onto the matching prompt, or click an answer and then click a prompt
4.Which verifier trust configuration correctly distinguishes fixed-key from keyless signing?
Select one answer
Footnotes
-
Building Secure and Reliable Systems — Ch. 14: Deploying Code — accepted-signer policy, artifact binding, signer-key protection, and privilege separation from user-defined build commands ↩1 ↩2 ↩3 ↩4
-
rules_oci — daemonless OCI image rules for Bazel — digest-pinned OCI inputs, the build-versus-registry boundary, and developer-preview Cosign signing and attestation examples ↩1 ↩2 ↩3
-
Distroless — minimal runtime images built with Bazel — published keyless signature-verification route and internal separation of image composition, signing, and pushing ↩1 ↩2