Build provenance and attestations
Build provenance is a signed, verifiable statement of what produced an artifact — the source repository, commit, ref, workflow path, and builder that ran. When a workflow step attests an artifact, KiCI records that statement, signs it, and makes it retrievable so anyone can later prove the artifact came from a specific KiCI run and was not swapped along the way.
This is the same idea behind supply-chain attestation systems like
SLSA: a downstream consumer (a release
gate, a security audit, a "show me the provenance" request) can verify the
artifact’s origin without trusting the person who handed it over.
What an attestation contains
Section titled “What an attestation contains”An attestation is a self-contained bundle holding three things:
- An in-toto SLSA v1.0 statement describing the build: the subject artifact (name + content digest) and the provenance predicate (source repository, commit, ref, workflow, run/job identifiers, timestamps).
- A DSSE signature over that statement, made with an ephemeral signing key generated for the run.
- A short-lived OIDC identity token issued by your orchestrator that
binds the signature to the build identity. The token’s identity claims
(
repository,ref,sha, run/job ids) are derived by the orchestrator from the run itself — a step cannot forge them.
The orchestrator owns the provenance root of trust: it holds its own long-lived ES256 signing key, mints and signs the identity token locally from its own run records, and publishes its own OIDC discovery + public key set (JWKS). Builds therefore produce verifiable provenance with no dependency on the hosted KiCI platform — the availability, sovereignty, and air-gap story all follow from this.
Because the bundle carries the identity token and the public signing key, it is offline-verifiable: a verifier checks it against the orchestrator’s published signing keys with no per-attestation online lookup.
Attesting an artifact in a workflow
Section titled “Attesting an artifact in a workflow”Call ctx.attestProvenance({ subject }) from a step after you have produced the
artifact:
import { workflow, job, step } from '@kici-dev/sdk';
export default workflow('release', { on: { push: { branches: ['main'] } }, jobs: [ job('publish', { steps: [ step('build', async (ctx) => { await ctx.$`npm pack`; }), step('attest', async (ctx) => { const result = await ctx.attestProvenance({ subject: { name: 'my-pkg-1.2.3.tgz', path: 'my-pkg-1.2.3.tgz' }, }); ctx.log.info(`Attestation stored at ${result.storageKey}`); }), ], }), ],});The subject is caller-supplied — you name the artifact and give KiCI either a path or a precomputed digest:
-
{ name, path }— a path relative to the step working directory. KiCI reads the file and computes its SHA-256 digest. -
{ name, digest }— a precomputed digest. For a container image, pass the OCI manifest digest your build tool emitted:await ctx.attestProvenance({subject: { name: 'ghcr.io/acme/app', digest: { sha256: '<manifest-digest>' } },});
The identity token is fetched and masked in logs automatically — you never
handle it. The call returns { storageKey, subjectDigest, bundleMediaType }
identifying the stored bundle.
ctx.attestProvenance is only available inside a running job step; calling it
outside one rejects with a clear error. kici run --local runs are supported:
the offline local dev plane signs with a dev identity under the
clearly-non-production issuer kici-local, and those bundles verify against a
trust root exported with kici local trust-root.
Requesting a raw identity token
Section titled “Requesting a raw identity token”ctx.attestProvenance builds on a lower-level primitive you can call directly
when you need the identity token for a different tool:
step('mint', async (ctx) => { const { token, expiresIn } = await ctx.kici.oidc.token({ audience: 'sigstore' }); ctx.log.info(`Got an ID token valid for ${expiresIn}s`); // Hand `token` to a tool that exchanges it with a service trusting the issuer.});The token is a short-lived (about 10 minutes) signed JWT scoped to the current
run and job. Its identity claims (repository, ref, sha, kici_run_id,
kici_job_id) are derived by the orchestrator from the run context, so a step
cannot spoof them. The returned token value is automatically masked in step logs,
and the step never holds signing credentials — the orchestrator mints and signs
the token on the step’s behalf from its own run records. Like attestProvenance,
it is only available inside a running job step.
Verifying an attestation
Section titled “Verifying an attestation”Verify a bundle with the kici verify-attestation command. It establishes the
full chain offline: the identity token verifies against the trusted issuer’s
JWKS, the DSSE signature verifies against the bundled signing key, and the
statement’s build context must match the token’s identity claims (a mismatch is
a hard failure).
kici verify-attestation [artifact] --bundle <path-or-url> [--trust-root <url-or-file>]Which trust root do I use?
Section titled “Which trust root do I use?”The trust root is your orchestrator’s provenance issuer — the orchestrator
you kici login against, which owns the provenance signing key and publishes its
own JWKS. That is the default: omit --trust-root and the verifier checks the
bundle against your configured orchestrator automatically. There are three ways to
verify, and offline is always the primary one:
- Offline against a JWKS / trust-root file (air-gap) — export the
{ issuer, jwks }file once withkici-admin signing-key export --publicand verify against it with--trust-root <file>. No network needed at verify time. - Directly online against your orchestrator — the default: the verifier
resolves your orchestrator’s discovery → JWKS. You can also POST a bundle to
the orchestrator’s native
POST /v1/verify-attestationendpoint for a verdict against its live keys (fresh rotations / revocations included). - Against the hosted KiCI platform — bundles produced before your orchestrator owned signing were signed by the hosted platform; those keep verifying forever. When no orchestrator is configured, the default falls back to the hosted platform’s issuer so those historical bundles still verify with no flag.
You pass --trust-root to verify against a different environment or, most
commonly, an offline { issuer, jwks } file for air-gapped checks.
Why you supply it out-of-band
Section titled “Why you supply it out-of-band”Given there’s a single issuer, why pass it at all instead of letting the
verifier read it from the token? Because the issuer named inside a token
cannot be trusted: a forged bundle could carry a token that names
iss: https://attacker.example and bundle a key set that “verifies” it,
making the whole signature chain circular and self-attesting. The verifier
therefore pins to an issuer you supply out-of-band and checks the token against
that — the bundle is verified against a key set you trust, not one it shipped
with. Naming the trust root is a security requirement, not a multiple-choice
question.
To override the default, supply the trusted issuer via --trust-root, in one of
two forms:
-
Online — an HTTPS issuer URL. The verifier fetches
<url>/.well-known/openid-configuration, reads itsissuerandjwks_uri, and fetches the JWKS. The token’sissis pinned to the discovery document’sissuer. -
Offline — a self-contained trust-root file. A local JSON file with the issuer and JWKS inlined, for air-gapped verification:
{"issuer": "https://platform.example/issuer","jwks": {"keys": [{ "kty": "EC", "crv": "P-256", "x": "...", "y": "...", "alg": "ES256", "kid": "..." }]}}
Pass an optional [artifact] to also digest-check the file against the
attestation subject — this is what binds the attestation to a specific set of
bytes. Omit it to verify the signatures and identity only. Use --json for a
machine-readable result. The command exits 0 when everything verifies and 1
when it does not (or on an error such as a missing flag or unreachable trust
root).
# Default: verify against your configured orchestrator (no --trust-root needed):kici verify-attestation ./dist/app.tgz --bundle ./app.tgz.kici.json
# Override the trust root to verify against a specific issuer:kici verify-attestation ./dist/app.tgz \ --bundle ./app.tgz.kici.json \ --trust-root https://platform.example/issuer
# Air-gapped: verify against a self-contained trust-root file:kici verify-attestation ./dist/app.tgz \ --bundle ./app.tgz.kici.json \ --trust-root ./kici-trust-root.jsonThe full flag reference is in the CLI reference.
Viewing attestations in the dashboard
Section titled “Viewing attestations in the dashboard”The run detail page has an Attestations tab listing each artifact a run’s
steps attested (via ctx.attestProvenance), one row per artifact.
Each row shows:
- Status — a verified badge computed in your browser. It checks the attestation’s signature, the build identity, and the build context against the trusted provenance issuer. verified (green) means all of those pass; failed (red) shows why in a tooltip; unverifiable means the provenance issuer is not configured; keys unavailable (amber) means the issuer is configured but its verification keys could not be fetched.
- Job / Artifact / Digest / Created — the producing job, the artifact name, its content digest, and when it was recorded.
- Download — saves the signed bundle as a
.sigstore.jsonfile.
The badge does not re-hash the artifact bytes — the dashboard does not have
the artifact. To bind the attestation to a specific file, run
kici verify-attestation <artifact> --bundle <bundle>. A run with no
attestations shows an empty state.
Browsing attestations across runs
Section titled “Browsing attestations across runs”The Attestations page (in the org sidebar) lists every build-provenance
attestation your organization has produced — not just one run’s. It is the
supply-chain audit surface: look up “who built sha256:…?” by digest, or browse
and filter every attestation across all runs.
The Attestations page lists every build-provenance attestation your organization has produced.
- Search by artifact digest (exact
sha256:…) or name to trace a specific artifact. - Filter by verification status, repository, workflow, job, or date.
- Each row’s status badge is the verdict KiCI recorded when the attestation was produced (
verified,failed,unverifiable, orpending). - Retry on a
pendingrow asks your orchestrator to mint that run’s outstanding attestations now; Retry pending does the same for every pending run at once.
Open a row for the parsed provenance statement and a live re-verification.
The status badge here is the server-side verdict, computed once when the
attestation was recorded (verify-at-ingest) — so the list stays fast at any
size. verified means the signature, build identity, and build context all
checked out against the provenance issuer; failed means verification ran and
the bundle did not pass; unverifiable means no verdict could be computed (no
provenance issuer configured, or its keys could not be read — not a forgery
signal); pending means the verdict has not been computed yet.
A pending row is one still waiting to be minted — the attestation was signed
at build time, but attaching its identity token has not completed yet. Those
rows carry a Retry button that asks your orchestrator to mint that run’s
outstanding attestations immediately, and the page header offers Retry
pending to do the same across every pending run. Only one retry runs at a
time — the other retry buttons are unavailable until it finishes.
Retrying is safe to repeat while the mint is only temporarily unavailable: the
row stays pending and the next retry tries again. A mint that is definitively
rejected — for example the run’s records are no longer there to bind the
attestation to — is terminal: the row stops being retried, and re-arming it is
an operator action (kici-admin attestations retry --include-rejected).
Opening a row leads to the attestation detail page:
This page shows the parsed provenance for one attestation.
- Builder identity, source, and build type come from the signed SLSA statement.
- The stored badge is the verdict recorded at build time; Re-verify runs the check live in your browser against the current signing keys.
- Download exports the signed bundle for offline verification with
kici verify-attestation.
See also
Section titled “See also”- SDK runtime reference — the
ctx.attestProvenanceandctx.kici.oidc.tokenstep APIs in full. - CLI reference — every
kici verify-attestationflag and exit code.