Skip to content

Commit f0eb70d

Browse files
Isolate signing step by moving to its own job
1 parent feb85b2 commit f0eb70d

File tree

1 file changed

+193
-86
lines changed

1 file changed

+193
-86
lines changed

.github/workflows/build.yml

Lines changed: 193 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ on:
2020
- '' # without this, it's technically "required" 🙃
2121
- 2022
2222
- 2019
23+
pruneArtifact:
24+
required: true
25+
type: choice
26+
options:
27+
- "true"
28+
- "false"
29+
default: "true"
2330
run-name: '${{ inputs.bashbrewArch }}: ${{ inputs.firstTag }} (${{ inputs.buildId }})'
2431
permissions:
2532
contents: read
@@ -45,6 +52,9 @@ jobs:
4552
name: Build ${{ inputs.buildId }}
4653
outputs:
4754
shouldSign: ${{ steps.json.outputs.shouldSign }}
55+
buildJson: ${{ steps.json.outputs.json }}
56+
artifactId: ${{ steps.oci.outputs.artifact-id }}
57+
sha256: ${{ steps.checksum.outputs.sha256 }}
4858
runs-on: ${{ inputs.bashbrewArch == 'windows-amd64' && format('windows-{0}', inputs.windowsVersion) || 'ubuntu-latest' }}
4959
steps:
5060

