refactor: add subcommands and separate functionality from artifacts a… (#231)

* refactor: add subcommands and separate functionality from artifacts and images

Signed-off-by: Asra Ali <asraa@google.com>
This commit is contained in:
asraa
2022-09-06 17:10:58 -05:00
committed by GitHub
parent 0211941480
commit ff0ced42ef
13 changed files with 591 additions and 234 deletions

View File

@@ -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())
}

View File

@@ -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))
}
}
})
}

130
cli/slsa-verifier/verify.go Normal file
View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
},
}
}