Duplicate .att / .sig layers when multiple type hints resolve to the same digest
Expected Behavior
When a Task emits multiple type-hint results that all resolve to the same image digest,
Tekton Chains should produce exactly one attestation layer (.att) and exactly one
signature layer (.sig) for that digest.
Actual Behavior
Chains signs the same digest once per type-hint loop in ExtractOCIImagesFromResults,
with no deduplication. The duplicate varies by combination:
| Type hints emitted |
.att layers |
.sig layers |
IMAGE_URL + IMAGE_DIGEST + ARTIFACT_OUTPUTS |
2 ❌ |
1 ✅ |
IMAGE_URL + IMAGE_DIGEST + IMAGES |
1 ✅ |
2 ❌ |
ARTIFACT_OUTPUTS only |
1 ✅ |
no .sig ⚠️ |
- The two
.att layers contain identical payloads — the same attestation stored twice.
- The two
.sig layers contain identical payloads but different signatures — the image
was independently signed twice.
ARTIFACT_OUTPUTS-only Tasks produce no .sig at all (tracked separately in
#1575).
Steps to Reproduce the Problem
Scenario A — duplicate .att (IMAGE_URL/IMAGE_DIGEST + ARTIFACT_OUTPUTS)
- Apply this Task:
apiVersion: tekton.dev/v1
kind: Task
metadata:
name: kaniko-chains-all-type-hints
spec:
params:
- name: IMAGE
- name: TAG
default: all-type-hints
- name: BUILDER_IMAGE
default: gcr.io/kaniko-project/executor:v1.5.1@sha256:c6166717f7fe0b7da44908c986137ecfeab21f31ec3992f6e128fff8a94be8a5
workspaces:
- name: source
- name: dockerconfig
optional: true
mountPath: /kaniko/.docker
results:
- name: IMAGE_DIGEST
- name: IMAGE_URL
- name: build-ARTIFACT_OUTPUTS
type: object
properties: {uri: {}, digest: {}, isBuildArtifact: {}}
steps:
- name: add-dockerfile
workingDir: $(workspaces.source.path)
image: bash
script: |
echo "FROM alpine@sha256:69e70a79f2d41ab5d637de98c1e0b055206ba40a8145e7bddb55ccc04e13cf8f" | tee ./Dockerfile
- name: build-and-push
workingDir: $(workspaces.source.path)
image: $(params.BUILDER_IMAGE)
args:
- --dockerfile=./Dockerfile
- --context=$(workspaces.source.path)/
- --destination=$(params.IMAGE):$(params.TAG)
- --digest-file=$(results.IMAGE_DIGEST.path)
securityContext:
runAsUser: 0
- name: write-url
image: bash
script: echo "$(params.IMAGE):$(params.TAG)" | tee $(results.IMAGE_URL.path)
securityContext:
runAsUser: 0
- name: write-artifact-outputs
image: bash
script: |
DIGEST=$(cat $(results.IMAGE_DIGEST.path))
printf '{"uri":"%s","digest":"%s","isBuildArtifact":"true"}' \
"$(params.IMAGE):$(params.TAG)" "$DIGEST" \
| tee $(results.build-ARTIFACT_OUTPUTS.path)
securityContext:
runAsUser: 0
- Run it and wait for Chains to sign (~10 s):
kubectl create -f - <<EOF
apiVersion: tekton.dev/v1
kind: TaskRun
metadata:
generateName: kaniko-run-all-type-hints-
spec:
taskRef:
name: kaniko-chains-all-type-hints
params:
- name: IMAGE
value: <your-registry>/<your-repo>
- name: TAG
value: all-type-hints
workspaces:
- name: source
emptyDir: {}
- name: dockerconfig
secret:
secretName: <your-pull-secret>
EOF
kubectl get taskrun -w
- Check the
.att manifest:
TASKRUN=<taskrun-name>
DIGEST=$(kubectl get taskrun $TASKRUN \
-o jsonpath='{.status.results[?(@.name=="IMAGE_DIGEST")].value}')
ATT_TAG="${DIGEST/sha256:/sha256-}.att"
oras manifest fetch "<your-registry>/<your-repo>:${ATT_TAG}" | jq '.layers | length'
# Expected: 1 Actual: 2
oras manifest fetch "<your-registry>/<your-repo>:${ATT_TAG}" | jq '[.layers[].digest]'
# Both digests are identical — same attestation stored twice
Scenario B — duplicate .sig (IMAGE_URL/IMAGE_DIGEST + IMAGES)
Same as above but replace the build-ARTIFACT_OUTPUTS result with an IMAGES result and
add a step that writes repo:tag@digest to it:
results:
- name: IMAGE_DIGEST
- name: IMAGE_URL
- name: IMAGES
- name: write-images
image: bash
script: |
DIGEST=$(cat $(results.IMAGE_DIGEST.path))
echo "$(params.IMAGE):$(params.TAG)@${DIGEST}" | tee $(results.IMAGES.path)
securityContext:
runAsUser: 0
Then check .sig:
SIG_TAG="${DIGEST/sha256:/sha256-}.sig"
oras manifest fetch "<your-registry>/<your-repo>:${SIG_TAG}" | jq '.layers | length'
# Expected: 1 Actual: 2
oras manifest fetch "<your-registry>/<your-repo>:${SIG_TAG}" \
| jq '{payloads: [.layers[].digest], signatures: [.layers[].annotations."dev.cosignproject.cosign/signature"]}'
# payloads → identical (same content signed twice)
# signatures → different (two independent signing calls)
Additional Info
Root Cause
ExtractOCIImagesFromResults in pkg/artifacts/signable.go has independent loops for
each type-hint format with no deduplication. Each loop that resolves to the same digest
appends it to the objs slice again, driving a separate Store call per entry:
// Loop 1 — IMAGE_URL / IMAGE_DIGEST
objs = append(objs, dgst)
// Loop 2 — IMAGES
objs = append(objs, dgst) // no dedup check
// Loop 3 — ARTIFACT_OUTPUTS
objs = append(objs, dgst) // no dedup check
Fix: deduplicate by dgst.String() before appending.
Related
- Issue #1575 —
ARTIFACT_OUTPUTS-only Tasks produce no .sig (separate asymmetry)
Duplicate
.att/.siglayers when multiple type hints resolve to the same digestExpected Behavior
When a Task emits multiple type-hint results that all resolve to the same image digest,
Tekton Chains should produce exactly one attestation layer (
.att) and exactly onesignature layer (
.sig) for that digest.Actual Behavior
Chains signs the same digest once per type-hint loop in
ExtractOCIImagesFromResults,with no deduplication. The duplicate varies by combination:
.attlayers.siglayersIMAGE_URL+IMAGE_DIGEST+ARTIFACT_OUTPUTSIMAGE_URL+IMAGE_DIGEST+IMAGESARTIFACT_OUTPUTSonly.sig.attlayers contain identical payloads — the same attestation stored twice..siglayers contain identical payloads but different signatures — the imagewas independently signed twice.
ARTIFACT_OUTPUTS-only Tasks produce no.sigat all (tracked separately in#1575).
Steps to Reproduce the Problem
Scenario A — duplicate
.att(IMAGE_URL/IMAGE_DIGEST+ARTIFACT_OUTPUTS).attmanifest:Scenario B — duplicate
.sig(IMAGE_URL/IMAGE_DIGEST+IMAGES)Same as above but replace the
build-ARTIFACT_OUTPUTSresult with anIMAGESresult andadd a step that writes
repo:tag@digestto it:Then check
.sig:Additional Info
Kubernetes version:
Output of
kubectl version:Tekton Pipeline version:
Output of
tkn version:Root Cause
ExtractOCIImagesFromResultsinpkg/artifacts/signable.gohas independent loops foreach type-hint format with no deduplication. Each loop that resolves to the same digest
appends it to the
objsslice again, driving a separateStorecall per entry:Fix: deduplicate by
dgst.String()before appending.Related
ARTIFACT_OUTPUTS-only Tasks produce no.sig(separate asymmetry)