Add tag and version verification (#18)

* Add tag verification

* fix

* fix

* fix
This commit is contained in:
laurentsimon
2022-04-01 12:22:36 -07:00
committed by GitHub
parent 095f60a0ba
commit 7c64c73c2a
9 changed files with 430 additions and 18 deletions

6
go.mod
View File

@@ -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
View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long