Skip to content

UID2-6764: Add SLSA build provenance attestations to docker publish workflows#228

Open
BehnamMozafari wants to merge 9 commits intomainfrom
bmz-UID2-6764-artifact-attestation
Open

UID2-6764: Add SLSA build provenance attestations to docker publish workflows#228
BehnamMozafari wants to merge 9 commits intomainfrom
bmz-UID2-6764-artifact-attestation

Conversation

@BehnamMozafari
Copy link
Copy Markdown
Contributor

@BehnamMozafari BehnamMozafari commented May 6, 2026

Summary

Adds SLSA build-provenance attestation to every non-snapshot image published by the shared docker workflows.

  • New composite action actions/attest_image wraps the full attest+verify path: it lowercases the image ref once, calls actions/attest@v4.1.0 (pinned to 59d8942), and immediately runs gh attestation verify against the just-pushed digest.
  • Both shared-publish-java-to-docker-versioned.yaml and actions/shared_publish_to_docker/action.yaml now call attest_image@v3 instead of inlining the attest block.
  • Publish jobs gain id-token: write and attestations: write.
  • Attestation is skipped on snapshot builds via the existing not_snapshot guard.
  • New manually-dispatchable smoke test workflow test-attest-image.yaml validates the composite action end-to-end without depending on a real consumer publish.

Closes UID2-6764. Spike was UID2-5763.

Review-comment responses

# Comment Resolution
1 Real smoke test without the attest step skipped New test-attest-image.yaml workflow exercises the full attest+verify path. Run 25542801315 succeeded — see "Smoke test evidence" below.
2 Add gh attestation verify step Built into attest_image so every release verifies in CI before any consumer pulls.
3 Case sensitivity of subject-name actions/attest@v4 already auto-lowercases subject-name when push-to-registry: true (verified in src/main.tsdowncaseName: inputs.pushToRegistry, applied at src/subject.ts line 47). However, gh attestation verify does not lowercase the OCI URI we pass it. To keep the signed name and the verify URI byte-identical, attest_image lowercases once at the top and reuses the value for both. (The smoke test caught a real case-sensitivity failure in the first run when ${{ github.repository }} evaluated to IABTechLab/... and docker push rejected it — proves the concern is real.)
4 Comment for NODE_OPTIONS Added inline in attest_image/action.yaml: Mirrors actions/attest-build-provenance, prevents oversized OCI registry auth-challenge headers triggering HPE_HEADER_OVERFLOW.
5 Extract duplication into a composite action Done — actions/attest_image/action.yaml is the single implementation.

Smoke test evidence

Run 25542801315test-attest-image.yaml on bmz-UID2-6764-artifact-attestation, all 9 steps green in 38s.

Test image: ghcr.io/iabtechlab/uid2-shared-actions/test-attest@sha256:e008cbdd1c67eee898020ad96d56ff0d42d762585ef4c1153479abaf5a4112bb

$ gh attestation verify \
    oci://ghcr.io/iabtechlab/uid2-shared-actions/test-attest@sha256:e008cbdd1c67eee898020ad96d56ff0d42d762585ef4c1153479abaf5a4112bb \
    --owner IABTechLab
# exit 0

Verified certificate identity:

{
  "ref":              "refs/heads/bmz-UID2-6764-artifact-attestation",
  "workflow":         "https://github.com/IABTechLab/uid2-shared-actions/.github/workflows/test-attest-image.yaml@refs/heads/bmz-UID2-6764-artifact-attestation",
  "builder":          "github-hosted",
  "predicateType":    "https://slsa.dev/provenance/v1"
}

The chain matches end to end: image digest → SLSA v1 provenance → workflow file at the exact ref → github-hosted runner identity.

Pre-merge cleanup (resolved)

  • The push: trigger on test-attest-image.yaml (added so the test could run before the workflow file exists on main) removed in commit 688a818. Workflow now only runs via workflow_dispatch (after merge to main, since the dispatch API requires the file on the default branch).

Test plan

  • Snapshot smoke test on IABTechLab/uid2-admin (Java path) — run 25421656856 — workflow succeeded, "Attest build provenance" step skipped (proves the not_snapshot guard works).
  • Real attest+verify smoke test — run 25542801315 — full attest+verify path green; external gh attestation verify succeeds.
  • Release-tag E2E on each consumer's first real publish after v3 float is promoted; verified digests will be recorded in the UID2-6764 ticket.

Caller-repo follow-up — already opened (one PR each, all open as of 2026-05-08)