@@ -158,136 +168,233 @@ jobs:
158168
fi
159169
eval "$shell"
160170
161-
# TODO signing prototype (see above where "shouldSign" is populated)
171+
- name: Generate Checksum
172+
id: checksum
173+
run: |
174+
cd build
175+
tar -cvf temp.tar -C temp .
176+
echo "sha256=$(sha256sum temp.tar)" >> "$GITHUB_OUTPUT"
177+
- name: Stage artifact
178+
id: oci
179+
uses: actions/upload-artifact@v4
180+
with:
181+
name: build-oci
182+
path: |
183+
build/temp.tar*
184+
retention-days: 5
185+
186+
sign:
187+
name: Sign
188+
needs: build
189+
if: ${{ needs.build.outputs.shouldSign == 'true' }}
190+
runs-on: ubuntu-latest
191+
permissions:
192+
contents: read
193+
actions: write # for https://github.com/andymckay/cancel-action (see usage below)
194+
id-token: write # for AWS KMS signing (see usage below)
195+
steps:
196+
- uses: actions/checkout@v4
197+
with:
198+
sparse-checkout-cone-mode: 'false'
199+
sparse-checkout: |
200+
.scripts/oci.jq
201+
.scripts/provenance.jq
202+
- name: Download a single artifact
203+
uses: actions/download-artifact@v4
204+
with:
205+
name: build-oci
206+
- name: Verify artifact
207+
run: |
208+
echo "${{ needs.build.outputs.sha256 }}" sha256sum -c
209+
mkdir -p temp
210+
tar -xvf temp.tar -C temp
162211
- name: Configure AWS (for signing)
163-
if: steps.json.outputs.shouldSign == 'true'
164212
# https://github.com/aws-actions/configure-aws-credentials/releases
165213
uses: aws-actions/configure-aws-credentials@010d0da01d0b5a38af31e9c3470dbfdabdecca3a # v4.0.1
166214
with:
167-
# TODO drop "subset" (github.ref_name == "main" && ... || ...)
168-
aws-region: ${{ contains(fromJSON('["main","subset"]'), github.ref_name) && secrets.AWS_KMS_PROD_REGION || secrets.AWS_KMS_STAGE_REGION }}
169-
role-to-assume: ${{ contains(fromJSON('["main","subset"]'), github.ref_name) && secrets.AWS_KMS_PROD_ROLE_ARN || secrets.AWS_KMS_STAGE_ROLE_ARN }}
170-
# TODO figure out if there's some way we could make our secrets ternaries here more DRY without major headaches 🙈
215+
aws-region: ${{ secrets.AWS_KMS_REGION }}
216+
role-to-assume: ${{ secrets.AWS_KMS_ROLE_ARN }}
217+
- name: Generate Provenance
218+
env:
219+
buildJson: ${{ needs.build.outputs.buildJson }}
220+
GITHUB_CONTEXT: ${{ toJson(github) }}
221+
run: |
222+
image-digest() {
223+
local dir="$1/blobs"
224+
img=$(
225+
grep -R --include "*" '"mediaType":\s"application/vnd.oci.image.layer.' "$dir" \
226+
| head -n 1 \
227+
| cut -d ':' -f1
228+
)
229+
[ "$(cat $img | jq -r '.mediaType')" = "application/vnd.oci.image.manifest.v1+json" ] || exit 1
230+
echo $img | rev | cut -d '/' -f2,1 --output-delimiter ':' | rev
231+
}
232+
233+
digest=$(image-digest temp)
234+
235+
echo $buildJson | jq -L.scripts --argjson github '${{ env.GITHUB_CONTEXT }}' --argjson runner '${{ toJson(runner) }}' --arg digest ${digest} '
236+
include "provenance";
237+
github_actions_provenance($github; $runner; $digest)
238+
' >> provenance.json
171239
- name: Sign
172-
if: steps.json.outputs.shouldSign == 'true'
173240
env:
174-
AWS_KMS_REGION: ${{ contains(fromJSON('["main","subset"]'), github.ref_name) && secrets.AWS_KMS_PROD_REGION || secrets.AWS_KMS_STAGE_REGION }}
175-
AWS_KMS_KEY_ARN: ${{ contains(fromJSON('["main","subset"]'), github.ref_name) && secrets.AWS_KMS_PROD_KEY_ARN || secrets.AWS_KMS_STAGE_KEY_ARN }}
241+
AWS_KMS_REGION: ${{ secrets.AWS_KMS_REGION }}
242+
AWS_KMS_KEY_ARN: ${{ secrets.AWS_KMS_KEY_ARN }}
243+
244+
DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
245+
DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }}
176246
run: |
177-
cd build
247+
validate-oci-layout() {
248+
local dir="$1"
249+
jq -L.scripts -s '
250+
include "oci";
251+
validate_oci_layout | true
252+
' "$dir/oci-layout" "$dir/index.json" || return "$?"
253+
local manifest
254+
manifest="$dir/blobs/$(jq -r '.manifests[0].digest | sub(":"; "/")' "$dir/index.json")" || return "$?"
255+
jq -L.scripts -s '
256+
include "oci";
257+
if length != 1 then
258+
error("unexpected image index document count: \(length)")
259+
else .[0] end
260+
| validate_oci_index
178261
179-
args=(
262+
# TODO more validation?
263+
' "$manifest" || return "$?"
264+
}
265+
266+
dockerArgs=(
180267
--interactive
181268
--rm
182269
--read-only
183270
--workdir /tmp # see "--tmpfs" below (TODO the signer currently uses PWD as TMPDIR -- something to fix in the future so we can drop this --workdir and only keep --tmpfs perhaps adding --env TMPDIR=/tmp if necessary)
184271
)
185272
if [ -t 0 ] && [ -t 1 ]; then
186-
args+=( --tty )
273+
dockerArgs+=( --tty )
187274
fi
188275
189276
user="$(id -u)"
190-
args+=( --tmpfs "/tmp:uid=$user" )
277+
dockerArgs+=( --tmpfs "/tmp:uid=$user" )
191278
user+=":$(id -g)"
192-
args+=( --user "$user" )
279+
dockerArgs+=( --user "$user" )
193280
194281
awsEnvs=( "${!AWS_@}" )
195-
args+=( "${awsEnvs[@]/#/--env=}" )
196-
197-
# some very light assumption verification (see TODO in --mount below)
198-
validate-oci-layout() {
199-
local dir="$1"
200-
jq -s '
201-
if length != 1 then
202-
error("unexpected 'oci-layout' document count: " + length)
203-
else .[0] end
204-
| if .imageLayoutVersion != "1.0.0" then
205-
error("unsupported imageLayoutVersion: " + .imageLayoutVersion)
206-
else . end
207-
' "$dir/oci-layout" || return "$?"
208-
jq -s '
209-
if length != 1 then
210-
error("unexpected 'index.json' document count: " + length)
211-
else .[0] end
282+
dockerArgs+=( "${awsEnvs[@]/#/--env=}" )
212283
213-
| if .schemaVersion != 2 then
214-
error("unsupported schemaVersion: " + .schemaVersion)
215-
else . end
216-
| if .mediaType != "application/vnd.oci.image.index.v1+json" and .mediaType then # TODO drop the second half of this validation: https://github.com/moby/buildkit/issues/4595
217-
error("unsupported index mediaType: " + .mediaType)
218-
else . end
219-
| if .manifests | length != 1 then
220-
error("expected only one manifests entry, not " + (.manifests | length))
221-
else . end
222-
223-
| .manifests[0] |= (
224-
if .mediaType != "application/vnd.oci.image.index.v1+json" then
225-
error("unsupported descriptor mediaType: " + .mediaType)
226-
else . end
227-
# TODO validate .digest somehow (`crane validate`?) - would also be good to validate all descriptors recursively
228-
| if .size < 0 then
229-
error("invalid descriptor size: " + .size)
230-
else . end
231-
)
232-
' "$dir/index.json" || return "$?"
233-
local manifest
234-
manifest="$dir/blobs/$(jq -r '.manifests[0].digest | sub(":"; "/")' "$dir/index.json")" || return "$?"
235-
jq -s '
236-
if length != 1 then
237-
error("unexpected image index document count: " + length)
238-
else .[0] end
239-
| if .schemaVersion != 2 then
240-
error("unsupported schemaVersion: " + .schemaVersion)
241-
else . end
242-
| if .mediaType != "application/vnd.oci.image.index.v1+json" then
243-
error("unsupported image index mediaType: " + .mediaType)
244-
else . end
245-
246-
# TODO more validation?
247-
' "$manifest" || return "$?"
248-
}
249284
validate-oci-layout temp
250285
251-
mkdir signed
286+
# Login to Docker Hub
287+
export DOCKER_CONFIG="$PWD/.docker"
288+
mkdir "$DOCKER_CONFIG"
289+
trap 'find "$DOCKER_CONFIG" -type f -exec shred -fuvz "{}" + || :; rm -rf "$DOCKER_CONFIG"' EXIT
290+
docker login --username "$DOCKER_HUB_USERNAME" --password-stdin <<<"$DOCKER_HUB_PASSWORD"
291+
unset DOCKER_HUB_USERNAME DOCKER_HUB_PASSWORD
252292
253-
args+=(
254-
--mount "type=bind,src=$PWD/temp,dst=/doi-build/unsigned" # TODO this currently assumes normalized_builder == "buildkit" and !should_use_docker_buildx_driver -- we need to factor that in later (although this signs the attestations, not the image, so buildkit/buildx is the only builder whose output we *can* sign right now)
255-
--mount "type=bind,src=$PWD/signed,dst=/doi-build/signed"
293+
# Create signatures
294+
dockerArgs+=(
295+
--mount "type=bind,src=$PWD/temp,dst=/doi-build/image" # TODO this currently assumes normalized_builder == "buildkit" and !should_use_docker_buildx_driver -- we need to factor that in later (although this signs the attestations, not the image, so buildkit/buildx is the only builder whose output we *can* sign right now)
296+
--mount "type=bind,src=$PWD/provenance.json,dst=/doi-build/provenance.json"
297+
--mount "type=bind,src=$PWD/.docker,dst=/.docker"
256298
257299
# https://explore.ggcr.dev/?repo=docker/image-signer-verifier
258-
docker/image-signer-verifier:0.3.3@sha256:a5351e6495596429bacea85fbf8f41a77ce7237c26c74fd7c3b94c3e6d409c82
259-
260-
sign
300+
"$IMAGE_SIGNER"
301+
)
261302
262-
--envelope-style oci-content-descriptor
303+
kmsArg=(
304+
# kms key used to sign attestation artifacts
305+
--kms="AWS"
306+
--kms-region="$AWS_KMS_REGION"
307+
--kms-key-ref="$AWS_KMS_KEY_ARN"
263308
264-
--aws_region "$AWS_KMS_REGION"
265-
--aws_arn "awskms:///$AWS_KMS_KEY_ARN"
309+
--referrers-dest="$REFERRERS_REPO" # repo to store attestation artifacts and provenance
310+
)
266311
267-
--input oci:///doi-build/unsigned
268-
--output oci:///doi-build/signed
312+
# Sign buildkit statements
313+
signArgs=(
314+
${kmsArg[@]}
315+
--input=oci:///doi-build/image
316+
--keep=true # keep preserves the unsigned attestations generated by buildkit
269317
)
270318
271-
docker run "${args[@]}"
319+
docker run "${dockerArgs[@]}" sign "${signArgs[@]}"
272320
273-
validate-oci-layout signed
321+
# Attach and sign provenance
322+
provArgs=(
323+
${kmsArg[@]}
324+
--image=oci:///doi-build/image
325+
--statement="/doi-build/provenance.json"
326+
)
327+
docker run "${dockerArgs[@]}" attest "${provArgs[@]}"
328+
329+
push:
330+
name: Push
331+
needs:
332+
- build
333+
- sign
334+
# - verify
335+
if: ${{ always() }}
336+
runs-on: ubuntu-latest
337+
steps:
338+
- run: ${{ (needs.sign.result == 'skipped' && needs.build.result == 'success') || needs.verify.result == 'success' || 'exit 1' }}
339+
- name: Download a single artifact
340+
uses: actions/download-artifact@v4
341+
with:
342+
name: build-oci
343+
- name: Verify artifact
344+
run: |
345+
echo "${{ needs.build.outputs.sha256 }}" sha256sum -c
346+
mkdir -p temp
347+
tar -xvf temp.tar -C temp
348+
- name: Tools
349+
run: |
350+
mkdir .gha-bin
351+
echo "$PWD/.gha-bin" >> "$GITHUB_PATH"
274352
275-
# TODO validate that "signed" still has all the original layer blobs from "temp" (ie, that the attestation manifest *just* has some new layers and everything else is unchanged)
353+
case "${RUNNER_ARCH}" in \
354+
X64) ARCH='amd64';; \
355+
esac
276356
277-
rm -rf temp
278-
mv signed temp
357+
_download() {
358+
local target="$1"; shift
359+
local url="$1"; shift
360+
wget --timeout=5 -O "$target" "$url" --progress=dot:giga
361+
}
279362
363+
# https://doi-janky.infosiftr.net/job/wip/job/crane
364+
_download ".gha-bin/crane" "https://doi-janky.infosiftr.net/job/wip/job/crane/lastSuccessfulBuild/artifact/crane-$ARCH"
365+
# TODO checksum verification ("checksums.txt")
366+
chmod +x ".gha-bin/crane"
367+
".gha-bin/crane" version
280368
- name: Push
281369
env:
282370
DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
283371
DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }}
372+
buildJson: ${{ needs.build.outputs.buildJson }}
284373
run: |
285374
export DOCKER_CONFIG="$PWD/.docker"
286375
mkdir "$DOCKER_CONFIG"
287376
trap 'find "$DOCKER_CONFIG" -type f -exec shred -fuvz "{}" + || :; rm -rf "$DOCKER_CONFIG"' EXIT
288377
docker login --username "$DOCKER_HUB_USERNAME" --password-stdin <<<"$DOCKER_HUB_PASSWORD"
289378
unset DOCKER_HUB_USERNAME DOCKER_HUB_PASSWORD
290379
291-
cd build
292-
shell="$(jq <<<"$json" -r '.commands.push')"
380+
shell="$(jq <<<"$buildJson" -r '.commands.push')"
293381
eval "$shell"
382+
383+
clean:
384+
name: Cleanup
385+
needs:
386+
- build
387+
# - verify
388+
- push
389+
if: ${{ always() }}
390+
runs-on: ubuntu-latest
391+
steps:
392+
- name: Clean Up Artifact
393+
if: ${{ inputs.pruneArtifact == 'true' }}
394+
run: |
395+
curl -L \
396+
-X DELETE \
397+
-H "Accept: application/vnd.github+json" \
398+
-H "Authorization: Bearer ${{ github.token }}" \
399+
-H "X-GitHub-Api-Version: 2022-11-28" \
400+
https://api.github.com/repos/${{ github.repository }}/actions/artifacts/${{ needs.build.outputs.artifactId }}

0 commit comments

Comments
 (0)