Skip to content

Duplicate .att / .sig layers when multiple type hints resolve to the same digest #1596

@anithapriyanatarajan

Description

@anithapriyanatarajan

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)

  1. 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
  1. 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
  1. 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

  • Kubernetes version:

    Output of kubectl version:

    Client Version: v1.29.15
    Kustomize Version: v5.0.4-0.20230601165947-6ce0bf390ce3
    Server Version: v1.35.0
    
  • Tekton Pipeline version:

    Output of tkn version:

    Client version: 0.43.0
    Chains version: v0.26.2
    Pipeline version: v1.10.0
    

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 #1575ARTIFACT_OUTPUTS-only Tasks produce no .sig (separate asymmetry)

Metadata

Metadata

Assignees

Labels

kind/bugCategorizes issue or PR as related to a bug.

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions