diff --git a/README.md b/README.md index 8a3e336..13f1790 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,8 @@ The Go the spin-stack repositories build with, and hold their code to: tools a repository runs from its go.mod (`go tool `), each a command under `cmd/`. Only `versions`, -`versions/upstream`, `trustroot`, `provenance`, `attestgate`, `sbom` and `fulciotest` (a test -Sigstore whose certificates carry Fulcio's source-repository and build extensions, which a verifier -of a reusable builder pins) are packages another repository imports; the rest is +`versions/upstream`, `trustroot`, `provenance`, `attestgate` and `sbom` are packages another +repository imports; the rest is `internal/`. Nothing here is linked into what any of them ships but versions' reading half, which spin's binaries use to read the pins they carry, and the trusted root, which what verifies a release's signatures checks them against. @@ -95,9 +94,7 @@ an hour. provenance predicate of that run - the workflow at its ref as the builder, the repository at its commit and every versions.yaml pin as the inputs, and no clock - for `cosign attest-blob --type slsaprovenance1 --predicate ` to sign over the files. Package `provenance` is the same, for -a build that adds inputs of its own (`Build`'s extra). In a reusable workflow, `-builder -.github/workflows/` (`Predicate.BuiltBy`) makes that workflow the builder, as the run's -certificate names it; GitHub's environment names only the caller there. +a build that adds inputs of its own (`Build`'s extra). `attestgate.Gate`, run from a test like the versions gate, fails on a workflow step that publishes (a release action, `oras push`, `gh release upload`, `docker push`, `aws s3 cp`, a task @@ -105,12 +102,7 @@ named in `UploadTasks`) without a step before it in its job that attests (`cosig --type slsaprovenance1`, or a task in `AttestTasks`) unconditionally and with `id-token: write`. `NotPublished` excuses a step by `//`, with its reason, and an excuse that matches nothing fails; `MainOnly` holds release workflows to no push trigger and every job on -`refs/heads/main`. `Builders` name the reusable workflows (`on: workflow_call` alone) every upload -must be in, each called only from a job on `refs/heads/main` that grants `id-token: write`: the -certificate then names the builder, which a verifier pins - with the source repository too, since -any repository can call a public reusable workflow and be issued the builder's SAN: `Repository` -holds every builder job to `if: github.repository == ''`. The YAML cannot say which -files a glob covered, so the step right before an +`refs/heads/main`. The YAML cannot say which files a glob covered, so the step right before an upload runs `go tool attestgate files -require provenance [-except checksums.txt] `, which fails on a file there with no `.provenance.sigstore.json` beside it. diff --git a/attestgate/attestgate.go b/attestgate/attestgate.go index db5beef..9a840b2 100644 --- a/attestgate/attestgate.go +++ b/attestgate/attestgate.go @@ -39,49 +39,6 @@ type Gate struct { // MainOnly are the workflow files that cut a release: none has a push trigger, and every job // in one runs only on refs/heads/main. MainOnly []string - // Builders are the reusable workflows every publishing step is in, by their file names: each - // on: workflow_call and nothing else, so it runs only as a job of a workflow that calls it with - // uses:, and only from a job that runs on refs/heads/main. The certificate a step in one signs - // with names that builder, not the caller, so a verifier pinning it pins the code that built and - // attested; a caller that published by itself would sign as itself and be outside the pin. None: - // a release workflow publishes itself. - Builders []string - // Repository is the repository the builders are, owner/name, which every job of a builder runs - // only in (if: github.repository == ''): any repository can call a public reusable - // workflow, and its run's certificate names the builder at the ref it called, over what that - // repository checked out. Required with Builders. - Repository string -} - -// checkBuilders is what is wrong with the Builders named, given the workflow files there are. -func (g Gate) checkBuilders(files map[string]bool) []string { - if len(g.Builders) == 0 { - return nil - } - var problems []string - for _, b := range g.Builders { - if !files[b] { - problems = append(problems, b+": Builders names a workflow there is not") - } - } - if !strings.Contains(g.Repository, "/") { - problems = append(problems, "Builders with no Repository (owner/name) to run only in") - } - return problems -} - -func (g Gate) isBuilder(file string) bool { return slices.Contains(g.Builders, file) } - -// ownRepository is whether a job's if: holds it to the builder's repository. -func (g Gate) ownRepository(cond string) bool { - return g.Repository != "" && - regexp.MustCompile(`github\.repository\s*==\s*'`+regexp.QuoteMeta(g.Repository)+`'`).MatchString(cond) -} - -// callsBuilder is whether a job's uses: is a builder, as a workflow of the same repository names it. -func (g Gate) callsBuilder(j job) bool { - file, ok := strings.CutPrefix(j.Uses, "./.github/workflows/") - return ok && g.isBuilder(file) } type workflow struct { @@ -247,7 +204,6 @@ func (g Gate) Check() error { problems = append(problems, fmt.Sprintf("%s: MainOnly names a workflow there is not", f)) } } - problems = append(problems, g.checkBuilders(files)...) for _, k := range slices.Sorted(maps.Keys(g.NotPublished)) { if strings.TrimSpace(g.NotPublished[k]) == "" { problems = append(problems, fmt.Sprintf("%s: NotPublished with no reason", k)) @@ -262,48 +218,22 @@ func (g Gate) Check() error { return nil } -// checkJob is what is wrong with job j at where, as a whole: a release job that may run off main, -// a builder job that may run in another repository, and a call to a builder from a job that may -// run off main or with no token to sign. -func (g Gate) checkJob(where string, j job, mainOnly, builder, token bool) []string { - var problems []string - if mainOnly && !mainRef.MatchString(j.If) { - problems = append(problems, where+": a release job with no if: github.ref == 'refs/heads/main'") - } - if builder && !g.ownRepository(j.If) { - problems = append(problems, where+": a builder job with no if: github.repository == '"+g.Repository+"': another repository's call would sign as the builder") - } - if !g.callsBuilder(j) { - return problems - } - if !mainRef.MatchString(j.If) { - problems = append(problems, where+": calls a builder with no if: github.ref == 'refs/heads/main'") - } - // A called workflow has at most its caller's permissions, whatever it declares. - if !token { - problems = append(problems, where+": calls the builder with no id-token: write, so it cannot sign") - } - return problems -} - func (g Gate) check(file string, w workflow, excused map[string]bool) []string { var problems []string mainOnly := slices.Contains(g.MainOnly, file) if mainOnly && slices.Contains(triggers(w.On), "push") { problems = append(problems, fmt.Sprintf("%s: a release workflow with a push trigger: it runs on whatever was pushed", file)) } - builder := g.isBuilder(file) - if on := triggers(w.On); builder && (len(on) != 1 || on[0] != "workflow_call") { - problems = append(problems, fmt.Sprintf("%s: the builder is on: %v, not workflow_call alone: it runs without a release workflow calling it", file, on)) - } for _, jn := range slices.Sorted(maps.Keys(w.Jobs)) { j := w.Jobs[jn] + if mainOnly && !mainRef.MatchString(j.If) { + problems = append(problems, fmt.Sprintf("%s/%s: a release job with no if: github.ref == 'refs/heads/main'", file, jn)) + } // A job's permissions replace the workflow's whole, they do not add to them. token := idToken(w.Permissions) if j.Permissions.Kind != 0 { token = idToken(j.Permissions) } - problems = append(problems, g.checkJob(file+"/"+jn, j, mainOnly, builder, token)...) attested := covers{} for _, s := range j.Steps { where := file + "/" + jn + "/" + s.key() @@ -325,8 +255,6 @@ func (g Gate) check(file string, w workflow, excused map[string]bool) []string { } has := attested.or(self) switch { - case len(g.Builders) > 0 && !builder: - problems = append(problems, fmt.Sprintf("%s: publishes outside the builders %v: a workflow calls one with uses:", where, g.Builders)) case !has.provenance: problems = append(problems, fmt.Sprintf("%s: publishes with no provenance attested before it in its job", where)) case g.RequireSBOM && !has.sbom: diff --git a/attestgate/attestgate_test.go b/attestgate/attestgate_test.go index 552da85..7146b11 100644 --- a/attestgate/attestgate_test.go +++ b/attestgate/attestgate_test.go @@ -173,104 +173,6 @@ func TestAnUploadNeedsAnAttestationBeforeIt(t *testing.T) { } } -// With builders, what is published is published from them alone: reusable workflows that run only -// when called, in their own repository, called only by a job on main that gives them a token to -// sign with, and held inside to the rules every publishing job is held to. -func TestABuilderIsWhereEveryUploadIs(t *testing.T) { - const main = "github.ref == 'refs/heads/main'" - caller := func(on, perms, jobIf string) string { - w := "name: release\non: " + on + "\n" - if perms != "" { - w += "permissions:\n " + perms + "\n" - } - w += "jobs:\n release:\n" - if jobIf != "" { - w += " if: " + jobIf + "\n" - } - return w + " uses: ./.github/workflows/build-release.yml\n" - } - good := caller("workflow_dispatch", "id-token: write", main) - const own = "github.repository == 'spin-stack/x'" - builder := workflow("workflow_call", "id-token: write", own, "", build, attest, release) - gate := attestgate.Gate{MainOnly: []string{"release.yml"}, Builders: []string{"build-release.yml"}, Repository: "spin-stack/x"} - // CI on a push to main, calling a second builder for what it saves. - ci := "name: ci\non: [push, pull_request]\npermissions:\n id-token: write\njobs:\n save:\n if: " + main + - "\n uses: ./.github/workflows/build-packs.yml\n" - two := attestgate.Gate{MainOnly: []string{"release.yml"}, Builders: []string{"build-release.yml", "build-packs.yml"}, Repository: "spin-stack/x"} - for name, tc := range map[string]struct { - files map[string]string - gate attestgate.Gate - problems []string - }{ - "called from main": {files: map[string]string{"release.yml": good, "build-release.yml": builder}, gate: gate}, - "no builder at all": {files: map[string]string{"release.yml": workflow("workflow_dispatch", "id-token: write", main, "", attest, release)}, gate: attestgate.Gate{MainOnly: []string{"release.yml"}}}, - "an upload in the caller": {files: map[string]string{"release.yml": workflow("workflow_dispatch", "id-token: write", main, "", attest, release), "build-release.yml": builder}, - gate: gate, problems: []string{"release.yml/release/Release: publishes outside the builders [build-release.yml]"}}, - "an upload in another workflow": {files: map[string]string{"release.yml": good, "build-release.yml": builder, - "nightly.yml": workflow("schedule", "id-token: write", "", "", attest, orasPush)}, - gate: gate, problems: []string{"nightly.yml/release/Push: publishes outside the builders"}}, - "an excused upload elsewhere": {files: map[string]string{"release.yml": good, "build-release.yml": builder, - "ci.yml": workflow("push", "", "", "", " - name: Logs\n uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1\n")}, - gate: attestgate.Gate{MainOnly: []string{"release.yml"}, Builders: []string{"build-release.yml"}, Repository: "spin-stack/x", NotPublished: map[string]string{"ci.yml/release/Logs": "the run's logs"}}}, - "two builders, one called from CI on main": {files: map[string]string{"release.yml": good, "build-release.yml": builder, "ci.yml": ci, - "build-packs.yml": workflow("workflow_call", "id-token: write", own, "", build, attest, orasPush)}, gate: two}, - "a builder called from CI off main": {files: map[string]string{"release.yml": good, "build-release.yml": builder, - "ci.yml": strings.Replace(ci, " if: "+main+"\n", "", 1), - "build-packs.yml": workflow("workflow_call", "id-token: write", own, "", build, attest, orasPush)}, - gate: two, problems: []string{"ci.yml/save: calls a builder with no if: github.ref == 'refs/heads/main'"}}, - "a reusable workflow that is not a builder publishes": {files: map[string]string{"release.yml": good, "build-release.yml": builder, "ci.yml": ci, - "build-packs.yml": workflow("workflow_call", "id-token: write", own, "", build, attest, orasPush)}, - gate: gate, problems: []string{"build-packs.yml/release/Push: publishes outside the builders"}}, - "a builder that runs by itself": {files: map[string]string{"release.yml": good, - "build-release.yml": workflow("[workflow_call, workflow_dispatch]", "id-token: write", own, "", attest, release)}, - gate: gate, problems: []string{"build-release.yml: the builder is on: [workflow_call workflow_dispatch]"}}, - "a builder on something else": {files: map[string]string{"release.yml": good, - "build-release.yml": workflow("push", "id-token: write", own, "", attest, release)}, - gate: gate, problems: []string{"build-release.yml: the builder is on: [push]"}}, - "a builder there is not": {files: map[string]string{"release.yml": good}, gate: gate, - problems: []string{"build-release.yml: Builders names a workflow there is not"}}, - "a caller from any ref": {files: map[string]string{"release.yml": caller("workflow_dispatch", "id-token: write", ""), "build-release.yml": builder}, - gate: gate, problems: []string{"release.yml/release: a release job with no if", "release.yml/release: calls a builder with no if: github.ref"}}, - "a caller with no token to give": {files: map[string]string{"release.yml": caller("workflow_dispatch", "contents: write", main), "build-release.yml": builder}, - gate: gate, problems: []string{"release.yml/release: calls the builder with no id-token: write"}}, - "another workflow's uses is not the builder": {files: map[string]string{"release.yml": good, "build-release.yml": builder, - "lint.yml": strings.Replace(caller("push", "", ""), "build-release.yml", "lint-reusable.yml", 1)}, gate: gate}, - "a builder that publishes unattested": {files: map[string]string{"release.yml": good, - "build-release.yml": workflow("workflow_call", "id-token: write", own, "", build, release)}, - gate: gate, problems: []string{"build-release.yml/release/Release: publishes with no provenance"}}, - "a builder that runs in any repository": {files: map[string]string{"release.yml": good, - "build-release.yml": workflow("workflow_call", "id-token: write", "", "", build, attest, release)}, - gate: gate, problems: []string{"build-release.yml/release: a builder job with no if: github.repository == 'spin-stack/x'"}}, - "a builder that runs in another repository": {files: map[string]string{"release.yml": good, - "build-release.yml": workflow("workflow_call", "id-token: write", "github.repository == 'spin-stack/y'", "", build, attest, release)}, - gate: gate, problems: []string{"build-release.yml/release: a builder job with no if: github.repository == 'spin-stack/x'"}}, - "a builder with no repository": {files: map[string]string{"release.yml": good, "build-release.yml": builder}, - gate: attestgate.Gate{MainOnly: []string{"release.yml"}, Builders: []string{"build-release.yml"}}, - problems: []string{"build-release.yml/release: a builder job with no if: github.repository == ''", "Builders with no Repository"}}, - } { - t.Run(name, func(t *testing.T) { - root := t.TempDir() - dir := filepath.Join(root, ".github", "workflows") - require.NoError(t, os.MkdirAll(dir, 0o755)) - for f, body := range tc.files { - require.NoError(t, os.WriteFile(filepath.Join(dir, f), []byte(body), 0o600)) - } - tc.gate.Root = root - err := tc.gate.Check() - if len(tc.problems) == 0 { - require.NoError(t, err) - return - } - require.Error(t, err) - lines := strings.Split(err.Error(), "\n")[1:] - require.Len(t, lines, len(tc.problems), err.Error()) - for i, p := range tc.problems { - assert.Contains(t, lines[i], p) - } - }) - } -} - // A repository with no workflows, or one that is not YAML, is no clean answer. func TestWorkflowsItCannotReadAreAnError(t *testing.T) { require.Error(t, attestgate.Gate{Root: t.TempDir()}.Check()) diff --git a/cmd/provenance/main.go b/cmd/provenance/main.go index 901a6cb..23daf7f 100644 --- a/cmd/provenance/main.go +++ b/cmd/provenance/main.go @@ -3,7 +3,7 @@ // cosign attest-blob --type slsaprovenance1 --predicate to sign over the files a release // publishes. Outside a workflow it refuses. // -// go tool provenance -version v20261013.1 -out _output/provenance.json [-builder .github/workflows/build-release.yml] +// go tool provenance -version v20261013.1 -out _output/provenance.json package main import ( @@ -27,7 +27,6 @@ func run(env func(string) string, args []string, stderr io.Writer) int { version := fs.String("version", "", "the release the run builds") out := fs.String("out", "", "where to write the predicate") pins := fs.String("versions", versions.File, "the repository's versions.yaml") - builder := fs.String("builder", "", "the reusable workflow the run signs in (.github/workflows/); the dispatched one when empty") if err := fs.Parse(args); err != nil { return 2 } @@ -41,9 +40,6 @@ func run(env func(string) string, args []string, stderr io.Writer) int { return 2 } p, err := provenance.Build(env, *version, v, nil) - if err == nil && *builder != "" { - p, err = p.BuiltBy(*builder) - } if err != nil { say(stderr, err) return 2 diff --git a/cmd/provenance/main_test.go b/cmd/provenance/main_test.go index c0786d2..65c3b1f 100644 --- a/cmd/provenance/main_test.go +++ b/cmd/provenance/main_test.go @@ -40,7 +40,6 @@ func TestItWritesTheRunsPredicateOrNothing(t *testing.T) { args func(dir string) []string pins string written bool - builder string }{ "in a run": {env: run1, pins: pins, written: true, args: func(dir string) []string { return []string{"-version", "v1", "-out", filepath.Join(dir, "p.json")} }}, @@ -50,14 +49,6 @@ func TestItWritesTheRunsPredicateOrNothing(t *testing.T) { args: func(dir string) []string { return []string{"-version", "v1", "-out", filepath.Join(dir, "p.json")} }}, "no versions.yaml": {env: run1, args: func(dir string) []string { return []string{"-version", "v1", "-out", filepath.Join(dir, "p.json")} }}, - "built by a reusable workflow": {env: run1, pins: pins, written: true, builder: "https://github.com/spin-stack/machine/.github/workflows/build-release.yml@refs/heads/main", - args: func(dir string) []string { - return []string{"-version", "v1", "-builder", ".github/workflows/build-release.yml", "-out", filepath.Join(dir, "p.json")} - }}, - "a builder that is no workflow": {env: run1, pins: pins, - args: func(dir string) []string { - return []string{"-version", "v1", "-builder", "build-release.yml", "-out", filepath.Join(dir, "p.json")} - }}, "an unknown flag": {env: run1, pins: pins, args: func(string) []string { return []string{"-subject", "x"} }}, "an -out it cannot write": {env: run1, pins: pins, @@ -85,11 +76,6 @@ func TestItWritesTheRunsPredicateOrNothing(t *testing.T) { require.NoError(t, json.Unmarshal(got, &p)) assert.Equal(t, "v1", p.BuildDefinition.ExternalParameters.Version) assert.Equal(t, "debian-snapshot", p.BuildDefinition.ResolvedDependencies[1].Name) - builder := tc.builder - if builder == "" { - builder = "https://github.com/spin-stack/machine/.github/workflows/release.yml@refs/heads/main" - } - assert.Equal(t, builder, p.RunDetails.Builder.ID) }) } } diff --git a/fulciotest/fulciotest.go b/fulciotest/fulciotest.go deleted file mode 100644 index 5e4371a..0000000 --- a/fulciotest/fulciotest.go +++ /dev/null @@ -1,271 +0,0 @@ -// Package fulciotest is a Sigstore for tests whose certificates carry what Fulcio writes into a -// GitHub Actions run's: the source repository, its ref, the build's signer and config. sigstore-go's -// VirtualSigstore mints a leaf with the issuer and nothing else, so a verifier that pins the -// source repository - which a reusable builder needs, since any repository may call one and be -// issued its SAN - has no certificate to pass in a test. This one has its own Fulcio CA and keeps -// the VirtualSigstore's Rekor, CT log and timestamp authority. -package fulciotest - -import ( - "context" - "crypto" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/sha256" - "crypto/x509" - "crypto/x509/pkix" - "encoding/asn1" - "encoding/base64" - "encoding/hex" - "errors" - "fmt" - "math/big" - "net/url" - "time" - - "github.com/secure-systems-lab/go-securesystemslib/dsse" - "github.com/sigstore/rekor/pkg/pki" - "github.com/sigstore/rekor/pkg/types" - "github.com/sigstore/rekor/pkg/types/hashedrekord" - _ "github.com/sigstore/rekor/pkg/types/hashedrekord/v0.0.1" // the version types.NewProposedEntry makes - "github.com/sigstore/sigstore-go/pkg/bundle" - "github.com/sigstore/sigstore-go/pkg/fulcio/certificate" - "github.com/sigstore/sigstore-go/pkg/root" - "github.com/sigstore/sigstore-go/pkg/testing/ca" - "github.com/sigstore/sigstore-go/pkg/tlog" - "github.com/sigstore/sigstore-go/pkg/verify" - "github.com/sigstore/sigstore/pkg/cryptoutils" - "github.com/sigstore/sigstore/pkg/signature" - sigdsse "github.com/sigstore/sigstore/pkg/signature/dsse" -) - -// Sigstore is trusted material for a verifier, and what signs and attests under it. -type Sigstore struct { - *ca.VirtualSigstore - fulcio *root.FulcioCertificateAuthority - key *ecdsa.PrivateKey -} - -// New is a Sigstore whose Fulcio is valid from five hours ago to an hour from now, as the -// VirtualSigstore's is. -func New() (*Sigstore, error) { - v, err := ca.NewVirtualSigstore() - if err != nil { - return nil, fmt.Errorf("fulciotest: %w", err) - } - rootCert, rootKey, err := ca.GenerateRootCa() - if err != nil { - return nil, fmt.Errorf("fulciotest: %w", err) - } - inter, key, err := ca.GenerateFulcioIntermediate(rootCert, rootKey) - if err != nil { - return nil, fmt.Errorf("fulciotest: %w", err) - } - now := time.Now() - return &Sigstore{VirtualSigstore: v, key: key, fulcio: &root.FulcioCertificateAuthority{ - Root: rootCert, Intermediates: []*x509.Certificate{inter}, URI: "https://fulciotest.invalid", - ValidityPeriodStart: now.Add(-5 * time.Hour), ValidityPeriodEnd: now.Add(time.Hour), // mutate-exempt: how far back the start is, a test signs within it; only the end is a test's (signed past it) - }}, nil -} - -// FulcioCertificateAuthorities is this Sigstore's own Fulcio, the one that knows the extensions. -func (s *Sigstore) FulcioCertificateAuthorities() []root.CertificateAuthority { - return []root.CertificateAuthority{s.fulcio} -} - -// The extensions a leaf carries, by their OIDs: the issuer twice, as Fulcio writes it - raw in the -// deprecated OID, DER in the current one - and the rest as DER strings. -func render(e certificate.Extensions) ([]pkix.Extension, error) { - out := []pkix.Extension{{Id: certificate.OIDIssuer, Value: []byte(e.Issuer)}} //nolint:staticcheck // Fulcio still writes it, and older verifiers read only it - if e.GithubWorkflowRepository != "" { - // What cosign's --certificate-github-workflow-repository reads, which Fulcio still writes. - out = append(out, pkix.Extension{Id: certificate.OIDGitHubWorkflowRepository, Value: []byte(e.GithubWorkflowRepository)}) //nolint:staticcheck // see above - } - for _, f := range []struct { - id asn1.ObjectIdentifier - val string - }{ - {certificate.OIDIssuerV2, e.Issuer}, - {certificate.OIDBuildSignerURI, e.BuildSignerURI}, - {certificate.OIDBuildSignerDigest, e.BuildSignerDigest}, - {certificate.OIDRunnerEnvironment, e.RunnerEnvironment}, - {certificate.OIDSourceRepositoryURI, e.SourceRepositoryURI}, - {certificate.OIDSourceRepositoryDigest, e.SourceRepositoryDigest}, - {certificate.OIDSourceRepositoryRef, e.SourceRepositoryRef}, - {certificate.OIDBuildConfigURI, e.BuildConfigURI}, - {certificate.OIDBuildTrigger, e.BuildTrigger}, - } { - if f.val == "" { - continue - } - der, err := asn1.MarshalWithParams(f.val, "utf8") - if err != nil { - return nil, err - } - out = append(out, pkix.Extension{Id: f.id, Value: der}) - } - return out, nil -} - -// leaf is a certificate for san - a URI, as Fulcio's for a workflow is - carrying ext, valid from a -// minute before at to ten minutes after it, and its key. -func (s *Sigstore) leaf(san string, ext certificate.Extensions, at time.Time) (*x509.Certificate, *ecdsa.PrivateKey, error) { - u, err := url.Parse(san) - if err != nil || u.Scheme == "" { - return nil, nil, fmt.Errorf("fulciotest: %q is not a URI", san) - } - if ext.Issuer == "" { - return nil, nil, errors.New("fulciotest: a certificate with no issuer") - } - extra, err := render(ext) - if err != nil { - return nil, nil, fmt.Errorf("fulciotest: %w", err) - } - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - return nil, nil, fmt.Errorf("fulciotest: %w", err) - } - tmpl := &x509.Certificate{ - SerialNumber: big.NewInt(at.UnixNano()), URIs: []*url.URL{u}, - NotBefore: at.Add(-time.Minute), NotAfter: at.Add(10 * time.Minute), - KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, - ExtraExtensions: extra, - } - der, err := x509.CreateCertificate(rand.Reader, tmpl, s.fulcio.Intermediates[0], key.Public(), s.key) - if err != nil { - return nil, nil, fmt.Errorf("fulciotest: %w", err) - } - cert, err := x509.ParseCertificate(der) - if err != nil { - return nil, nil, fmt.Errorf("fulciotest: %w", err) - } - return cert, key, nil -} - -// AttestAtTime is an in-toto attestation of body, signed by a certificate for san with ext, logged at -// at, with the log's proof of inclusion when proof. -func (s *Sigstore) AttestAtTime(san string, ext certificate.Extensions, body []byte, at time.Time, proof bool) (verify.SignedEntity, error) { - cert, key, err := s.leaf(san, ext, at) - if err != nil { - return nil, err - } - signer, err := signature.LoadECDSASignerVerifier(key, crypto.SHA256) - if err != nil { - return nil, fmt.Errorf("fulciotest: %w", err) - } - envSigner, err := dsse.NewEnvelopeSigner(&sigdsse.SignerAdapter{SignatureSigner: signer, Pub: key.Public()}) - if err != nil { - return nil, fmt.Errorf("fulciotest: %w", err) - } - env, err := envSigner.SignPayload(context.Background(), "application/vnd.in-toto+json", body) - if err != nil { - return nil, fmt.Errorf("fulciotest: %w", err) - } - sig, err := base64.StdEncoding.DecodeString(env.Signatures[0].Sig) - if err != nil { - return nil, fmt.Errorf("fulciotest: %w", err) - } - entry, err := s.GenerateTlogEntry(cert, env, sig, at.Unix(), proof) - if err != nil { - return nil, fmt.Errorf("fulciotest: %w", err) - } - return &entity{chain: s.chain(cert), envelope: env, entries: []*tlog.Entry{entry}}, nil -} - -// SignAtTime is a signature over artifact by a certificate for san with ext, logged at at, as -// cosign sign-blob's is: a hashedrekord entry with the log's promise of inclusion. -func (s *Sigstore) SignAtTime(san string, ext certificate.Extensions, artifact []byte, at time.Time) (verify.SignedEntity, error) { - cert, key, err := s.leaf(san, ext, at) - if err != nil { - return nil, err - } - digest := sha256.Sum256(artifact) - sig, err := ecdsa.SignASN1(rand.Reader, key, digest[:]) - if err != nil { - return nil, fmt.Errorf("fulciotest: %w", err) - } - entry, err := s.hashedRekord(cert, digest[:], sig, at.Unix()) - if err != nil { - return nil, err - } - return &entity{chain: s.chain(cert), message: bundle.NewMessageSignature(digest[:], "SHA2_256", sig), entries: []*tlog.Entry{entry}}, nil -} - -func (s *Sigstore) chain(leaf *x509.Certificate) []*x509.Certificate { - return []*x509.Certificate{leaf, s.fulcio.Intermediates[0], s.fulcio.Root} -} - -// hashedRekord is the log's entry of a signature over digest by cert, as the VirtualSigstore's own -// is: canonical, signed by its log, at index 1000. -func (s *Sigstore) hashedRekord(cert *x509.Certificate, digest, sig []byte, at int64) (*tlog.Entry, error) { - ctx := context.Background() - pem, err := cryptoutils.MarshalCertificateToPEM(cert) - if err != nil { - return nil, fmt.Errorf("fulciotest: %w", err) - } - proposed, err := types.NewProposedEntry(ctx, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), types.ArtifactProperties{ - PublicKeyBytes: [][]byte{pem}, PKIFormat: string(pki.X509), ArtifactHash: hex.EncodeToString(digest), SignatureBytes: sig, - }) - if err != nil { - return nil, fmt.Errorf("fulciotest: %w", err) - } - impl, err := types.CreateVersionedEntry(proposed) - if err != nil { - return nil, fmt.Errorf("fulciotest: %w", err) - } - body, err := types.CanonicalizeEntry(ctx, impl) - if err != nil { - return nil, fmt.Errorf("fulciotest: %w", err) - } - logID, err := s.RekorLogID() - if err != nil { - return nil, fmt.Errorf("fulciotest: %w", err) - } - const index = 1000 // mutate-exempt: the entry's index in a log with no proof; the promise signs whichever it is - set, err := s.RekorSignPayload(tlog.RekorPayload{LogID: logID, IntegratedTime: at, LogIndex: index, Body: base64.StdEncoding.EncodeToString(body)}) - if err != nil { - return nil, fmt.Errorf("fulciotest: %w", err) - } - raw, err := hex.DecodeString(logID) - if err != nil { - return nil, fmt.Errorf("fulciotest: %w", err) - } - return tlog.NewEntry(body, at, index, raw, set, nil) //nolint:staticcheck // the constructor the VirtualSigstore uses for the same entry -} - -// entity is a signed thing as a verifier reads it out of a bundle. -type entity struct { - chain []*x509.Certificate - envelope *dsse.Envelope - message *bundle.MessageSignature - entries []*tlog.Entry -} - -func (e *entity) VerificationContent() (verify.VerificationContent, error) { - return bundle.NewCertificate(e.chain[0]), nil -} - -func (e *entity) SignatureContent() (verify.SignatureContent, error) { - if e.envelope != nil { - return &bundle.Envelope{Envelope: e.envelope}, nil - } - return e.message, nil -} - -func (e *entity) HasInclusionPromise() bool { return true } - -func (e *entity) HasInclusionProof() bool { - for _, t := range e.entries { - if t.HasInclusionProof() { - return true - } - } - return false -} - -func (e *entity) Timestamps() ([][]byte, error) { return nil, nil } - -func (e *entity) TlogEntries() ([]*tlog.Entry, error) { return e.entries, nil } - -func (e *entity) Version() (string, error) { return "v0.3", nil } diff --git a/fulciotest/fulciotest_test.go b/fulciotest/fulciotest_test.go deleted file mode 100644 index 82ba0d4..0000000 --- a/fulciotest/fulciotest_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package fulciotest_test - -import ( - "crypto/sha256" - "encoding/hex" - "encoding/json" - "testing" - "time" - - "github.com/sigstore/sigstore-go/pkg/fulcio/certificate" - "github.com/sigstore/sigstore-go/pkg/verify" - "github.com/stretchr/testify/require" - - "github.com/spin-stack/go-tools/fulciotest" -) - -const ( - issuer = "https://token.actions.githubusercontent.com" - builder = "https://github.com/spin-stack/spin-machine/.github/workflows/build-release.yml@refs/heads/main" -) - -var own = certificate.Extensions{ - Issuer: issuer, SourceRepositoryURI: "https://github.com/spin-stack/spin-machine", SourceRepositoryRef: "refs/heads/main", - BuildSignerURI: builder, BuildConfigURI: "https://github.com/spin-stack/spin-machine/.github/workflows/release.yml@refs/heads/main", - GithubWorkflowRepository: "spin-stack/spin-machine", -} - -// statement is an in-toto statement over artifact. -func statement(t *testing.T, artifact []byte) []byte { - t.Helper() - sum := sha256.Sum256(artifact) - b, err := json.Marshal(map[string]any{ - "_type": "https://in-toto.io/Statement/v1", - "subject": []map[string]any{{"name": "f", "digest": map[string]string{"sha256": hexOf(sum[:])}}}, - "predicateType": "https://slsa.dev/provenance/v1", - "predicate": map[string]any{}, - }) - require.NoError(t, err) - return b -} - -func hexOf(b []byte) string { return hex.EncodeToString(b) } - -// What a verifier pinning the source repository and the build's config takes: a signature and an -// attestation whose certificate says so. One from another repository with the builder's SAN - what -// any repository calling a public reusable builder is issued - is refused, and so is one that says -// nothing of its source. -func TestACertificateCarriesWhatFulcioWrites(t *testing.T) { - s, err := fulciotest.New() - require.NoError(t, err) - artifact := []byte("the machine") - sum := sha256.Sum256(artifact) - now := time.Now() - pinned, err := verify.NewShortCertificateIdentity(issuer, "", builder, "") - require.NoError(t, err) - pinned.SourceRepositoryURI = own.SourceRepositoryURI - pinned.BuildConfigURI = own.BuildConfigURI - verifier, err := verify.NewVerifier(s, verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1)) - require.NoError(t, err) - check := func(e verify.SignedEntity) error { - result, err := verifier.Verify(e, verify.NewPolicy(verify.WithArtifactDigest("sha256", sum[:]), verify.WithCertificateIdentity(pinned))) - if err == nil { - // What cosign's --certificate-github-workflow-repository reads, beside what the policy did. - require.Equal(t, own.GithubWorkflowRepository, result.Signature.Certificate.GithubWorkflowRepository) - require.Equal(t, own.BuildSignerURI, result.Signature.Certificate.BuildSignerURI) - } - return err - } - - other := own - other.SourceRepositoryURI, other.GithubWorkflowRepository = "https://github.com/someone/fork", "someone/fork" - none := certificate.Extensions{Issuer: issuer} - for name, tc := range map[string]struct { - ext certificate.Extensions - ok bool - }{ - "its own repository": {ext: own, ok: true}, - "another repository": {ext: other}, - "nothing of a source": {ext: none}, - } { - t.Run(name, func(t *testing.T) { - signed, err := s.SignAtTime(builder, tc.ext, artifact, now) - require.NoError(t, err) - attested, err := s.AttestAtTime(builder, tc.ext, statement(t, artifact), now, true) - require.NoError(t, err) - require.True(t, attested.HasInclusionProof()) - require.False(t, signed.HasInclusionProof()) - for what, e := range map[string]verify.SignedEntity{"signature": signed, "attestation": attested} { - if tc.ok { - require.NoError(t, check(e), what) - } else { - require.Error(t, check(e), what) - } - } - }) - } - - // The SAN is what it was asked for; a certificate needs an issuer and a URI to be for. - signed, err := s.SignAtTime("https://github.com/spin-stack/spin-machine/.github/workflows/release.yml@refs/heads/main", own, artifact, now) - require.NoError(t, err) - require.Error(t, check(signed)) - _, err = s.SignAtTime("not a uri", own, artifact, now) - require.Error(t, err) - _, err = s.AttestAtTime(builder, certificate.Extensions{}, statement(t, artifact), now, true) - require.Error(t, err) - // Signed at a time its certificate was not yet valid, or its Fulcio was not: refused. - late, err := s.SignAtTime(builder, own, artifact, now.Add(3*time.Hour)) - require.NoError(t, err) - require.Error(t, check(late)) -} diff --git a/go.mod b/go.mod index 62d5afc..070090b 100644 --- a/go.mod +++ b/go.mod @@ -7,9 +7,6 @@ toolchain go1.27.2 require ( github.com/CycloneDX/cyclonedx-go v0.12.0 github.com/in-toto/attestation v1.2.0 - github.com/secure-systems-lab/go-securesystemslib v0.11.0 - github.com/sigstore/rekor v1.5.3 - github.com/sigstore/sigstore v1.10.8 github.com/sigstore/sigstore-go v1.3.0 github.com/stretchr/testify v1.12.1 go.yaml.in/yaml/v3 v3.0.5 @@ -26,7 +23,6 @@ require ( github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect - github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.25.5 // indirect @@ -58,22 +54,17 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect github.com/in-toto/in-toto-golang v0.11.0 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b // indirect - github.com/letsencrypt/boulder v0.20260309.0 // indirect github.com/oklog/ulid/v2 v2.1.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/sassoftware/relic v7.2.1+incompatible // indirect + github.com/secure-systems-lab/go-securesystemslib v0.11.0 // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/sigstore/protobuf-specs v0.5.1 // indirect + github.com/sigstore/rekor v1.5.3 // indirect github.com/sigstore/rekor-tiles/v2 v2.3.0 // indirect + github.com/sigstore/sigstore v1.10.8 // indirect github.com/sigstore/timestamp-authority/v2 v2.1.3 // indirect - github.com/spf13/cobra v1.10.2 // indirect - github.com/spf13/pflag v1.0.10 // indirect - github.com/theupdateframework/go-tuf v0.7.0 // indirect github.com/theupdateframework/go-tuf/v2 v2.4.2 // indirect - github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect github.com/transparency-dev/formats v0.1.1 // indirect github.com/transparency-dev/merkle v0.0.2 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect @@ -83,14 +74,14 @@ require ( go.opentelemetry.io/otel/trace v1.44.0 // indirect golang.org/x/crypto v0.57.0 // indirect golang.org/x/mod v0.41.0 // indirect - golang.org/x/net v0.60.0 // indirect + golang.org/x/net v0.59.0 // indirect golang.org/x/sync v0.23.0 // indirect golang.org/x/sys v0.48.0 // indirect golang.org/x/telemetry v0.0.0-20260924152758-ed294f943157 // indirect golang.org/x/term v0.46.0 // indirect golang.org/x/text v0.42.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/grpc v1.83.1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 // indirect + google.golang.org/grpc v1.82.1 // indirect k8s.io/klog/v2 v2.140.0 // indirect ) diff --git a/go.sum b/go.sum index 441c08f..5741484 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,6 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.42.3 h1:ErklX/7uhSbkAAeyQD/Y1OoQ9hO3 github.com/aws/aws-sdk-go-v2/service/sts v1.42.3/go.mod h1:ULe4HCzfKPiR6R3HEurE3b1upEkuk8AkMrOKtaOxKO8= github.com/aws/smithy-go v1.26.0 h1:9ouqbi+NyKP7fV3Te7UElCwdAb6Y8uk7LGwPE5tVe/s= github.com/aws/smithy-go v1.26.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= @@ -82,7 +80,6 @@ github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUo github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q= github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= @@ -173,8 +170,6 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.21.7 h1:/vPFuVXDjtFREsVArW+0h1CIl5urnOhzei4X2DMW9IU= github.com/google/go-containerregistry v0.21.7/go.mod h1:kjSbt7/zMsKLWfnHrIvKvhXHUw91jbe9DNjPPJ32gXE= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/trillian v1.7.3 h1:hziW+vo4czis48tzx2GK5xRBl/ZxBA9B0/UR5avXOro= @@ -221,12 +216,6 @@ github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b h1:ZGiXF8sz7P github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b/go.mod h1:hQmNrgofl+IY/8L+n20H6E6PWBBTokdsv+q49j0QhsU= github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY= github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4= -github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs= -github.com/jmhodges/clock v1.2.0/go.mod h1:qKjhA7x7u/lQpPB1XAqX1b1lCI/w3/fNuYpI/ZjLynI= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/letsencrypt/boulder v0.20260309.0 h1:kZynrxK3QfqLGx6hhoz+Rfs3hgltJs1p9Mp+4+VwnY0= @@ -235,8 +224,6 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= @@ -250,17 +237,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= -github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= -github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= -github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= -github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= -github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/sassoftware/relic v7.2.1+incompatible h1:Pwyh1F3I0r4clFJXkSI8bOyJINGqpgjJU3DYAZeI05A= @@ -295,7 +271,6 @@ github.com/sigstore/timestamp-authority/v2 v2.1.3 h1:Fc+LjCTfik1lh3YLkaosENfkXa3 github.com/sigstore/timestamp-authority/v2 v2.1.3/go.mod h1:myoFOKJB/u5vNTFwvBBJVkG3NnOBeIJevbfjNeasLjo= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= -github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= @@ -356,15 +331,14 @@ go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= -golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= -golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= +golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= @@ -395,15 +369,12 @@ google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgn google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y= -google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 h1:PvEgGJf9C/1u5CHkInMg7UFYYUoiaQmW2LbtH0pjB78= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= diff --git a/provenance/provenance.go b/provenance/provenance.go index 08f8d67..dd472f3 100644 --- a/provenance/provenance.go +++ b/provenance/provenance.go @@ -137,19 +137,6 @@ func Build(env func(string) string, version string, pins *versions.Versions, ext }, nil } -// BuiltBy is p built by the reusable workflow at path (.github/workflows/) of the same -// repository, at the same ref: what the run's certificate names when the steps that sign are that -// workflow's. GitHub sets GITHUB_WORKFLOW_REF to the caller in a called workflow too, so the run -// cannot say this itself; the caller stays the build's workflow, which is what was dispatched. -func (p Predicate) BuiltBy(path string) (Predicate, error) { - if !strings.HasPrefix(path, ".github/workflows/") || strings.Contains(path, "@") { - return Predicate{}, fmt.Errorf("provenance: builder %q is not a workflow file of the repository", path) - } - w := p.BuildDefinition.ExternalParameters.Workflow - p.RunDetails.Builder.ID = w.Repository + "/" + path + "@" + w.Ref - return p, nil -} - // Pinned is a versions.yaml entry as an input: its name, where it is, and the digest that holds // it - none for a module, which the checksum database holds by its version, or a date. func Pinned(e versions.Entry) ResourceDescriptor { diff --git a/provenance/provenance_test.go b/provenance/provenance_test.go index 5cf5b15..f3700af 100644 --- a/provenance/provenance_test.go +++ b/provenance/provenance_test.go @@ -96,23 +96,6 @@ func TestARunsPredicateIsTheGolden(t *testing.T) { assert.Equal(t, p, again) } -// Built by a reusable workflow, the builder is that workflow at the run's ref, and the dispatched -// one stays the build's workflow; a builder that is not a workflow file of the repository is no -// builder. -func TestABuilderIsTheReusableWorkflowAtTheRunsRef(t *testing.T) { - p, err := provenance.Build(getenv(run), "v20261013.1", nil, nil) - require.NoError(t, err) - built, err := p.BuiltBy(".github/workflows/build-release.yml") - require.NoError(t, err) - assert.Equal(t, "https://github.com/spin-stack/spin/.github/workflows/build-release.yml@refs/heads/main", built.RunDetails.Builder.ID) - assert.Equal(t, ".github/workflows/release.yml", built.BuildDefinition.ExternalParameters.Workflow.Path) - assert.Equal(t, "https://github.com/spin-stack/spin/.github/workflows/release.yml@refs/heads/main", p.RunDetails.Builder.ID) - for _, path := range []string{"hack/build.yml", "build-release.yml", ".github/workflows/build-release.yml@refs/heads/x"} { - _, err := p.BuiltBy(path) - assert.Error(t, err, path) - } -} - // An environment that is not a workflow run's, or a workflow ref that is not the repository's, // is refused: provenance with no builder in it names nobody. func TestAnEnvironmentThatIsNotARunIsRefused(t *testing.T) {