Repo PR
IABTechLab/uid2-operator #2531
IABTechLab/uid2-core #403
IABTechLab/uid2-admin #632
IABTechLab/uid2-optout #402
UnifiedID2/uid2-snowflake #299
UnifiedID2/uid2-databricks #132

Each grants id-token: write + attestations: write (plus the implicit defaults the publish job already relied on). They're additive and harmless until this PR merges and v3 is promoted.

SDK images are explicitly out of scope; follow-up ticket to be filed separately.

BehnamMozafari and others added 5 commits May 6, 2026 13:46
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Attestation runs after the docker push but before the changelog/release
steps. Without continue-on-error, an attest failure leaves a half-finished
release: image pushed, no GitHub Release created. Tolerate attest failures
during the v3 rollout so consumers aren't stuck mid-release if attestation
breaks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jon8787
Copy link
Copy Markdown
Contributor

jon8787 commented May 8, 2026

Can we do a real smoke test without "Attest build provenance" step being skipped?
e.g. using uid2-test-source or similar?

with:
subject-name: ${{ inputs.docker_registry }}/${{ inputs.docker_image_name }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for both these attestations, is it possible to add a verification step that runs gh attestation verify?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — extracted into a new actions/attest_image composite action that runs both actions/attest@v4.1.0 and gh attestation verify against the just-pushed digest. Both shared workflows now call it. See PR description for the full smoke-test evidence (run 25542801315, external verify exit 0).

env:
NODE_OPTIONS: --max-http-header-size=32768
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}${{ inputs.append_image_name }}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have potential case sensitivity issue here? image name has previously been lowercased but subject name is not?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Real concern, partial fix needed. actions/attest@v4.1.0 already auto-lowercases subject-name when push-to-registry: true (src/main.ts passes downcaseName: inputs.pushToRegistry; src/subject.ts applies .toLowerCase()). However gh attestation verify does not lowercase the URI — so the new in-line verify step still needs a lowercased value. The new attest_image composite action lowercases once at the top and reuses for both signing and verifying. The smoke test's first run actually caught a real case-sensitivity failure (docker push rejected the mixed-case tag), so the concern was correct.

if: ${{ inputs.not_snapshot == 'true' }}
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
env:
NODE_OPTIONS: --max-http-header-size=32768
Copy link
Copy Markdown
Contributor

@jon8787 jon8787 May 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this (and the one above) still required? If yes, can we add a comment explaining why

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept and commented. The new actions/attest_image/action.yaml has: # Mirrors actions/attest-build-provenance, prevents oversized OCI registry auth-challenge headers triggering HPE_HEADER_OVERFLOW.

with:
subject-name: ${{ inputs.docker_registry }}/${{ inputs.docker_image_name }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth extracting this duplication into a small composite action?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — actions/attest_image/action.yaml is the single implementation. Both shared workflows call IABTechLab/uid2-shared-actions/actions/attest_image@v3 instead of inlining the attest block.

BehnamMozafari and others added 4 commits May 8, 2026 17:19
Addresses jon8787's review comments on PR #228:
- #2 verify step: attest_image now calls 'gh attestation verify' immediately
  after signing so misconfigured signatures fail at build time, not consumer
  pull time.
- #3 case sensitivity: lowercase the image ref once and reuse it for both
  signing and verifying. actions/attest@v4 already lowercases subject-name
  internally when push-to-registry is true (verified at the pinned commit
  59d8942 in src/main.ts and src/subject.ts), but 'gh attestation verify'
  does NOT lowercase the OCI URI we pass it; doing it ourselves keeps the
  signed name and the verified URI byte-identical.
- #4 NODE_OPTIONS comment: brief comment explaining why we mirror
  actions/attest-build-provenance's defensive HTTP header bump.
- #5 extract: pulled the attest+verify pair into a single composite action
  so the Java workflow and the non-Java composite action share one
  implementation.

Adds .github/workflows/test-attest-image.yaml: a manually-dispatched smoke
test that builds a throwaway image and exercises the full attest+verify
path. Use this whenever attest_image or actions/attest@v4 changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop after merge — only here so the smoke test can run before the workflow
file lands on main (gh workflow run / API dispatch require the file to
exist on the default branch).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
github.repository is mixed case; docker rejects mixed-case tags at push
time. Compute a lowercased ref once and reuse it for the push tag, the
attest_image input, and the independent re-verify command.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…st is green

Run 25542801315 verified the attest+verify path end-to-end. Reverting to
workflow_dispatch only so the test stops auto-firing and remains as an
on-demand regression check after merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants