diff --git a/.github/config-release.yml b/.github/config-release.yml index 6b681c1..d14ac58 100644 --- a/.github/config-release.yml +++ b/.github/config-release.yml @@ -11,4 +11,7 @@ flags: goos: linux goarch: amd64 binary: slsa-verifier-{{ .Os }}-{{ .Arch }} -dir: ./cli/slsa-verifier \ No newline at end of file +dir: ./cli/slsa-verifier + +ldflags: + - "-X version.Version={{ .Version }}" diff --git a/.github/workflows/pre-submit.yml b/.github/workflows/pre-submit.yml index b66930b..71a1c1e 100644 --- a/.github/workflows/pre-submit.yml +++ b/.github/workflows/pre-submit.yml @@ -22,10 +22,10 @@ jobs: go mod vendor # Build cli - go build -mod=vendor -o slsa-verifier ./cli/slsa-verifier/main.go + go build -mod=vendor -o slsa-verifier ./cli/slsa-verifier/ # Builder service - go build -mod=vendor -o service ./cli/experimental/service/main.go + go build -mod=vendor -o service ./cli/experimental/service/ # Tests go test -mod=vendor -v ./... diff --git a/README.md b/README.md index 5e33e6e..cc33ab3 100644 --- a/README.md +++ b/README.md @@ -49,34 +49,36 @@ $ sha256sum -c --strict SHA256SUM.md ## Verification of Provenance +We currently support artifact verification (for binary blobs) and container images. + ### Available options -Below is a list of options currently supported. Note that signature verification is handled seamlessly without the need for developers to manipulate public keys. +Below is a list of options currently supported for binary blobs and container images. Note that signature verification is handled seamlessly without the need for developers to manipulate public keys. See [Available options](#available-options) for details on the options exposed to validate the provenance. ```bash $ git clone git@github.com:slsa-framework/slsa-verifier.git -$ go run ./cli/slsa-verifier --help - Usage of ./slsa-verifier: - -artifact-path string - path to an artifact to verify - -branch string - expected branch the binary was compiled from (default "main") - -print-provenance - output the verified provenance - -provenance string - path to a provenance file - -source string - expected source repository that should have produced the binary, e.g. github.com/some/repo - -tag string - [optional] expected tag the binary was compiled from - -versioned-tag string - [optional] expected version the binary was compiled from. Uses semantic version to match the tag +$ go run ./cli/slsa-verifier/ verify-artifact --help +Verifies SLSA provenance on an artifact blob + +Usage: + slsa-verifier verify-artifact [flags] + +Flags: + --build-workflow-input map[] [optional] a workflow input provided by a user at trigger time in the format 'key=value'. (Only for 'workflow_dispatch' events). (default map[]) + --builder-id string EXPERIMENTAL: the unique builder ID who created the provenance + -h, --help help for verify-artifact + --print-provenance print the verified provenance to stdout + --provenance-path string path to a provenance file + --source-branch string [optional] expected branch the binary was compiled from + --source-tag string [optional] expected tag the binary was compiled from + --source-uri string expected source repository that should have produced the binary, e.g. github.com/some/repo + --source-versioned-tag string [optional] expected version the binary was compiled from. Uses semantic version to match the tag ``` ### Example ```bash -$ go run ./cli/slsa-verifier -artifact-path ~/Downloads/slsa-verifier-linux-amd64 -provenance ~/Downloads/slsa-verifier-linux-amd64.intoto.jsonl -source github.com/slsa-framework/slsa-verifier -tag v1.3.0 +$ go run ./cli/slsa-verifier -provenance-path ~/Downloads/slsa-verifier-linux-amd64.intoto.jsonl --source-uri github.com/slsa-framework/slsa-verifier --source-tag v1.3.0 ~/Downloads/slsa-verifier-linux-amd64 Verified signature against tlog entry index 3189970 at URL: https://rekor.sigstore.dev/api/v1/log/entries/206071d5ca7a2346e4db4dcb19a648c7f13b4957e655f4382b735894059bd199 Verified build using builder https://github.com/slsa-framework/slsa-github-generator/.github/workflows/builder_go_slsa3.yml@refs/tags/v1.2.0 at commit 5bb13ef508b2b8ded49f9264d7712f1316830d10 PASSED: Verified SLSA provenance @@ -84,6 +86,19 @@ PASSED: Verified SLSA provenance The verified in-toto statement may be written to stdout with the `--print-provenance` flag to pipe into policy engines. +### Options Details + +The following options are supported for [SLSA GitHub builders and generators](https://github.com/slsa-framework/slsa-github-generator#generation-of-provenance): + +| Option | Description | +| --- | ----------- | +| `source-uri` | Expects a source, for e.g. `github.com/org/repo`. | +| `source-branch` | Expects a `branch` like `main` or `dev`. Not supported for all GitHub Workflow triggers. | +| `source-tag` | Expects a `tag` like `v0.0.1`. Verifies exact tag used to create the binary. NSupported for new [tag](https://github.com/slsa-framework/example-package/blob/main/.github/workflows/e2e.go.tag.main.config-ldflags-assets-tag.slsa3.yml#L5) and [release](https://github.com/slsa-framework/example-package/blob/main/.github/workflows/e2e.go.release.main.config-ldflags-assets-tag.slsa3.yml) triggers. | +| `source-versioned-tag` | Like `tag`, but verifies using semantic versioning. | +| `build-workflow-input` | Expects key-value pairs like `key=value` to match against [inputs](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onworkflow_dispatchinputs) for GitHub Actions `workflow_dispatch` triggers. | + + ## Technical design ### Blog post diff --git a/cli/slsa-verifier/main.go b/cli/slsa-verifier/main.go index 564bf76..baefbce 100644 --- a/cli/slsa-verifier/main.go +++ b/cli/slsa-verifier/main.go @@ -1,202 +1,42 @@ package main import ( - "context" - "crypto/sha256" - "encoding/hex" - "flag" + "errors" "fmt" - "io" "os" - "strings" - serrors "github.com/slsa-framework/slsa-verifier/errors" - - "github.com/slsa-framework/slsa-verifier/options" - "github.com/slsa-framework/slsa-verifier/verifiers" - "github.com/slsa-framework/slsa-verifier/verifiers/container" + "github.com/spf13/cobra" ) -type workflowInputs struct { - kv map[string]string +func check(err error) { + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } } -var ( - provenancePath string - builderID string - artifactPath string - artifactImage string - source string - branch string - tag string - versiontag string - inputs workflowInputs - printProvenance bool -) - -func experimentalEnabled() bool { +func ExperimentalEnabled() bool { return os.Getenv("SLSA_VERIFIER_EXPERIMENTAL") == "1" } -func (i *workflowInputs) String() string { - return fmt.Sprintf("%v", i.kv) -} - -func (i *workflowInputs) Set(value string) error { - l := strings.Split(value, "=") - if len(l) != 2 { - return fmt.Errorf("%w: expected 'key=value' format, got '%s'", serrors.ErrorInvalidFormat, value) +func rootCmd() *cobra.Command { + c := &cobra.Command{ + Use: "slsa-verifier", + Short: "Verify SLSA provenance for Github Actions", + Long: `Verify SLSA provenance for Github Actions. +For more information on SLSA, visit https://slsa.dev`, + RunE: func(cmd *cobra.Command, args []string) error { + return errors.New("expected command") + }, } - i.kv[l[0]] = l[1] - return nil -} - -func (i *workflowInputs) AsMap() map[string]string { - return i.kv + c.AddCommand(versionCmd()) + c.AddCommand(verifyArtifactCmd()) + c.AddCommand(verifyImageCmd()) + // We print our own errors and usage in the check function. + c.SilenceErrors = true + return c } func main() { - if experimentalEnabled() { - flag.StringVar(&builderID, "builder-id", "", "EXPERIMENTAL: the unique builder ID who created the provenance") - } - flag.StringVar(&provenancePath, "provenance", "", "path to a provenance file") - flag.StringVar(&artifactPath, "artifact-path", "", "path to an artifact to verify") - flag.StringVar(&artifactImage, "artifact-image", "", "name of the OCI image to verify") - flag.StringVar(&source, "source", "", - "expected source repository that should have produced the binary, e.g. github.com/some/repo") - flag.StringVar(&branch, "branch", "", "[optional] expected branch the binary was compiled from") - flag.StringVar(&tag, "tag", "", "[optional] expected tag the binary was compiled from") - flag.StringVar(&versiontag, "versioned-tag", "", - "[optional] expected version the binary was compiled from. Uses semantic version to match the tag") - flag.BoolVar(&printProvenance, "print-provenance", false, - "print the verified provenance to std out") - inputs.kv = make(map[string]string) - flag.Var(&inputs, "workflow-input", - "[optional] a workflow input provided by a user at trigger time in the format 'key=value'. (Only for 'workflow_dispatch' events).") - flag.Parse() - - if artifactImage != "" && artifactPath != "" { - fmt.Fprintf(os.Stderr, "'artifact-image' and 'artifact-path' cannot be specified together\n") - flag.Usage() - os.Exit(1) - } - - if source == "" { - flag.Usage() - os.Exit(1) - } - - var pbuilderID, pbranch, ptag, pversiontag *string - - // Note: nil tag, version-tag and builder-id means we ignore them during verification. - if isFlagPassed("tag") { - ptag = &tag - } - if isFlagPassed("versioned-tag") { - pversiontag = &versiontag - } - if experimentalEnabled() && isFlagPassed("builder-id") { - pbuilderID = &builderID - } - if isFlagPassed("branch") { - pbranch = &branch - } - - if ptag != nil && pversiontag != nil { - fmt.Fprintf(os.Stderr, "'version' and 'tag' options cannot be used together\n") - os.Exit(1) - } - - verifiedProvenance, _, err := runVerify(artifactImage, artifactPath, provenancePath, source, - pbranch, pbuilderID, ptag, pversiontag, inputs.AsMap(), nil) - if err != nil { - fmt.Fprintf(os.Stderr, "FAILED: SLSA verification failed: %v\n", err) - os.Exit(2) - } - - fmt.Fprintf(os.Stderr, "PASSED: Verified SLSA provenance\n") - if printProvenance { - fmt.Fprintf(os.Stdout, "%s\n", string(verifiedProvenance)) - } -} - -func isFlagPassed(name string) bool { - found := false - flag.Visit(func(f *flag.Flag) { - if f.Name == name { - found = true - } - }) - return found -} - -type ComputeDigestFn func(string) (string, error) - -func runVerify(artifactImage, artifactPath, provenancePath, source string, - branch, builderID, ptag, pversiontag *string, inputs map[string]string, - fn ComputeDigestFn, -) ([]byte, string, error) { - ctx := context.Background() - - // Artifact hash retrieval depends on the artifact type. - artifactHash, err := getArtifactHash(artifactImage, artifactPath, fn) - if err != nil { - return nil, "", err - } - - provenanceOpts := &options.ProvenanceOpts{ - ExpectedSourceURI: source, - ExpectedBranch: branch, - ExpectedDigest: artifactHash, - ExpectedVersionedTag: pversiontag, - ExpectedTag: ptag, - ExpectedWorkflowInputs: inputs, - } - - builderOpts := &options.BuilderOpts{ - ExpectedID: builderID, - } - - var provenance []byte - if provenancePath != "" { - provenance, err = os.ReadFile(provenancePath) - if err != nil { - return nil, "", err - } - } - - return verifiers.Verify(ctx, artifactImage, provenance, artifactHash, provenanceOpts, builderOpts) -} - -func getArtifactHash(artifactImage, artifactPath string, - // This function is used to handle unit tests and adapt - // digest computation to local images. - fn ComputeDigestFn, -) (string, error) { - if artifactPath != "" { - f, err := os.Open(artifactPath) - if err != nil { - return "", err - } - defer f.Close() - h := sha256.New() - if _, err := io.Copy(h, f); err != nil { - return "", err - } - return hex.EncodeToString(h.Sum(nil)), nil - } - // Retrieve the image digest. - if fn == nil { - fn = container.GetImageDigest - } - digest, err := fn(artifactImage) - if err != nil { - return "", err - } - - // Verify that the reference is immutable. - if err := container.ValidateArtifactReference(artifactImage, digest); err != nil { - return "", err - } - return digest, nil + check(rootCmd().Execute()) } diff --git a/cli/slsa-verifier/main_test.go b/cli/slsa-verifier/main_test.go index eb86194..d93ae5c 100644 --- a/cli/slsa-verifier/main_test.go +++ b/cli/slsa-verifier/main_test.go @@ -18,6 +18,7 @@ import ( "github.com/sigstore/cosign/pkg/oci" "github.com/sigstore/cosign/pkg/oci/layout" + "github.com/slsa-framework/slsa-verifier/cli/slsa-verifier/verify" serrors "github.com/slsa-framework/slsa-verifier/errors" "github.com/slsa-framework/slsa-verifier/verifiers/container" ) @@ -486,13 +487,20 @@ func Test_runVerifyArtifactPath(t *testing.T) { } for _, v := range checkVersions { - artifactPath := filepath.Clean(filepath.Join(TEST_DIR, v, tt.artifact)) provenancePath := fmt.Sprintf("%s.intoto.jsonl", artifactPath) - _, outBuilderID, err := runVerify("", artifactPath, - provenancePath, - tt.source, tt.pbranch, tt.pbuilderID, - tt.ptag, tt.pversiontag, tt.inputs, nil) + + cmd := verify.VerifyArtifactCommand{ + ProvenancePath: provenancePath, + SourceURI: tt.source, + SourceBranch: tt.pbranch, + BuilderID: tt.pbuilderID, + SourceTag: tt.ptag, + SourceVersionTag: tt.pversiontag, + BuildWorkflowInputs: tt.inputs, + } + + outBuilderID, err := cmd.Exec(context.Background(), []string{artifactPath}) if !errCmp(err, tt.err) { t.Errorf(cmp.Diff(err, tt.err, cmpopts.EquateErrors())) @@ -505,6 +513,7 @@ func Test_runVerifyArtifactPath(t *testing.T) { if tt.outBuilderID != "" && outBuilderID != tt.outBuilderID { t.Errorf(cmp.Diff(outBuilderID, tt.outBuilderID)) } + } }) } @@ -646,9 +655,16 @@ func Test_runVerifyGHAArtifactImage(t *testing.T) { for _, v := range checkVersions { image := filepath.Clean(filepath.Join(TEST_DIR, v, tt.artifact)) - _, outBuilderID, err := runVerify(image, "", "", - tt.source, tt.pbranch, tt.pbuilderID, - tt.ptag, tt.pversiontag, nil, localDigestComputeFn) + cmd := verify.VerifyImageCommand{ + SourceURI: tt.source, + SourceBranch: tt.pbranch, + BuilderID: tt.pbuilderID, + SourceTag: tt.ptag, + SourceVersionTag: tt.pversiontag, + DigestFn: localDigestComputeFn, + } + + outBuilderID, err := cmd.Exec(context.Background(), []string{image}) if !errCmp(err, tt.err) { t.Errorf(cmp.Diff(err, tt.err, cmpopts.EquateErrors())) @@ -840,15 +856,23 @@ func Test_runVerifyGCBArtifactImage(t *testing.T) { for _, v := range checkVersions { provenance := filepath.Clean(filepath.Join(TEST_DIR, v, tt.provenance)) image := tt.artifact - var fn ComputeDigestFn + var fn verify.ComputeDigestFn if !tt.oci { image = filepath.Clean(filepath.Join(TEST_DIR, v, image)) fn = localDigestComputeFn } - _, outBuilderID, err := runVerify(image, "", provenance, - tt.source, nil, tt.pbuilderID, - nil, nil, nil, fn) + cmd := verify.VerifyImageCommand{ + SourceURI: tt.source, + SourceBranch: nil, + BuilderID: tt.pbuilderID, + SourceTag: nil, + SourceVersionTag: nil, + DigestFn: fn, + ProvenancePath: &provenance, + } + + outBuilderID, err := cmd.Exec(context.Background(), []string{image}) if !errCmp(err, tt.err) { t.Errorf(cmp.Diff(err, tt.err, cmpopts.EquateErrors())) @@ -861,6 +885,7 @@ func Test_runVerifyGCBArtifactImage(t *testing.T) { if tt.outBuilderID != "" && outBuilderID != tt.outBuilderID { t.Errorf(cmp.Diff(outBuilderID, tt.outBuilderID)) } + } }) } diff --git a/cli/slsa-verifier/verify.go b/cli/slsa-verifier/verify.go new file mode 100644 index 0000000..1962ed1 --- /dev/null +++ b/cli/slsa-verifier/verify.go @@ -0,0 +1,130 @@ +// Copyright 2022 SLSA Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "errors" + "fmt" + "os" + + "github.com/slsa-framework/slsa-verifier/cli/slsa-verifier/verify" + "github.com/spf13/cobra" +) + +const ( + SUCCESS = "PASSED: Verified SLSA provenance" + FAILURE = "FAILED: SLSA verification failed" +) + +func verifyArtifactCmd() *cobra.Command { + o := &verify.VerifyOptions{} + + cmd := &cobra.Command{ + Use: "verify-artifact", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return errors.New("expects a single path to an artifact") + } + return nil + }, + Short: "Verifies SLSA provenance on an artifact blob", + RunE: func(cmd *cobra.Command, args []string) error { + v := verify.VerifyArtifactCommand{ + ProvenancePath: o.ProvenancePath, + SourceURI: o.SourceURI, + PrintProvenance: o.PrintProvenance, + BuildWorkflowInputs: o.BuildWorkflowInputs.AsMap(), + } + if cmd.Flags().Changed("source-branch") { + v.SourceTag = &o.SourceBranch + } + if cmd.Flags().Changed("source-tag") { + v.SourceTag = &o.SourceTag + } + if cmd.Flags().Changed("source-versioned-tag") { + v.SourceVersionTag = &o.SourceVersionTag + } + if cmd.Flags().Changed("builder-id") { + if !ExperimentalEnabled() { + return fmt.Errorf("builder-id only supported with experimental flag") + } + v.BuilderID = &o.BuilderID + } + + if _, err := v.Exec(cmd.Context(), args); err != nil { + fmt.Fprintf(os.Stderr, "%s: %v\n", FAILURE, err) + return err + } + + fmt.Fprintf(os.Stderr, "%s\n", SUCCESS) + return nil + }, + } + + o.AddFlags(cmd) + cmd.MarkFlagRequired("provenance-path") + return cmd +} + +func verifyImageCmd() *cobra.Command { + o := &verify.VerifyOptions{} + + cmd := &cobra.Command{ + Use: "verify-image", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return errors.New("expects a single path to an image") + } + return nil + }, + Short: "Verifies SLSA provenance on a container image", + RunE: func(cmd *cobra.Command, args []string) error { + v := verify.VerifyImageCommand{ + SourceURI: o.SourceURI, + PrintProvenance: o.PrintProvenance, + BuildWorkflowInputs: o.BuildWorkflowInputs.AsMap(), + } + if cmd.Flags().Changed("provenance-path") { + v.ProvenancePath = &o.ProvenancePath + } + if cmd.Flags().Changed("source-branch") { + v.SourceTag = &o.SourceBranch + } + if cmd.Flags().Changed("source-tag") { + v.SourceTag = &o.SourceTag + } + if cmd.Flags().Changed("source-versioned-tag") { + v.SourceVersionTag = &o.SourceVersionTag + } + if cmd.Flags().Changed("builder-id") { + if !ExperimentalEnabled() { + return fmt.Errorf("builder-id only supported with experimental flag") + } + v.BuilderID = &o.BuilderID + } + + if _, err := v.Exec(cmd.Context(), args); err != nil { + fmt.Fprintf(os.Stderr, "%s: %v\n", FAILURE, err) + return err + } + + fmt.Fprintf(os.Stderr, "%s\n", SUCCESS) + return nil + }, + } + + o.AddFlags(cmd) + return cmd +} diff --git a/cli/slsa-verifier/verify/options.go b/cli/slsa-verifier/verify/options.go new file mode 100644 index 0000000..43d623d --- /dev/null +++ b/cli/slsa-verifier/verify/options.go @@ -0,0 +1,100 @@ +// Copyright 2022 SLSA Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package verify + +import ( + "fmt" + "strings" + + serrors "github.com/slsa-framework/slsa-verifier/errors" + "github.com/spf13/cobra" +) + +type Interface interface { + // AddFlags adds this options' flags to the cobra command. + AddFlags(cmd *cobra.Command) +} + +// VerifyOptions is the top-level options for all `verify` commands. +type VerifyOptions struct { + /* Source requirements */ + SourceURI string + SourceBranch string + SourceTag string + SourceVersionTag string + /* Builder Requirements */ + BuildWorkflowInputs workflowInputs + BuilderID string + /* Other */ + ProvenancePath string + PrintProvenance bool +} + +var _ Interface = (*VerifyOptions)(nil) + +// AddFlags implements Interface +func (o *VerifyOptions) AddFlags(cmd *cobra.Command) { + /* Builder options */ + cmd.Flags().Var(&o.BuildWorkflowInputs, "build-workflow-input", + "[optional] a workflow input provided by a user at trigger time in the format 'key=value'. (Only for 'workflow_dispatch' events on GitHub Actions).") + + cmd.Flags().StringVar(&o.BuilderID, "builder-id", "", "EXPERIMENTAL: the unique builder ID who created the provenance") + + /* Source options */ + cmd.Flags().StringVar(&o.SourceURI, "source-uri", "", + "expected source repository that should have produced the binary, e.g. github.com/some/repo") + + cmd.Flags().StringVar(&o.SourceBranch, "source-branch", "", "[optional] expected branch the binary was compiled from") + + cmd.Flags().StringVar(&o.SourceTag, "source-tag", "", "[optional] expected tag the binary was compiled from") + + cmd.Flags().StringVar(&o.SourceVersionTag, "source-versioned-tag", "", + "[optional] expected version the binary was compiled from. Uses semantic version to match the tag") + + /* Other options */ + cmd.Flags().StringVar(&o.ProvenancePath, "provenance-path", "", + "path to a provenance file") + + cmd.Flags().BoolVar(&o.PrintProvenance, "print-provenance", false, + "print the verified provenance to stdout") + + cmd.MarkFlagRequired("source-uri") + cmd.MarkFlagsMutuallyExclusive("source-versioned-tag", "source-tag") +} + +type workflowInputs struct { + kv map[string]string +} + +func (i *workflowInputs) Type() string { + return fmt.Sprintf("%v", i.kv) +} + +func (i *workflowInputs) String() string { + return fmt.Sprintf("%v", i.kv) +} + +func (i *workflowInputs) Set(value string) error { + l := strings.Split(value, "=") + if len(l) != 2 { + return fmt.Errorf("%w: expected 'key=value' format, got '%s'", serrors.ErrorInvalidFormat, value) + } + i.kv[l[0]] = l[1] + return nil +} + +func (i *workflowInputs) AsMap() map[string]string { + return i.kv +} diff --git a/cli/slsa-verifier/verify/verify_artifact.go b/cli/slsa-verifier/verify/verify_artifact.go new file mode 100644 index 0000000..6c40e58 --- /dev/null +++ b/cli/slsa-verifier/verify/verify_artifact.go @@ -0,0 +1,88 @@ +// Copyright 2022 SLSA Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package verify + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "os" + + "github.com/slsa-framework/slsa-verifier/options" + "github.com/slsa-framework/slsa-verifier/verifiers" +) + +// Note: nil branch, tag, version-tag and builder-id means we ignore them during verification. +type VerifyArtifactCommand struct { + ProvenancePath string + BuilderID *string + SourceURI string + SourceBranch *string + SourceTag *string + SourceVersionTag *string + BuildWorkflowInputs map[string]string + PrintProvenance bool +} + +func (c *VerifyArtifactCommand) Exec(ctx context.Context, artifacts []string) (string, error) { + artifactHash, err := getArtifactHash(artifacts[0]) + if err != nil { + return "", err + } + + provenanceOpts := &options.ProvenanceOpts{ + ExpectedSourceURI: c.SourceURI, + ExpectedBranch: c.SourceBranch, + ExpectedDigest: artifactHash, + ExpectedVersionedTag: c.SourceVersionTag, + ExpectedTag: c.SourceTag, + ExpectedWorkflowInputs: c.BuildWorkflowInputs, + } + + builderOpts := &options.BuilderOpts{ + ExpectedID: c.BuilderID, + } + + provenance, err := os.ReadFile(c.ProvenancePath) + if err != nil { + return "", err + } + + verifiedProvenance, outBuilderID, err := verifiers.VerifyArtifact(ctx, provenance, artifactHash, provenanceOpts, builderOpts) + if err != nil { + return "", err + } + + if c.PrintProvenance { + fmt.Fprintf(os.Stdout, "%s\n", string(verifiedProvenance)) + } + + return outBuilderID, nil +} + +func getArtifactHash(artifactPath string) (string, error) { + f, err := os.Open(artifactPath) + if err != nil { + return "", err + } + defer f.Close() + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return "", err + } + return hex.EncodeToString(h.Sum(nil)), nil +} diff --git a/cli/slsa-verifier/verify/verify_image.go b/cli/slsa-verifier/verify/verify_image.go new file mode 100644 index 0000000..064a4df --- /dev/null +++ b/cli/slsa-verifier/verify/verify_image.go @@ -0,0 +1,90 @@ +// Copyright 2022 SLSA Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package verify + +import ( + "context" + "fmt" + "os" + + "github.com/slsa-framework/slsa-verifier/options" + "github.com/slsa-framework/slsa-verifier/verifiers" + "github.com/slsa-framework/slsa-verifier/verifiers/container" +) + +type ComputeDigestFn func(string) (string, error) + +// Note: nil branch, tag, version-tag and builder-id means we ignore them during verification. +type VerifyImageCommand struct { + // May be nil if supplied alongside in the registry + ProvenancePath *string + BuilderID *string + SourceURI string + SourceBranch *string + SourceTag *string + SourceVersionTag *string + BuildWorkflowInputs map[string]string + PrintProvenance bool + DigestFn ComputeDigestFn +} + +func (c *VerifyImageCommand) Exec(ctx context.Context, artifacts []string) (string, error) { + artifactImage := artifacts[0] + // Retrieve the image digest. + if c.DigestFn == nil { + c.DigestFn = container.GetImageDigest + } + digest, err := c.DigestFn(artifactImage) + if err != nil { + return "", err + } + + // Verify that the reference is immutable. + if err := container.ValidateArtifactReference(artifactImage, digest); err != nil { + return "", err + } + + provenanceOpts := &options.ProvenanceOpts{ + ExpectedSourceURI: c.SourceURI, + ExpectedBranch: c.SourceBranch, + ExpectedDigest: digest, + ExpectedVersionedTag: c.SourceVersionTag, + ExpectedTag: c.SourceTag, + ExpectedWorkflowInputs: c.BuildWorkflowInputs, + } + + builderOpts := &options.BuilderOpts{ + ExpectedID: c.BuilderID, + } + + var provenance []byte + if c.ProvenancePath != nil { + provenance, err = os.ReadFile(*c.ProvenancePath) + if err != nil { + return "", err + } + } + + verifiedProvenance, outBuilderID, err := verifiers.VerifyImage(ctx, artifacts[0], provenance, provenanceOpts, builderOpts) + if err != nil { + return "", err + } + + if c.PrintProvenance { + fmt.Fprintf(os.Stdout, "%s\n", string(verifiedProvenance)) + } + + return outBuilderID, nil +} diff --git a/cli/slsa-verifier/version.go b/cli/slsa-verifier/version.go new file mode 100644 index 0000000..f279352 --- /dev/null +++ b/cli/slsa-verifier/version.go @@ -0,0 +1,33 @@ +// Copyright 2022 SLSA Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/slsa-framework/slsa-verifier/version" +) + +func versionCmd() *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Print the version and exit", + Run: func(cmd *cobra.Command, args []string) { + fmt.Println(version.Version) + }, + } +} diff --git a/experimental/rest/service.go b/experimental/rest/service.go index 5187366..b3f5871 100644 --- a/experimental/rest/service.go +++ b/experimental/rest/service.go @@ -129,7 +129,7 @@ func verifyHandlerV1(r *http.Request) *v1Result { } ctx := context.Background() - p, builderID, err := verifiers.Verify(ctx, "", []byte(query.DsseEnvelope), + p, builderID, err := verifiers.VerifyArtifact(ctx, []byte(query.DsseEnvelope), query.ArtifactHash, provenanceOpts, builderOpts) if err != nil { return results.withError(err) diff --git a/verifiers/verifier.go b/verifiers/verifier.go index 3af722d..7770679 100644 --- a/verifiers/verifier.go +++ b/verifiers/verifier.go @@ -11,35 +11,48 @@ import ( "github.com/slsa-framework/slsa-verifier/verifiers/internal/gha" ) -func Verify(ctx context.Context, artifactImage string, - provenance []byte, artifactHash string, - provenanceOpts *options.ProvenanceOpts, - builderOpts *options.BuilderOpts, -) ([]byte, string, error) { +func getVerifier(builderOpts *options.BuilderOpts) (register.SLSAVerifier, error) { // By default, use the GHA builders verifier := register.SLSAVerifiers[gha.VerifierName] // If user provids a builderID, find the right verifier based on its ID. if builderOpts.ExpectedID != nil && *builderOpts.ExpectedID != "" { - foundBuilder := false for _, v := range register.SLSAVerifiers { if v.IsAuthoritativeFor(*builderOpts.ExpectedID) { - foundBuilder = true - verifier = v - break + return v, nil } } - if !foundBuilder { - // No builder found. - return nil, "", fmt.Errorf("%w: %s", serrors.ErrorVerifierNotSupported, *builderOpts.ExpectedID) - } + // No builder found. + return nil, fmt.Errorf("%w: %s", serrors.ErrorVerifierNotSupported, *builderOpts.ExpectedID) } - // By default, try the GHA builders. - if artifactImage != "" { - return verifier.VerifyImage(ctx, provenance, artifactImage, provenanceOpts, builderOpts) + return verifier, nil +} + +func VerifyImage(ctx context.Context, artifactImage string, + provenance []byte, + provenanceOpts *options.ProvenanceOpts, + builderOpts *options.BuilderOpts, +) ([]byte, string, error) { + verifier, err := getVerifier(builderOpts) + if err != nil { + return nil, "", err } + + return verifier.VerifyImage(ctx, provenance, artifactImage, provenanceOpts, builderOpts) +} + +func VerifyArtifact(ctx context.Context, + provenance []byte, artifactHash string, + provenanceOpts *options.ProvenanceOpts, + builderOpts *options.BuilderOpts, +) ([]byte, string, error) { + verifier, err := getVerifier(builderOpts) + if err != nil { + return nil, "", err + } + return verifier.VerifyArtifact(ctx, provenance, artifactHash, provenanceOpts, builderOpts) } diff --git a/version/version.go b/version/version.go new file mode 100644 index 0000000..6101a6d --- /dev/null +++ b/version/version.go @@ -0,0 +1,20 @@ +// Copyright 2022 SLSA Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package version + +// Version is the version of slsa-verifier. +// It is meant to be overwritten with +// -ldflags="-X github.com/slsa-framework/slsa-verifier/version.Version=X.Y". +var Version = `unknown`