CI/lock and cosign
Cosign is the Sigstore project's CLI for signing container images, blobs, and SLSA / in-toto attestations. CI/lock and cosign share the wire format — both produce and consume DSSE-wrapped in-toto Statements — and they sit at different abstraction levels in a supply chain. They are complementary, not competitive.
The user story below is validated end-to-end against cosign v3.0.2 in the interop-cosign-dsse example.
Different abstraction levels
| Layer | Cosign | Cilock |
|---|---|---|
| What it signs | An artifact's digest (blob, OCI image, SBOM file) | A pipeline step's full evidence — argv, materials, products, environment, embedded findings |
| Predicate body | Whatever you pass to --predicate (often SLSA Provenance or a custom JSON) | A Collection predicate that bundles every per-attestor record from the step |
| Subject in the Statement | The artifact digest | Merkle roots over the step's materials and products trees |
| Evidence model | One signed envelope per artifact | One signed envelope per pipeline step, structured by attestor |
| Verification surface | "Is this signature valid? Is the signer in Rekor? Does the cert SAN match?" | "Did every step the policy requires happen, signed by the right functionary, with the right embedded evidence — and any required external attestations from upstream?" |
| Native verification language | cosign verify-blob / cosign verify-attestation per file | A Rego-evaluated policy spanning multiple steps + externals, gating release |
Put plainly:
- Cosign answers "is this artifact's signature trustworthy?"
- Cilock answers "is the pipeline that produced this artifact trustworthy under my policy?"
Both questions are valid. Pipelines that already use cosign for image signing can keep doing exactly that, then use cilock policy to verify the cosign attestation as one input alongside cilock's per-step evidence.
Shared wire format
Both tools emit and consume DSSE envelopes carrying in-toto Statements. The relevant primitives:
| Spec | What it standardises |
|---|---|
| DSSE | The signed envelope: { "payloadType", "payload", "signatures": [...] } |
| in-toto Statement v0.1 / v1 | The payload shape: { "_type", "predicateType", "subject", "predicate" } |
| SLSA Provenance v0.2 / v1 | A common predicate type both tools handle natively |
| VSA | A Verification Summary Attestation cilock also emits via cilock verify --vsa-outfile |
Because the wire format is shared, an envelope cosign produces is structurally indistinguishable from one cilock produces in cases where the predicate type matches. The signature is verified by ECDSA / ED25519 / RSA the same way on both sides.
The user story: cilock verifies a cosign attestation
Scenario. A pipeline uses cosign to sign release artifacts (a long-established practice). The release-gate team wants to add a richer policy that requires the cosign-signed provenance plus cilock per-step evidence from the same build. They don't want to throw out the cosign signing flow.
Solution. Add the cosign predicate type to the cilock policy as an externalAttestations entry. Cilock will verify the cosign signature using the embedded public key and treat the cosign envelope as required evidence.
Step 1: cosign produces the attestation (no change to the existing flow)
cosign attest-blob \
--key cosign.key \
--predicate predicate.json \
--type slsaprovenance \
--new-bundle-format=false \
--use-signing-config=false \
--yes \
hello > cosign-dsse.jsonThe result is a classic DSSE envelope (payloadType=application/vnd.in-toto+json) carrying an SLSA Provenance v0.2 predicate over hello.
--new-bundle-format=falseCosign v3 defaults to the new bundle format which wraps the DSSE envelope in additional verification material. Cilock policy currently ingests classic DSSE envelopes directly. Both forms carry the same in-toto Statement under the signature; the classic form is what you want to feed cilock today.
Step 2: cilock attests the build step (in addition)
cilock run --step build \
--signer-file-key-path cilock-key.pem \
--outfile cilock-attestation.json \
--attestations environment \
-- go build -o hello hello.goThis produces a cilock collection envelope. The pipeline keeps both the cosign envelope and the cilock collection envelope as release artifacts.
Step 3: write a policy that requires both
{
"expires": "2030-01-01T00:00:00Z",
"publickeys": {
"<cilock-keyid>": { "key": "..." },
"<cosign-keyid>": { "key": "..." }
},
"steps": {
"build": {
"name": "build",
"functionaries": [{ "publickeyid": "<cilock-keyid>" }],
"attestations": [
{ "type": "https://aflock.ai/attestations/material/v0.3" },
{ "type": "https://aflock.ai/attestations/product/v0.3" },
{ "type": "https://aflock.ai/attestations/command-run/v0.2" }
]
}
},
"externalAttestations": {
"slsa-provenance-from-cosign": {
"name": "slsa-provenance-from-cosign",
"predicateType": "https://slsa.dev/provenance/v0.2",
"required": true,
"functionaries": [{ "publickeyid": "<cosign-keyid>" }]
}
}
}Two keys in publickeys: one trusts cilock-signed envelopes (the build step), one trusts cosign-signed envelopes (the external SLSA Provenance). Both are functionaries on their respective evidence requirements.
The <cosign-keyid> is the hex-encoded SHA-256 of the public key re-encoded as PKIX PEM. cilock derives every key ID this way. It parses the key and hashes its own encoding, not the file's bytes, so sha256sum cosign.pub only agrees when the file happens to be byte-identical to that encoding. cilock keyid show prints the ID exactly as cilock verify computes it. Check the result before you put it in a policy, because an empty ID trusts no key:
COSIGN_KEYID=$(cilock keyid show cosign.pub | awk '{print $1}')
if printf '%s' "$COSIGN_KEYID" | grep -Eq '^[0-9a-f]{64}