|
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
|
|
44 | 51 | build:
|
45 | 52 | name: Build ${{ inputs.buildId }}
|
46 | 53 | outputs:
|
47 |
| - shouldSign: ${{ steps.json.outputs.shouldSign }} |
| 54 | + buildJson: ${{ steps.json.outputs.json }} |
| 55 | + artifactId: ${{ steps.oci.outputs.artifact-id }} |
| 56 | + sha256: ${{ steps.checksum.outputs.sha256 }} |
48 | 57 | runs-on: ${{ inputs.bashbrewArch == 'windows-amd64' && format('windows-{0}', inputs.windowsVersion) || 'ubuntu-latest' }}
|
49 | 58 | steps:
|
50 | 59 |
|
@@ -156,136 +165,233 @@ jobs:
|
156 | 165 | fi
|
157 | 166 | eval "$shell"
|
158 | 167 |
|
159 |
| - # TODO signing prototype (see above where "shouldSign" is populated) |
| 168 | + - name: Generate Checksum |
| 169 | + id: checksum |
| 170 | + run: | |
| 171 | + cd build |
| 172 | + tar -cvf temp.tar -C temp . |
| 173 | + echo "sha256=$(sha256sum temp.tar)" >> "$GITHUB_OUTPUT" |
| 174 | + - name: Stage artifact |
| 175 | + id: oci |
| 176 | + uses: actions/upload-artifact@v4 |
| 177 | + with: |
| 178 | + name: build-oci |
| 179 | + path: | |
| 180 | + build/temp.tar* |
| 181 | + retention-days: 5 |
| 182 | + |
| 183 | + sign: |
| 184 | + name: Sign |
| 185 | + needs: build |
| 186 | + if: fromJSON(needs.build.outputs.buildJson).shouldSign |
| 187 | + runs-on: ubuntu-latest |
| 188 | + permissions: |
| 189 | + contents: read |
| 190 | + actions: write # for https://github.com/andymckay/cancel-action (see usage below) |
| 191 | + id-token: write # for AWS KMS signing (see usage below) |
| 192 | + steps: |
| 193 | + - uses: actions/checkout@v4 |
| 194 | + with: |
| 195 | + sparse-checkout-cone-mode: 'false' |
| 196 | + sparse-checkout: | |
| 197 | + .scripts/oci.jq |
| 198 | + .scripts/provenance.jq |
| 199 | + - name: Download a single artifact |
| 200 | + uses: actions/download-artifact@v4 |
| 201 | + with: |
| 202 | + name: build-oci |
| 203 | + - name: Verify artifact |
| 204 | + run: | |
| 205 | + echo "${{ needs.build.outputs.sha256 }}" sha256sum -c |
| 206 | + mkdir -p temp |
| 207 | + tar -xvf temp.tar -C temp |
160 | 208 | - name: Configure AWS (for signing)
|
161 |
| - if: fromJSON(steps.json.outputs.json).shouldSign |
162 | 209 | # https://github.com/aws-actions/configure-aws-credentials/releases
|
163 | 210 | uses: aws-actions/configure-aws-credentials@010d0da01d0b5a38af31e9c3470dbfdabdecca3a # v4.0.1
|
164 | 211 | with:
|
165 |
| - # TODO drop "subset" (github.ref_name == "main" && ... || ...) |
166 |
| - aws-region: ${{ contains(fromJSON('["main","subset"]'), github.ref_name) && secrets.AWS_KMS_PROD_REGION || secrets.AWS_KMS_STAGE_REGION }} |
167 |
| - role-to-assume: ${{ contains(fromJSON('["main","subset"]'), github.ref_name) && secrets.AWS_KMS_PROD_ROLE_ARN || secrets.AWS_KMS_STAGE_ROLE_ARN }} |
168 |
| - # TODO figure out if there's some way we could make our secrets ternaries here more DRY without major headaches 🙈 |
| 212 | + aws-region: ${{ secrets.AWS_KMS_REGION }} |
| 213 | + role-to-assume: ${{ secrets.AWS_KMS_ROLE_ARN }} |
| 214 | + - name: Generate Provenance |
| 215 | + env: |
| 216 | + buildJson: ${{ needs.build.outputs.buildJson }} |
| 217 | + GITHUB_CONTEXT: ${{ toJson(github) }} |
| 218 | + run: | |
| 219 | + image-digest() { |
| 220 | + local dir="$1/blobs" |
| 221 | + img=$( |
| 222 | + grep -R --include "*" '"mediaType":\s"application/vnd.oci.image.layer.' "$dir" \ |
| 223 | + | head -n 1 \ |
| 224 | + | cut -d ':' -f1 |
| 225 | + ) |
| 226 | + [ "$(cat $img | jq -r '.mediaType')" = "application/vnd.oci.image.manifest.v1+json" ] || exit 1 |
| 227 | + echo $img | rev | cut -d '/' -f2,1 --output-delimiter ':' | rev |
| 228 | + } |
| 229 | +
|
| 230 | + digest=$(image-digest temp) |
| 231 | +
|
| 232 | + echo $buildJson | jq -L.scripts --argjson github '${{ env.GITHUB_CONTEXT }}' --argjson runner '${{ toJson(runner) }}' --arg digest ${digest} ' |
| 233 | + include "provenance"; |
| 234 | + github_actions_provenance($github; $runner; $digest) |
| 235 | + ' >> provenance.json |
169 | 236 | - name: Sign
|
170 |
| - if: fromJSON(steps.json.outputs.json).shouldSign |
171 | 237 | env:
|
172 |
| - AWS_KMS_REGION: ${{ contains(fromJSON('["main","subset"]'), github.ref_name) && secrets.AWS_KMS_PROD_REGION || secrets.AWS_KMS_STAGE_REGION }} |
173 |
| - AWS_KMS_KEY_ARN: ${{ contains(fromJSON('["main","subset"]'), github.ref_name) && secrets.AWS_KMS_PROD_KEY_ARN || secrets.AWS_KMS_STAGE_KEY_ARN }} |
| 238 | + AWS_KMS_REGION: ${{ secrets.AWS_KMS_REGION }} |
| 239 | + AWS_KMS_KEY_ARN: ${{ secrets.AWS_KMS_KEY_ARN }} |
| 240 | + |
| 241 | + DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }} |
| 242 | + DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }} |
174 | 243 | run: |
|
175 |
| - cd build |
| 244 | + validate-oci-layout() { |
| 245 | + local dir="$1" |
| 246 | + jq -L.scripts -s ' |
| 247 | + include "oci"; |
| 248 | + validate_oci_layout | true |
| 249 | + ' "$dir/oci-layout" "$dir/index.json" || return "$?" |
| 250 | + local manifest |
| 251 | + manifest="$dir/blobs/$(jq -r '.manifests[0].digest | sub(":"; "/")' "$dir/index.json")" || return "$?" |
| 252 | + jq -L.scripts -s ' |
| 253 | + include "oci"; |
| 254 | + if length != 1 then |
| 255 | + error("unexpected image index document count: \(length)") |
| 256 | + else .[0] end |
| 257 | + | validate_oci_index |
176 | 258 |
|
177 |
| - args=( |
| 259 | + # TODO more validation? |
| 260 | + ' "$manifest" || return "$?" |
| 261 | + } |
| 262 | +
|
| 263 | + dockerArgs=( |
178 | 264 | --interactive
|
179 | 265 | --rm
|
180 | 266 | --read-only
|
181 | 267 | --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)
|
182 | 268 | )
|
183 | 269 | if [ -t 0 ] && [ -t 1 ]; then
|
184 |
| - args+=( --tty ) |
| 270 | + dockerArgs+=( --tty ) |
185 | 271 | fi
|
186 | 272 |
|
187 | 273 | user="$(id -u)"
|
188 |
| - args+=( --tmpfs "/tmp:uid=$user" ) |
| 274 | + dockerArgs+=( --tmpfs "/tmp:uid=$user" ) |
189 | 275 | user+=":$(id -g)"
|
190 |
| - args+=( --user "$user" ) |
| 276 | + dockerArgs+=( --user "$user" ) |
191 | 277 |
|
192 | 278 | awsEnvs=( "${!AWS_@}" )
|
193 |
| - args+=( "${awsEnvs[@]/#/--env=}" ) |
194 |
| -
|
195 |
| - # some very light assumption verification (see TODO in --mount below) |
196 |
| - validate-oci-layout() { |
197 |
| - local dir="$1" |
198 |
| - jq -s ' |
199 |
| - if length != 1 then |
200 |
| - error("unexpected 'oci-layout' document count: " + length) |
201 |
| - else .[0] end |
202 |
| - | if .imageLayoutVersion != "1.0.0" then |
203 |
| - error("unsupported imageLayoutVersion: " + .imageLayoutVersion) |
204 |
| - else . end |
205 |
| - ' "$dir/oci-layout" || return "$?" |
206 |
| - jq -s ' |
207 |
| - if length != 1 then |
208 |
| - error("unexpected 'index.json' document count: " + length) |
209 |
| - else .[0] end |
| 279 | + dockerArgs+=( "${awsEnvs[@]/#/--env=}" ) |
210 | 280 |
|
211 |
| - | if .schemaVersion != 2 then |
212 |
| - error("unsupported schemaVersion: " + .schemaVersion) |
213 |
| - else . end |
214 |
| - | 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 |
215 |
| - error("unsupported index mediaType: " + .mediaType) |
216 |
| - else . end |
217 |
| - | if .manifests | length != 1 then |
218 |
| - error("expected only one manifests entry, not " + (.manifests | length)) |
219 |
| - else . end |
220 |
| -
|
221 |
| - | .manifests[0] |= ( |
222 |
| - if .mediaType != "application/vnd.oci.image.index.v1+json" then |
223 |
| - error("unsupported descriptor mediaType: " + .mediaType) |
224 |
| - else . end |
225 |
| - # TODO validate .digest somehow (`crane validate`?) - would also be good to validate all descriptors recursively |
226 |
| - | if .size < 0 then |
227 |
| - error("invalid descriptor size: " + .size) |
228 |
| - else . end |
229 |
| - ) |
230 |
| - ' "$dir/index.json" || return "$?" |
231 |
| - local manifest |
232 |
| - manifest="$dir/blobs/$(jq -r '.manifests[0].digest | sub(":"; "/")' "$dir/index.json")" || return "$?" |
233 |
| - jq -s ' |
234 |
| - if length != 1 then |
235 |
| - error("unexpected image index document count: " + length) |
236 |
| - else .[0] end |
237 |
| - | if .schemaVersion != 2 then |
238 |
| - error("unsupported schemaVersion: " + .schemaVersion) |
239 |
| - else . end |
240 |
| - | if .mediaType != "application/vnd.oci.image.index.v1+json" then |
241 |
| - error("unsupported image index mediaType: " + .mediaType) |
242 |
| - else . end |
243 |
| -
|
244 |
| - # TODO more validation? |
245 |
| - ' "$manifest" || return "$?" |
246 |
| - } |
247 | 281 | validate-oci-layout temp
|
248 | 282 |
|
249 |
| - mkdir signed |
| 283 | + # Login to Docker Hub |
| 284 | + export DOCKER_CONFIG="$PWD/.docker" |
| 285 | + mkdir "$DOCKER_CONFIG" |
| 286 | + trap 'find "$DOCKER_CONFIG" -type f -exec shred -fuvz "{}" + || :; rm -rf "$DOCKER_CONFIG"' EXIT |
| 287 | + docker login --username "$DOCKER_HUB_USERNAME" --password-stdin <<<"$DOCKER_HUB_PASSWORD" |
| 288 | + unset DOCKER_HUB_USERNAME DOCKER_HUB_PASSWORD |
250 | 289 |
|
251 |
| - args+=( |
252 |
| - --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) |
253 |
| - --mount "type=bind,src=$PWD/signed,dst=/doi-build/signed" |
| 290 | + # Create signatures |
| 291 | + dockerArgs+=( |
| 292 | + --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) |
| 293 | + --mount "type=bind,src=$PWD/provenance.json,dst=/doi-build/provenance.json" |
| 294 | + --mount "type=bind,src=$PWD/.docker,dst=/.docker" |
254 | 295 |
|
255 | 296 | # https://explore.ggcr.dev/?repo=docker/image-signer-verifier
|
256 |
| - docker/image-signer-verifier:0.3.3@sha256:a5351e6495596429bacea85fbf8f41a77ce7237c26c74fd7c3b94c3e6d409c82 |
257 |
| -
|
258 |
| - sign |
| 297 | + "$IMAGE_SIGNER" |
| 298 | + ) |
259 | 299 |
|
260 |
| - --envelope-style oci-content-descriptor |
| 300 | + kmsArg=( |
| 301 | + # kms key used to sign attestation artifacts |
| 302 | + --kms="AWS" |
| 303 | + --kms-region="$AWS_KMS_REGION" |
| 304 | + --kms-key-ref="$AWS_KMS_KEY_ARN" |
261 | 305 |
|
262 |
| - --aws_region "$AWS_KMS_REGION" |
263 |
| - --aws_arn "awskms:///$AWS_KMS_KEY_ARN" |
| 306 | + --referrers-dest="$REFERRERS_REPO" # repo to store attestation artifacts and provenance |
| 307 | + ) |
264 | 308 |
|
265 |
| - --input oci:///doi-build/unsigned |
266 |
| - --output oci:///doi-build/signed |
| 309 | + # Sign buildkit statements |
| 310 | + signArgs=( |
| 311 | + ${kmsArg[@]} |
| 312 | + --input=oci:///doi-build/image |
| 313 | + --keep=true # keep preserves the unsigned attestations generated by buildkit |
267 | 314 | )
|
268 | 315 |
|
269 |
| - docker run "${args[@]}" |
| 316 | + docker run "${dockerArgs[@]}" sign "${signArgs[@]}" |
270 | 317 |
|
271 |
| - validate-oci-layout signed |
| 318 | + # Attach and sign provenance |
| 319 | + provArgs=( |
| 320 | + ${kmsArg[@]} |
| 321 | + --image=oci:///doi-build/image |
| 322 | + --statement="/doi-build/provenance.json" |
| 323 | + ) |
| 324 | + docker run "${dockerArgs[@]}" attest "${provArgs[@]}" |
| 325 | +
|
| 326 | + push: |
| 327 | + name: Push |
| 328 | + needs: |
| 329 | + - build |
| 330 | + - sign |
| 331 | + # - verify |
| 332 | + if: ${{ always() }} |
| 333 | + runs-on: ubuntu-latest |
| 334 | + steps: |
| 335 | + - run: ${{ (needs.sign.result == 'skipped' && needs.build.result == 'success') || needs.verify.result == 'success' || 'exit 1' }} |
| 336 | + - name: Download a single artifact |
| 337 | + uses: actions/download-artifact@v4 |
| 338 | + with: |
| 339 | + name: build-oci |
| 340 | + - name: Verify artifact |
| 341 | + run: | |
| 342 | + echo "${{ needs.build.outputs.sha256 }}" sha256sum -c |
| 343 | + mkdir -p temp |
| 344 | + tar -xvf temp.tar -C temp |
| 345 | + - name: Tools |
| 346 | + run: | |
| 347 | + mkdir .gha-bin |
| 348 | + echo "$PWD/.gha-bin" >> "$GITHUB_PATH" |
272 | 349 |
|
273 |
| - # 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) |
| 350 | + case "${RUNNER_ARCH}" in \ |
| 351 | + X64) ARCH='amd64';; \ |
| 352 | + esac |
274 | 353 |
|
275 |
| - rm -rf temp |
276 |
| - mv signed temp |
| 354 | + _download() { |
| 355 | + local target="$1"; shift |
| 356 | + local url="$1"; shift |
| 357 | + wget --timeout=5 -O "$target" "$url" --progress=dot:giga |
| 358 | + } |
277 | 359 |
|
| 360 | + # https://doi-janky.infosiftr.net/job/wip/job/crane |
| 361 | + _download ".gha-bin/crane" "https://doi-janky.infosiftr.net/job/wip/job/crane/lastSuccessfulBuild/artifact/crane-$ARCH" |
| 362 | + # TODO checksum verification ("checksums.txt") |
| 363 | + chmod +x ".gha-bin/crane" |
| 364 | + ".gha-bin/crane" version |
278 | 365 | - name: Push
|
279 | 366 | env:
|
280 | 367 | DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
|
281 | 368 | DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
| 369 | + buildJson: ${{ needs.build.outputs.buildJson }} |
282 | 370 | run: |
|
283 | 371 | export DOCKER_CONFIG="$PWD/.docker"
|
284 | 372 | mkdir "$DOCKER_CONFIG"
|
285 | 373 | trap 'find "$DOCKER_CONFIG" -type f -exec shred -fuvz "{}" + || :; rm -rf "$DOCKER_CONFIG"' EXIT
|
286 | 374 | docker login --username "$DOCKER_HUB_USERNAME" --password-stdin <<<"$DOCKER_HUB_PASSWORD"
|
287 | 375 | unset DOCKER_HUB_USERNAME DOCKER_HUB_PASSWORD
|
288 | 376 |
|
289 |
| - cd build |
290 |
| - shell="$(jq <<<"$json" -r '.commands.push')" |
| 377 | + shell="$(jq <<<"$buildJson" -r '.commands.push')" |
291 | 378 | eval "$shell"
|
| 379 | +
|
| 380 | + clean: |
| 381 | + name: Cleanup |
| 382 | + needs: |
| 383 | + - build |
| 384 | + # - verify |
| 385 | + - push |
| 386 | + if: ${{ always() }} |
| 387 | + runs-on: ubuntu-latest |
| 388 | + steps: |
| 389 | + - name: Clean Up Artifact |
| 390 | + if: ${{ inputs.pruneArtifact == 'true' }} |
| 391 | + run: | |
| 392 | + curl -L \ |
| 393 | + -X DELETE \ |
| 394 | + -H "Accept: application/vnd.github+json" \ |
| 395 | + -H "Authorization: Bearer ${{ github.token }}" \ |
| 396 | + -H "X-GitHub-Api-Version: 2022-11-28" \ |
| 397 | + https://api.github.com/repos/${{ github.repository }}/actions/artifacts/${{ needs.build.outputs.artifactId }} |
0 commit comments