mirror of
https://github.com/slsa-framework/slsa-verifier.git
synced 2026-05-20 23:42:54 +00:00
✨ Add tag and version verification (#18)
* Add tag verification * fix * fix * fix
This commit is contained in:
6
go.mod
6
go.mod
@@ -13,7 +13,10 @@ require (
|
||||
github.com/sigstore/sigstore v1.1.1-0.20220217212907-e48ca03a5ba7
|
||||
)
|
||||
|
||||
require github.com/sigstore/cosign v1.6.0
|
||||
require (
|
||||
github.com/sigstore/cosign v1.6.0
|
||||
golang.org/x/mod v0.5.1
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.100.2 // indirect
|
||||
@@ -178,7 +181,6 @@ require (
|
||||
go.uber.org/multierr v1.7.0 // indirect
|
||||
go.uber.org/zap v1.21.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220213190939-1e6e3497d506 // indirect
|
||||
golang.org/x/mod v0.5.1 // indirect
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||
|
||||
53
main.go
53
main.go
@@ -30,11 +30,16 @@ var (
|
||||
binaryPath string
|
||||
source string
|
||||
branch string
|
||||
tag string
|
||||
versiontag string
|
||||
)
|
||||
|
||||
var defaultRekorAddr = "https://rekor.sigstore.dev"
|
||||
|
||||
func verify(ctx context.Context, provenancePath, artifactHash, source, branch string) error {
|
||||
func verify(ctx context.Context,
|
||||
provenancePath, artifactHash, source, branch string,
|
||||
tag, versiontag *string,
|
||||
) error {
|
||||
rClient, err := rekor.NewClient(defaultRekorAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -83,6 +88,20 @@ func verify(ctx context.Context, provenancePath, artifactHash, source, branch st
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify the tag.
|
||||
if tag != nil {
|
||||
if err := pkg.VerifyTag(env, *tag); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the versioned tag.
|
||||
if versiontag != nil {
|
||||
if err := pkg.VerifyVersionedTag(env, *versiontag); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(workflowInfo, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -96,7 +115,9 @@ func main() {
|
||||
flag.StringVar(&provenancePath, "provenance", "", "path to a provenance file")
|
||||
flag.StringVar(&binaryPath, "binary", "", "path to a binary 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", "main", "expected branch the binary was compiled from. Default: main")
|
||||
flag.StringVar(&branch, "branch", "main", "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.Parse()
|
||||
|
||||
if provenancePath == "" || binaryPath == "" || source == "" {
|
||||
@@ -104,6 +125,20 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var ptag, pversiontag *string
|
||||
|
||||
if isFlagPassed("tag") {
|
||||
ptag = &tag
|
||||
}
|
||||
if isFlagPassed("versioned-tag") {
|
||||
pversiontag = &versiontag
|
||||
}
|
||||
|
||||
if pversiontag != nil && ptag != nil {
|
||||
fmt.Fprintf(os.Stderr, "'version' and 'tag' options cannot be used together\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
f, err := os.Open(binaryPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -118,10 +153,20 @@ func main() {
|
||||
ctx := context.Background()
|
||||
if err := verify(ctx, provenancePath,
|
||||
hex.EncodeToString(h.Sum(nil)),
|
||||
source,
|
||||
branch); err != nil {
|
||||
source, branch,
|
||||
ptag, pversiontag); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println("successfully verified SLSA provenance")
|
||||
}
|
||||
|
||||
func isFlagPassed(name string) bool {
|
||||
found := false
|
||||
flag.Visit(func(f *flag.Flag) {
|
||||
if f.Name == name {
|
||||
found = true
|
||||
}
|
||||
})
|
||||
return found
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/mod/semver"
|
||||
|
||||
cjson "github.com/docker/go/canonical/json"
|
||||
"github.com/go-openapi/runtime"
|
||||
"github.com/google/trillian/merkle/logverifier"
|
||||
@@ -45,11 +47,14 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
errorInvalidDssePayload = errors.New("invalid DSSE envelope payload")
|
||||
errorRekorSearch = errors.New("error searching rekor entries")
|
||||
errorMismatchHash = errors.New("binary artifact hash does not match provenance subject")
|
||||
errorMismatchBranch = errors.New("branch used to generate the binary does not match provenance")
|
||||
errorInvalidVersion = errors.New("invalid version")
|
||||
errorInvalidDssePayload = errors.New("invalid DSSE envelope payload")
|
||||
errorRekorSearch = errors.New("error searching rekor entries")
|
||||
errorMismatchHash = errors.New("binary artifact hash does not match provenance subject")
|
||||
errorMismatchBranch = errors.New("branch used to generate the binary does not match provenance")
|
||||
errorMismatchTag = errors.New("tag used to generate the binary does not match provenance")
|
||||
errorMismatchVersionedTag = errors.New("tag used to generate the binary does not match provenance")
|
||||
errorInvalidSemver = errors.New("invalid semantic version")
|
||||
errorInvalidVersion = errors.New("invalid version")
|
||||
)
|
||||
|
||||
func EnvelopeFromBytes(payload []byte) (env *dsselib.Envelope, err error) {
|
||||
@@ -414,6 +419,42 @@ func VerifyBranch(env *dsselib.Envelope, expectedBranch string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func VerifyTag(env *dsselib.Envelope, expectedTag string) error {
|
||||
tag, err := getTag(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.EqualFold(tag, "refs/tags/"+expectedTag) {
|
||||
return fmt.Errorf("tag '%s': %w", tag, errorMismatchTag)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func VerifyVersionedTag(env *dsselib.Envelope, expectedTag string) error {
|
||||
if !semver.IsValid(expectedTag) {
|
||||
return fmt.Errorf("%s: %w", expectedTag, errorInvalidSemver)
|
||||
}
|
||||
|
||||
tag, err := getTag(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
semTag := strings.TrimPrefix(tag, "refs/tags/")
|
||||
if !semver.IsValid(semTag) {
|
||||
return fmt.Errorf("%s: %w", expectedTag, errorInvalidSemver)
|
||||
}
|
||||
|
||||
if semver.Compare(semTag, expectedTag) < 0 {
|
||||
return errorMismatchVersionedTag
|
||||
}
|
||||
|
||||
// Match.
|
||||
return nil
|
||||
}
|
||||
|
||||
func getAsInt(parameters map[string]interface{}, field string) (int, error) {
|
||||
value, ok := parameters[field]
|
||||
if !ok {
|
||||
@@ -454,7 +495,7 @@ func getBaseRef(parameters map[string]interface{}) (string, error) {
|
||||
}
|
||||
|
||||
// Look at the event payload instead.
|
||||
// We don't do thatt it all the time because the payload
|
||||
// We don't do that for all triggers because the payload
|
||||
// is event-specific; and only the `push` event seems to have a `base_ref``.
|
||||
eventName, err := getAsString(parameters, "event_name")
|
||||
if err != nil {
|
||||
@@ -478,6 +519,44 @@ func getBaseRef(parameters map[string]interface{}) (string, error) {
|
||||
return getAsString(payload, "base_ref")
|
||||
}
|
||||
|
||||
// Get tag from the provenance invocation parameters.
|
||||
func getTag(env *dsselib.Envelope) (string, error) {
|
||||
pyld, err := base64.StdEncoding.DecodeString(env.Payload)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: %s", errorInvalidDssePayload, "decoding payload")
|
||||
}
|
||||
|
||||
var prov intoto.ProvenanceStatement
|
||||
if err := json.Unmarshal([]byte(pyld), &prov); err != nil {
|
||||
return "", fmt.Errorf("%w: %s", errorInvalidDssePayload, "unmarshalling json")
|
||||
}
|
||||
|
||||
parameters, ok := prov.Predicate.Invocation.Parameters.(map[string]interface{})
|
||||
if !ok {
|
||||
return "", fmt.Errorf("%w: %s", errorInvalidDssePayload, "parameters type")
|
||||
}
|
||||
|
||||
// Validate version.
|
||||
if err := validateVersion(parameters); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
refType, err := getAsString(parameters, "ref_type")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
switch refType {
|
||||
case "branch":
|
||||
return "", nil
|
||||
case "tag":
|
||||
return getAsString(parameters, "ref")
|
||||
default:
|
||||
return "", fmt.Errorf("%w: %s %s", errorInvalidDssePayload,
|
||||
"unknown ref type", refType)
|
||||
}
|
||||
}
|
||||
|
||||
// Get branch from the provenance invocation parameters.
|
||||
func getBranch(env *dsselib.Envelope) (string, error) {
|
||||
pyld, err := base64.StdEncoding.DecodeString(env.Payload)
|
||||
@@ -495,14 +574,10 @@ func getBranch(env *dsselib.Envelope) (string, error) {
|
||||
return "", fmt.Errorf("%w: %s", errorInvalidDssePayload, "parameters type")
|
||||
}
|
||||
|
||||
// Version.
|
||||
version, err := getAsInt(parameters, "version")
|
||||
if err != nil {
|
||||
// Validate version.
|
||||
if err := validateVersion(parameters); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if version != 1 {
|
||||
return "", fmt.Errorf("%w", errorInvalidVersion)
|
||||
}
|
||||
|
||||
refType, err := getAsString(parameters, "ref_type")
|
||||
if err != nil {
|
||||
@@ -519,3 +594,14 @@ func getBranch(env *dsselib.Envelope) (string, error) {
|
||||
"unknown ref type", refType)
|
||||
}
|
||||
}
|
||||
|
||||
func validateVersion(parameters map[string]interface{}) error {
|
||||
version, err := getAsInt(parameters, "version")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if version != 1 {
|
||||
return fmt.Errorf("%w", errorInvalidVersion)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -304,3 +304,232 @@ func Test_VerifyBranch(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_VerifyTag(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
tag string
|
||||
expected error
|
||||
}{
|
||||
{
|
||||
name: "ref main",
|
||||
path: "./testdata/dsse-main-ref.intoto.jsonl",
|
||||
expected: errorMismatchTag,
|
||||
},
|
||||
{
|
||||
name: "ref branch3",
|
||||
path: "./testdata/dsse-branch3-ref.intoto.jsonl",
|
||||
expected: errorMismatchTag,
|
||||
},
|
||||
{
|
||||
name: "invalid ref type",
|
||||
path: "./testdata/dsse-invalid-ref-type.intoto.jsonl",
|
||||
expected: errorInvalidDssePayload,
|
||||
},
|
||||
{
|
||||
name: "invalid version",
|
||||
path: "./testdata/dsse-invalid-version.intoto.jsonl",
|
||||
expected: errorInvalidVersion,
|
||||
},
|
||||
{
|
||||
name: "tag vslsa1",
|
||||
path: "./testdata/dsse-vslsa1-tag.intoto.jsonl",
|
||||
tag: "vslsa1",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt // Re-initializing variable so it is not changed while executing the closure below
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
content, err := os.ReadFile(tt.path)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("os.ReadFile: %w", err))
|
||||
}
|
||||
env, err := envelopeFromBytes(content)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("envelopeFromBytes: %w", err))
|
||||
}
|
||||
|
||||
err = VerifyTag(env, tt.tag)
|
||||
if !errCmp(err, tt.expected) {
|
||||
t.Errorf(cmp.Diff(err, tt.expected))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_VerifyVersionedTag(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
tag string
|
||||
expected error
|
||||
}{
|
||||
{
|
||||
name: "ref main",
|
||||
path: "./testdata/dsse-main-ref.intoto.jsonl",
|
||||
expected: errorInvalidSemver,
|
||||
tag: "v1.2.3",
|
||||
},
|
||||
{
|
||||
name: "ref branch3",
|
||||
path: "./testdata/dsse-branch3-ref.intoto.jsonl",
|
||||
expected: errorInvalidSemver,
|
||||
tag: "v1.2.3",
|
||||
},
|
||||
{
|
||||
name: "tag v1.2 invalid versioning",
|
||||
path: "./testdata/dsse-v1.2-tag.intoto.jsonl",
|
||||
tag: "1.2",
|
||||
expected: errorInvalidSemver,
|
||||
},
|
||||
|
||||
{
|
||||
name: "invalid ref",
|
||||
path: "./testdata/dsse-invalid-ref-type.intoto.jsonl",
|
||||
expected: errorInvalidDssePayload,
|
||||
tag: "v1.2.3",
|
||||
},
|
||||
{
|
||||
name: "invalid version",
|
||||
path: "./testdata/dsse-invalid-version.intoto.jsonl",
|
||||
expected: errorInvalidVersion,
|
||||
tag: "v1.2.3",
|
||||
},
|
||||
{
|
||||
name: "tag vslsa1 invalid",
|
||||
path: "./testdata/dsse-vslsa1-tag.intoto.jsonl",
|
||||
tag: "vslsa1",
|
||||
expected: errorInvalidSemver,
|
||||
},
|
||||
{
|
||||
name: "tag vslsa1 invalid semver",
|
||||
path: "./testdata/dsse-vslsa1-tag.intoto.jsonl",
|
||||
tag: "v1.2.3",
|
||||
expected: errorInvalidSemver,
|
||||
},
|
||||
{
|
||||
name: "tag v1.2.3 exact match",
|
||||
path: "./testdata/dsse-v1.2.3-tag.intoto.jsonl",
|
||||
tag: "v1.2.3",
|
||||
},
|
||||
{
|
||||
name: "tag v1.2.3 match v1.2",
|
||||
path: "./testdata/dsse-v1.2.3-tag.intoto.jsonl",
|
||||
tag: "v1.2",
|
||||
},
|
||||
{
|
||||
name: "tag v1.2.3 match v1",
|
||||
path: "./testdata/dsse-v1.2.3-tag.intoto.jsonl",
|
||||
tag: "v1",
|
||||
},
|
||||
{
|
||||
name: "tag v1.2.3 no match v2",
|
||||
path: "./testdata/dsse-v1.2.3-tag.intoto.jsonl",
|
||||
tag: "v2",
|
||||
expected: errorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v1.2.3 no match v1.3",
|
||||
path: "./testdata/dsse-v1.2.3-tag.intoto.jsonl",
|
||||
tag: "v1.3",
|
||||
expected: errorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v1.2.3 no match v1.2.4",
|
||||
path: "./testdata/dsse-v1.2.3-tag.intoto.jsonl",
|
||||
tag: "v1.2.4",
|
||||
expected: errorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v1.2 exact v1.2",
|
||||
path: "./testdata/dsse-v1.2-tag.intoto.jsonl",
|
||||
tag: "v1.2",
|
||||
},
|
||||
{
|
||||
name: "tag v1.2 match v1",
|
||||
path: "./testdata/dsse-v1.2-tag.intoto.jsonl",
|
||||
tag: "v1",
|
||||
},
|
||||
{
|
||||
name: "tag v1.2 no match v1.3",
|
||||
path: "./testdata/dsse-v1.2-tag.intoto.jsonl",
|
||||
tag: "v1.3",
|
||||
expected: errorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v1.2 no match v1.2.3",
|
||||
path: "./testdata/dsse-v1.2-tag.intoto.jsonl",
|
||||
tag: "v1.2.3",
|
||||
expected: errorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v1.2 match v1.2.0",
|
||||
path: "./testdata/dsse-v1.2-tag.intoto.jsonl",
|
||||
tag: "v1.2.0",
|
||||
},
|
||||
{
|
||||
name: "tag v1.2 no match v2",
|
||||
path: "./testdata/dsse-v1.2-tag.intoto.jsonl",
|
||||
tag: "v2",
|
||||
expected: errorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v1 exact match",
|
||||
path: "./testdata/dsse-v1-tag.intoto.jsonl",
|
||||
tag: "v1",
|
||||
},
|
||||
{
|
||||
name: "tag v1 no match v2",
|
||||
path: "./testdata/dsse-v1-tag.intoto.jsonl",
|
||||
tag: "v2",
|
||||
expected: errorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v1 no match v1.2",
|
||||
path: "./testdata/dsse-v1-tag.intoto.jsonl",
|
||||
tag: "v1.2",
|
||||
expected: errorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v1 no match v1.2.3",
|
||||
path: "./testdata/dsse-v1-tag.intoto.jsonl",
|
||||
tag: "v1.2.3",
|
||||
expected: errorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v1 match v1.0",
|
||||
path: "./testdata/dsse-v1-tag.intoto.jsonl",
|
||||
tag: "v1.0",
|
||||
},
|
||||
{
|
||||
name: "tag v1 match v1.0.0",
|
||||
path: "./testdata/dsse-v1-tag.intoto.jsonl",
|
||||
tag: "v1.0.0",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt // Re-initializing variable so it is not changed while executing the closure below
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
content, err := os.ReadFile(tt.path)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("os.ReadFile: %w", err))
|
||||
}
|
||||
env, err := envelopeFromBytes(content)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("envelopeFromBytes: %w", err))
|
||||
}
|
||||
|
||||
err = VerifyVersionedTag(env, tt.tag)
|
||||
if !errCmp(err, tt.expected) {
|
||||
t.Errorf(cmp.Diff(err, tt.expected))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
10
pkg/testdata/dsse-invalid-version-valid-tag.intoto.jsonl
vendored
Normal file
10
pkg/testdata/dsse-invalid-version-valid-tag.intoto.jsonl
vendored
Normal file
File diff suppressed because one or more lines are too long
10
pkg/testdata/dsse-v1-tag.intoto.jsonl
vendored
Normal file
10
pkg/testdata/dsse-v1-tag.intoto.jsonl
vendored
Normal file
File diff suppressed because one or more lines are too long
10
pkg/testdata/dsse-v1.2-tag.intoto.jsonl
vendored
Normal file
10
pkg/testdata/dsse-v1.2-tag.intoto.jsonl
vendored
Normal file
File diff suppressed because one or more lines are too long
10
pkg/testdata/dsse-v1.2.3-tag.intoto.jsonl
vendored
Normal file
10
pkg/testdata/dsse-v1.2.3-tag.intoto.jsonl
vendored
Normal file
File diff suppressed because one or more lines are too long
10
pkg/testdata/dsse-vslsa1-tag.intoto.jsonl
vendored
Normal file
10
pkg/testdata/dsse-vslsa1-tag.intoto.jsonl
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user