Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 12 commits into from
Sep 6, 2022
5 changes: 4 additions & 1 deletion .github/config-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@ flags:
goos: linux
goarch: amd64
binary: slsa-verifier-{{ .Os }}-{{ .Arch }}
dir: ./cli/slsa-verifier
dir: ./cli/slsa-verifier

ldflags:
- "-X version.Version={{ .Version }}"
4 changes: 2 additions & 2 deletions .github/workflows/pre-submit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 ./...
51 changes: 33 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,41 +49,56 @@ $ 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
```

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
Expand Down
208 changes: 24 additions & 184 deletions cli/slsa-verifier/main.go
Original file line number Diff line number Diff line change
@@ -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
}

var (
provenancePath string
builderID string
artifactPath string
artifactImage string
source string
branch string
tag string
versiontag string
inputs workflowInputs
printProvenance 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)
}
i.kv[l[0]] = l[1]
return nil
}

func (i *workflowInputs) AsMap() map[string]string {
return i.kv
}

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)
func check(err error) {
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))
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

func isFlagPassed(name string) bool {
found := false
flag.Visit(func(f *flag.Flag) {
if f.Name == name {
found = true
}
})
return found
func ExperimentalEnabled() bool {
return os.Getenv("SLSA_VERIFIER_EXPERIMENTAL") == "1"
}

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

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)
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 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
func main() {
check(rootCmd().Execute())
}
Loading