diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index d5dee00..c44c76a 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -32,6 +32,11 @@ permissions: jobs: release: + # Any repository may call a public reusable workflow, and the certificate its run gets names + # this file at the ref it called - build-release.yml@refs/heads/main from someone else's + # workflow, over what that repository checked out. So the builder runs here or nowhere, and a + # verifier pins the source repository besides the SAN (--certificate-github-workflow-repository). + if: github.repository == 'spin-stack/spin-machine' runs-on: ubuntu-24.04 # Every artefact compiled from cold on four cores: QEMU and the kernel are each tens of minutes. timeout-minutes: 300 @@ -194,10 +199,12 @@ jobs: cosign attest-blob --yes --type slsaprovenance1 --predicate _output/provenance.json \ --bundle "${f}.provenance.sigstore.json" "${f}" > /dev/null cosign verify-blob --bundle "${f}.sigstore.json" --certificate-identity "${IDENTITY}" \ - --certificate-oidc-issuer https://token.actions.githubusercontent.com "${f}" + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + --certificate-github-workflow-repository spin-stack/spin-machine "${f}" cosign verify-blob-attestation --type slsaprovenance1 --bundle "${f}.provenance.sigstore.json" \ --certificate-identity "${IDENTITY}" \ - --certificate-oidc-issuer https://token.actions.githubusercontent.com "${f}" + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + --certificate-github-workflow-repository spin-stack/spin-machine "${f}" done # The tarball's SBOM (hack/release), attested over it: what it is made of, by the # workflow that built it. The same SBOM is inside it, at usr/share/spin-stack/sbom. @@ -206,7 +213,8 @@ jobs: --bundle "${f}.cdx.sigstore.json" "${f}" > /dev/null cosign verify-blob-attestation --type cyclonedx --bundle "${f}.cdx.sigstore.json" \ --certificate-identity "${IDENTITY}" \ - --certificate-oidc-issuer https://token.actions.githubusercontent.com "${f}" + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + --certificate-github-workflow-repository spin-stack/spin-machine "${f}" done go tool attestgate files -require provenance _output/release go tool attestgate files -require cdx -except '*.sha256,machine.env,*.cdx.json' _output/release diff --git a/Taskfile.yml b/Taskfile.yml index ada8e44..5ceca30 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -303,7 +303,8 @@ tasks: gh release download "$tag" -p "$sbom" -p "$sbom.sigstore.json" -D "$dir" cosign verify-blob --bundle "$dir/$sbom.sigstore.json" \ --certificate-identity https://github.com/spin-stack/spin-machine/.github/workflows/build-release.yml@refs/heads/main \ - --certificate-oidc-issuer https://token.actions.githubusercontent.com "$dir/$sbom" + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + --certificate-github-workflow-repository spin-stack/spin-machine "$dir/$sbom" # osv-scanner exits 1 when it found something, which is the answer; above 1 it failed. status=0 go run "github.com/google/osv-scanner/v2/cmd/osv-scanner@$(go tool versions version osv-scanner)" \ diff --git a/docs/releasing.md b/docs/releasing.md index cf91bdb..785f24c 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -43,14 +43,17 @@ and `.provenance.sigstore.json`, its SLSA provenance (`go tool provenance` dispatched workflow at its commit, and every pin in `versions.yaml`). `release.yml` publishes nothing itself: it calls `build-release.yml`, a workflow only a call starts, which builds from cold on GitHub's own runner, signs, attests and publishes - so the certificate names the builder, and -that is the identity whoever takes a file checks both bundles against: +that is the identity whoever takes a file checks both bundles against. With the repository it ran +in: any repository can call a public reusable workflow, and its run's certificate names this +builder too, so the SAN alone is not this repository's (the builder's job also refuses to run +anywhere else). ```sh id=https://github.com/spin-stack/spin-machine/.github/workflows/build-release.yml@refs/heads/main -cosign verify-blob --bundle f.sigstore.json --certificate-identity "$id" \ - --certificate-oidc-issuer https://token.actions.githubusercontent.com f -cosign verify-blob-attestation --type slsaprovenance1 --bundle f.provenance.sigstore.json \ - --certificate-identity "$id" --certificate-oidc-issuer https://token.actions.githubusercontent.com f +pin=(--certificate-identity "$id" --certificate-oidc-issuer https://token.actions.githubusercontent.com + --certificate-github-workflow-repository spin-stack/spin-machine) +cosign verify-blob --bundle f.sigstore.json "${pin[@]}" f +cosign verify-blob-attestation --type slsaprovenance1 --bundle f.provenance.sigstore.json "${pin[@]}" f ``` Every release ran on `refs/heads/main`, and that is the ref its certificate names; which release diff --git a/go.mod b/go.mod index f6d492e..935d685 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ tool ( ) require ( - github.com/spin-stack/go-tools v0.0.0-20261013171334-a38a073cfc2d + github.com/spin-stack/go-tools v0.0.0-20261013172709-defd29ba99c4 golang.org/x/sys v0.48.0 ) diff --git a/go.sum b/go.sum index e569f11..f3159da 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ github.com/in-toto/attestation v1.2.0 h1:aPRUZ3azbqD7yEBD5fP3TD8Dszf+YHo284SOcpa github.com/in-toto/attestation v1.2.0/go.mod h1:r79G45gOmzPismgObLSL+rZTFxUgZLOQJI6LofTZgXk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/spin-stack/go-tools v0.0.0-20261013171334-a38a073cfc2d h1:4xHpFO/xiIXTnln50gS42x3zLGYGIlXhaePU3NTIFiA= -github.com/spin-stack/go-tools v0.0.0-20261013171334-a38a073cfc2d/go.mod h1:cy1ojfIbEjrfZi0lGzE+wjueaWw1jrotHQ+vyFz9pZQ= +github.com/spin-stack/go-tools v0.0.0-20261013172709-defd29ba99c4 h1:0w4kv6lPpa80XPxqPuEYjcSSzc4pgYLtrLtcDsrPX5c= +github.com/spin-stack/go-tools v0.0.0-20261013172709-defd29ba99c4/go.mod h1:cy1ojfIbEjrfZi0lGzE+wjueaWw1jrotHQ+vyFz9pZQ= github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/terminalstatic/go-xsd-validate v0.1.8 h1:UVrTCy1j3DhwaYTTUF+QYO/Nan13S0tf+Jwi+p45Bf0= diff --git a/workflows/workflows_test.go b/workflows/workflows_test.go index 6278826..30d4de4 100644 --- a/workflows/workflows_test.go +++ b/workflows/workflows_test.go @@ -14,7 +14,7 @@ import ( // runs after an unconditional provenance attestation in its job; the ones that look like an upload // and ship nothing anyone installs are named here, each with why. func TestEveryUploadIsAttested(t *testing.T) { - g := attestgate.Gate{Root: "..", RequireSBOM: true, MainOnly: []string{"release.yml"}, Builder: "build-release.yml", NotPublished: map[string]string{ + g := attestgate.Gate{Root: "..", RequireSBOM: true, MainOnly: []string{"release.yml"}, Builder: "build-release.yml", Repository: "spin-stack/spin-machine", NotPublished: map[string]string{ "ci.yml/gate/Upload Go coverage HTML": "a coverage page for a person, kept seven days", "kernel.yml/build/Upload the kernel": "a lane's kernel for a person to fetch; a release builds its own", "lab.yml/lab/Keep the log": "a measurement's log, for a person",