|
20 | 20 | - '' # without this, it's technically "required" 🙃
|
21 | 21 | - 2022
|
22 | 22 | - 2019
|
| 23 | + pruneArtifact: |
| 24 | + required: true |
| 25 | + type: choice |
| 26 | + options: |
| 27 | + - "true" |
| 28 | + - "false" |
| 29 | + default: "true" |
23 | 30 | run-name: '${{ inputs.bashbrewArch }}: ${{ inputs.firstTag }} (${{ inputs.buildId }})'
|
24 | 31 | permissions:
|
25 | 32 | contents: read
|
|
45 | 52 | name: Build ${{ inputs.buildId }}
|
46 | 53 | outputs:
|
47 | 54 | shouldSign: ${{ steps.json.outputs.shouldSign }}
|
| 55 | + buildJson: ${{ steps.json.outputs.json }} |
| 56 | + artifactId: ${{ steps.oci.outputs.artifact-id }} |
| 57 | + sha256: ${{ steps.checksum.outputs.sha256 }} |
48 | 58 | runs-on: ${{ inputs.bashbrewArch == 'windows-amd64' && format('windows-{0}', inputs.windowsVersion) || 'ubuntu-latest' }}
|
49 | 59 | steps:
|
50 | 60 |
|
@@ -158,136 +168,233 @@ jobs:
|
158 | 168 | fi
|
159 | 169 | eval "$shell"
|
160 | 170 |
|
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 |
162 | 211 | - name: Configure AWS (for signing)
|
163 |
| - if: steps.json.outputs.shouldSign == 'true' |
164 | 212 | # https://github.com/aws-actions/configure-aws-credentials/releases
|
165 | 213 | uses: aws-actions/configure-aws-credentials@010d0da01d0b5a38af31e9c3470dbfdabdecca3a # v4.0.1
|
166 | 214 | 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 |
171 | 239 | - name: Sign
|
172 |
| - if: steps.json.outputs.shouldSign == 'true' |
173 | 240 | 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 }} |
176 | 246 | 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 |
178 | 261 |
|
179 |
| - args=( |
| 262 | + # TODO more validation? |
| 263 | + ' "$manifest" || return "$?" |
| 264 | + } |
| 265 | +
|
| 266 | + dockerArgs=( |
180 | 267 | --interactive
|
181 | 268 | --rm
|
182 | 269 | --read-only
|
183 | 270 | --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)
|
184 | 271 | )
|
185 | 272 | if [ -t 0 ] && [ -t 1 ]; then
|
186 |
| - args+=( --tty ) |
| 273 | + dockerArgs+=( --tty ) |
187 | 274 | fi
|
188 | 275 |
|
189 | 276 | user="$(id -u)"
|
190 |
| - args+=( --tmpfs "/tmp:uid=$user" ) |
| 277 | + dockerArgs+=( --tmpfs "/tmp:uid=$user" ) |
191 | 278 | user+=":$(id -g)"
|
192 |
| - args+=( --user "$user" ) |
| 279 | + dockerArgs+=( --user "$user" ) |
193 | 280 |
|
194 | 281 | 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=}" ) |
212 | 283 |
|
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 |
| - } |
249 | 284 | validate-oci-layout temp
|
250 | 285 |
|
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 |
252 | 292 |
|
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" |
256 | 298 |
|
257 | 299 | # 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 | + ) |
261 | 302 |
|
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" |
263 | 308 |
|
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 | + ) |
266 | 311 |
|
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 |
269 | 317 | )
|
270 | 318 |
|
271 |
| - docker run "${args[@]}" |
| 319 | + docker run "${dockerArgs[@]}" sign "${signArgs[@]}" |
272 | 320 |
|
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" |
274 | 352 |
|
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 |
276 | 356 |
|
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 | + } |
279 | 362 |
|
| 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 |
280 | 368 | - name: Push
|
281 | 369 | env:
|
282 | 370 | DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
|
283 | 371 | DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
| 372 | + buildJson: ${{ needs.build.outputs.buildJson }} |
284 | 373 | run: |
|
285 | 374 | export DOCKER_CONFIG="$PWD/.docker"
|
286 | 375 | mkdir "$DOCKER_CONFIG"
|
287 | 376 | trap 'find "$DOCKER_CONFIG" -type f -exec shred -fuvz "{}" + || :; rm -rf "$DOCKER_CONFIG"' EXIT
|
288 | 377 | docker login --username "$DOCKER_HUB_USERNAME" --password-stdin <<<"$DOCKER_HUB_PASSWORD"
|
289 | 378 | unset DOCKER_HUB_USERNAME DOCKER_HUB_PASSWORD
|
290 | 379 |
|
291 |
| - cd build |
292 |
| - shell="$(jq <<<"$json" -r '.commands.push')" |
| 380 | + shell="$(jq <<<"$buildJson" -r '.commands.push')" |
293 | 381 | 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