diff --git a/errors/errors.go b/errors/errors.go index 4d71cb0d2..ecb0f72f4 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -14,6 +14,7 @@ var ( ErrorInvalidSemver = errors.New("invalid semantic version") ErrorRekorSearch = errors.New("error searching rekor entries") ErrorMismatchHash = errors.New("artifact hash does not match provenance subject") + ErrorMismatchIntoto = errors.New("verified intoto provenance does not match text provenance") ErrorInvalidRef = errors.New("invalid ref") ErrorUntrustedReusableWorkflow = errors.New("untrusted reusable workflow") ErrorNoValidRekorEntries = errors.New("could not find a matching valid signature entry") diff --git a/go.mod b/go.mod index da9b0212a..1f188ae86 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/go-openapi/swag v0.22.3 github.com/google/go-containerregistry v0.11.0 github.com/gorilla/mux v1.8.0 + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/sigstore/cosign v1.11.0 github.com/slsa-framework/slsa-github-generator v1.2.0 github.com/transparency-dev/merkle v0.0.1 diff --git a/go.sum b/go.sum index f39f6c141..bb08418c3 100644 --- a/go.sum +++ b/go.sum @@ -1414,6 +1414,7 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/moricho/tparallel v0.2.1/go.mod h1:fXEIZxG2vdfl0ZF8b42f5a78EhjjD5mX8qUplsoSU4k= diff --git a/verifiers/internal/gcb/provenance.go b/verifiers/internal/gcb/provenance.go index 7434a20c7..d49fd26bb 100644 --- a/verifiers/internal/gcb/provenance.go +++ b/verifiers/internal/gcb/provenance.go @@ -6,9 +6,12 @@ import ( "encoding/json" "fmt" "os" + "reflect" "regexp" "strings" + "github.com/google/go-cmp/cmp" + intoto "github.com/in-toto/in-toto-golang/in_toto" slsa01 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.1" dsselib "github.com/secure-systems-lab/go-securesystemslib/dsse" @@ -25,10 +28,18 @@ type v01IntotoStatement struct { Predicate slsa01.ProvenancePredicate `json:"predicate"` } +// The GCB provenance contains a human-readable version of the intoto +// statement, but it is not compliant with the standard. It uses `slsaProvenance` +// instead of `predicate`. For backward compatibility, this has not been fixed +// by the GCB team. +type v01GCBIntotoStatement struct { + intoto.StatementHeader + SlsaProvenance slsa01.ProvenancePredicate `json:"slsaProvenance"` +} + type provenance struct { Build struct { - // TODO: compare to verified provenance. - // IntotoStatement v01IntotoStatement `json:"intotoStatement"` + UnverifiedTextIntotoStatement v01GCBIntotoStatement `json:"intotoStatement"` } `json:"build"` Kind string `json:"kind"` ResourceURI string `json:"resourceUri"` @@ -149,6 +160,32 @@ func (self *Provenance) VerifySummary(provenanceOpts *options.ProvenanceOpts) er return nil } +// VerifyTextProvenance verifies the text provenance prepended +// to the provenance.This text mirrors the DSSE payload but is human-readable. +func (self *Provenance) VerifyTextProvenance() error { + if err := self.isVerified(); err != nil { + return err + } + + // Note: there is an additional field `metadata.buildInvocationId` which + // is not part of the specs but is present. This field is currently ignored during comparison. + unverifiedTextIntotoStatement := v01IntotoStatement{ + StatementHeader: self.verifiedProvenance.Build.UnverifiedTextIntotoStatement.StatementHeader, + Predicate: self.verifiedProvenance.Build.UnverifiedTextIntotoStatement.SlsaProvenance, + } + + // Note: DeepEqual() has problem with time comparisons: https://github.com/onsi/gomega/issues/264 + // but this should not affect us since both times are supposed to have the the same string and + // they are both taken from a strng representation. + // We do not use cmp.Equal() because it *can* panic and is intended for unit tests only. + if !reflect.DeepEqual(unverifiedTextIntotoStatement, *self.verifiedIntotoStatement) { + return fmt.Errorf("%w: diff '%s'", serrors.ErrorMismatchIntoto, + cmp.Diff(unverifiedTextIntotoStatement, *self.verifiedIntotoStatement)) + } + + return nil +} + // VerifyIntotoHeaders verifies the headers are intoto format and the expected // slsa predicate. func (self *Provenance) VerifyIntotoHeaders() error { diff --git a/verifiers/internal/gcb/provenance_test.go b/verifiers/internal/gcb/provenance_test.go new file mode 100644 index 000000000..3056b8ada --- /dev/null +++ b/verifiers/internal/gcb/provenance_test.go @@ -0,0 +1,682 @@ +package gcb + +import ( + "encoding/json" + "fmt" + "os" + "strings" + "testing" + + //"time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + serrors "github.com/slsa-framework/slsa-verifier/errors" + "github.com/slsa-framework/slsa-verifier/options" +) + +// This function sets the statement of the proveannce, as if +// it had been verified. This is necessary because individual functions +// expect this statement to be populated; and this is done only +// after the signatue is verified. +func setStatement(gcb *Provenance) error { + var statement v01IntotoStatement + payload, err := payloadFromEnvelope(&gcb.gcloudProv.ProvenanceSummary.Provenance[0].Envelope) + if err != nil { + return fmt.Errorf("payloadFromEnvelope: %w", err) + } + if err := json.Unmarshal(payload, &statement); err != nil { + return fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, err.Error()) + } + gcb.verifiedIntotoStatement = &statement + gcb.verifiedProvenance = &gcb.gcloudProv.ProvenanceSummary.Provenance[0] + return nil +} + +func Test_VerifyIntotoHeaders(t *testing.T) { + t.Parallel() + tests := []struct { + name string + path string + expected error + }{ + { + name: "valid gcb provenance", + path: "./testdata/gcloud-container-github.json", + }, + { + name: "invalid intoto header", + path: "./testdata/gcloud-container-invalid-intotoheader.json", + expected: serrors.ErrorInvalidDssePayload, + }, + { + name: "invalid provenance header", + path: "./testdata/gcloud-container-invalid-slsaheader.json", + expected: serrors.ErrorInvalidDssePayload, + }, + } + 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)) + } + + prov, err := ProvenanceFromBytes(content) + if err != nil { + panic(fmt.Errorf("ProvenanceFromBytes: %w", err)) + } + + if err := setStatement(prov); err != nil { + panic(fmt.Errorf("ProvenanceFromBytes: %w", err)) + } + + err = prov.VerifyIntotoHeaders() + if !cmp.Equal(err, tt.expected, cmpopts.EquateErrors()) { + t.Errorf(cmp.Diff(err, tt.expected, cmpopts.EquateErrors())) + } + }) + } +} + +func Test_VerifyBuilder(t *testing.T) { + t.Parallel() + tests := []struct { + name string + path string + builderID string + expected error + }{ + { + name: "valid gcb provenance", + path: "./testdata/gcloud-container-github.json", + builderID: "https://cloudbuild.googleapis.com/GoogleHostedWorker@v0.2", + }, + { + name: "mismatch builder.id version", + path: "./testdata/gcloud-container-github.json", + builderID: "https://cloudbuild.googleapis.com/GoogleHostedWorker@v0.1", + expected: serrors.ErrorMismatchBuilderID, + }, + { + name: "mismatch builder.id name", + path: "./testdata/gcloud-container-github.json", + builderID: "https://cloudbuild.googleapis.com/GoogleHostedWorke@v0.2", + expected: serrors.ErrorMismatchBuilderID, + }, + { + name: "mismatch builder.id protocol", + path: "./testdata/gcloud-container-github.json", + builderID: "http://cloudbuild.googleapis.com/GoogleHostedWorker@v0.2", + expected: serrors.ErrorMismatchBuilderID, + }, + { + name: "mismatch recipe.arguments.type", + path: "./testdata/gcloud-container-invalid-recipe.arguments.type.json", + expected: serrors.ErrorMismatchBuilderID, + }, + { + name: "mismatch recipe.type", + path: "./testdata/gcloud-container-invalid-recipe.type.json", + expected: serrors.ErrorMismatchBuilderID, + }, + } + 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)) + } + + prov, err := ProvenanceFromBytes(content) + if err != nil { + panic(fmt.Errorf("ProvenanceFromBytes: %w", err)) + } + + if err := setStatement(prov); err != nil { + panic(fmt.Errorf("ProvenanceFromBytes: %w", err)) + } + + var builderOpts options.BuilderOpts + if tt.builderID != "" { + builderOpts.ExpectedID = &tt.builderID + } + outBuilderID, err := prov.VerifyBuilder(&builderOpts) + if !cmp.Equal(err, tt.expected, cmpopts.EquateErrors()) { + t.Errorf(cmp.Diff(err, tt.expected, cmpopts.EquateErrors())) + } + + if err != nil { + return + } + + if outBuilderID != tt.builderID { + t.Errorf(cmp.Diff(outBuilderID, tt.builderID)) + } + }) + } +} + +func Test_VerifySourceURI(t *testing.T) { + t.Parallel() + tests := []struct { + name string + path string + source string + expected error + }{ + { + name: "valid gcb provenance", + path: "./testdata/gcloud-container-github.json", + source: "https://github.com/laurentsimon/gcb-tests", + }, + { + name: "mismatch name", + path: "./testdata/gcloud-container-github.json", + source: "https://github.com/laurentsimon/gcb-tests2", + expected: serrors.ErrorMismatchSource, + }, + { + name: "mismatch org", + path: "./testdata/gcloud-container-github.json", + source: "https://github.com/wrong/gcb-tests", + expected: serrors.ErrorMismatchSource, + }, + { + name: "mismatch protocol", + path: "./testdata/gcloud-container-github.json", + source: "http://github.com/laurentsimon/gcb-tests", + expected: serrors.ErrorMismatchSource, + }, + { + name: "mismatch full uri", + path: "./testdata/gcloud-container-github.json", + source: "https://github.com/laurentsimon/gcb-tests/commit/fbbb98765e85ad464302dc5977968104d36e455e", + expected: serrors.ErrorMismatchSource, + }, + } + 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)) + } + + prov, err := ProvenanceFromBytes(content) + if err != nil { + panic(fmt.Errorf("ProvenanceFromBytes: %w", err)) + } + + if err := setStatement(prov); err != nil { + panic(fmt.Errorf("ProvenanceFromBytes: %w", err)) + } + + err = prov.VerifySourceURI(tt.source) + if !cmp.Equal(err, tt.expected, cmpopts.EquateErrors()) { + t.Errorf(cmp.Diff(err, tt.expected, cmpopts.EquateErrors())) + } + }) + } +} + +func Test_VerifySignature(t *testing.T) { + t.Parallel() + tests := []struct { + name string + path string + expected error + }{ + { + name: "valid gcb provenance", + path: "./testdata/gcloud-container-github.json", + }, + { + name: "invalid signature", + path: "./testdata/gcloud-container-invalid-signature.json", + expected: serrors.ErrorNoValidSignature, + }, + { + name: "invalid signature", + path: "./testdata/gcloud-container-invalid-signature-payloadtype.json", + expected: serrors.ErrorNoValidSignature, + }, + } + 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)) + } + + prov, err := ProvenanceFromBytes(content) + if err != nil { + panic(fmt.Errorf("ProvenanceFromBytes: %w", err)) + } + + if err := setStatement(prov); err != nil { + panic(fmt.Errorf("ProvenanceFromBytes: %w", err)) + } + + err = prov.VerifySignature() + if !cmp.Equal(err, tt.expected, cmpopts.EquateErrors()) { + t.Errorf(cmp.Diff(err, tt.expected, cmpopts.EquateErrors())) + } + }) + } +} + +func Test_VerifySubjectDigest(t *testing.T) { + t.Parallel() + tests := []struct { + name string + path string + hash string + expected error + }{ + { + name: "valid gcb provenance", + path: "./testdata/gcloud-container-github.json", + hash: "1a033b002f89ed2b8ea733162497fb70f1a4049a7f8602d6a33682b4ad9921fd", + }, + { + name: "mismatch hash", + path: "./testdata/gcloud-container-github.json", + hash: "0a033b002f89ed2b8ea733162497fb70f1a4049a7f8602d6a33682b4ad9921fd", + expected: serrors.ErrorMismatchHash, + }, + } + 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)) + } + + prov, err := ProvenanceFromBytes(content) + if err != nil { + panic(fmt.Errorf("ProvenanceFromBytes: %w", err)) + } + + if err := setStatement(prov); err != nil { + panic(fmt.Errorf("ProvenanceFromBytes: %w", err)) + } + + err = prov.VerifySubjectDigest(tt.hash) + if !cmp.Equal(err, tt.expected, cmpopts.EquateErrors()) { + t.Errorf(cmp.Diff(err, tt.expected, cmpopts.EquateErrors())) + } + }) + } +} + +func Test_VerifySummary(t *testing.T) { + t.Parallel() + tests := []struct { + name string + path string + hash string + expected error + }{ + { + name: "valid gcb provenance", + path: "./testdata/gcloud-container-github.json", + hash: "1a033b002f89ed2b8ea733162497fb70f1a4049a7f8602d6a33682b4ad9921fd", + }, + { + name: "mismatch digest", + path: "./testdata/gcloud-container-github.json", + hash: "2a033b002f89ed2b8ea733162497fb70f1a4049a7f8602d6a33682b4ad9921fd", + expected: serrors.ErrorMismatchHash, + }, + { + name: "mismatch fuly qualified digest", + path: "./testdata/gcloud-container-invalid-fullyqualifieddigest.json", + hash: "1a033b002f89ed2b8ea733162497fb70f1a4049a7f8602d6a33682b4ad9921fd", + expected: serrors.ErrorMismatchHash, + }, + } + 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)) + } + + prov, err := ProvenanceFromBytes(content) + if err != nil { + panic(fmt.Errorf("ProvenanceFromBytes: %w", err)) + } + + if err := setStatement(prov); err != nil { + panic(fmt.Errorf("ProvenanceFromBytes: %w", err)) + } + + provenanceOpts := options.ProvenanceOpts{ + ExpectedDigest: tt.hash, + } + err = prov.VerifySummary(&provenanceOpts) + if !cmp.Equal(err, tt.expected, cmpopts.EquateErrors()) { + t.Errorf(cmp.Diff(err, tt.expected, cmpopts.EquateErrors())) + } + }) + } +} + +func Test_VerifyMetadata(t *testing.T) { + t.Parallel() + tests := []struct { + name string + path string + hash string + expected error + }{ + { + name: "valid gcb provenance", + path: "./testdata/gcloud-container-github.json", + hash: "1a033b002f89ed2b8ea733162497fb70f1a4049a7f8602d6a33682b4ad9921fd", + }, + { + name: "mismatch hash", + path: "./testdata/gcloud-container-github.json", + hash: "2a033b002f89ed2b8ea733162497fb70f1a4049a7f8602d6a33682b4ad9921fd", + expected: serrors.ErrorMismatchHash, + }, + { + name: "invalid kind", + path: "./testdata/gcloud-container-invalid-kind.json", + hash: "1a033b002f89ed2b8ea733162497fb70f1a4049a7f8602d6a33682b4ad9921fd", + expected: serrors.ErrorInvalidFormat, + }, + } + 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)) + } + + prov, err := ProvenanceFromBytes(content) + if err != nil { + panic(fmt.Errorf("ProvenanceFromBytes: %w", err)) + } + + if err := setStatement(prov); err != nil { + panic(fmt.Errorf("ProvenanceFromBytes: %w", err)) + } + + provenanceOpts := options.ProvenanceOpts{ + ExpectedDigest: tt.hash, + } + err = prov.VerifyMetadata(&provenanceOpts) + if !cmp.Equal(err, tt.expected, cmpopts.EquateErrors()) { + t.Errorf(cmp.Diff(err, tt.expected, cmpopts.EquateErrors())) + } + }) + } +} + +func Test_VerifyTextProvenance(t *testing.T) { + t.Parallel() + tests := []struct { + name string + path string + alter bool + expected error + }{ + { + name: "valid gcb provenance", + path: "./testdata/gcloud-container-github.json", + }, + { + name: "mismatch everything", + path: "./testdata/gcloud-container-github.json", + alter: true, + expected: serrors.ErrorMismatchIntoto, + }, + } + 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)) + } + + prov, err := ProvenanceFromBytes(content) + if err != nil { + panic(fmt.Errorf("ProvenanceFromBytes: %w", err)) + } + + if err := setStatement(prov); err != nil { + panic(fmt.Errorf("ProvenanceFromBytes: %w", err)) + } + + if !tt.alter { + err = prov.VerifyTextProvenance() + if !cmp.Equal(err, tt.expected, cmpopts.EquateErrors()) { + t.Errorf(cmp.Diff(err, tt.expected, cmpopts.EquateErrors())) + } + return + } + + // Alter fields. + cpy, err := json.Marshal(prov.verifiedProvenance.Build.UnverifiedTextIntotoStatement) + if err != nil { + panic(err) + } + chars := map[byte]bool{',': true, ':': true, '[': true, ']': true, '{': true, '}': true, '"': true} + patch := []byte(strings.Clone(string(cpy))) + i := 0 + for i < len(patch) { + // If it's a character that changes the JSON format, ignore it. + if _, ok := chars[patch[i]]; ok { + i = i + 1 + continue + } + + ni, ctned := isFieldName(i, patch) + if !ctned { + i = ni + continue + } + + // Update the string representation. + if len(patch[i:]) >= 5 && string(patch[i:i+5]) == "false" { + // Update `false` booleans. + t := append([]byte("true"), patch[i+5:]...) + patch = append(patch[:i], t...) + i += 4 + } else if len(patch[i:]) >= 4 && string(patch[i:i+4]) == "true" { + // Update `true` booleans. + t := append([]byte("false"), patch[i+4:]...) + patch = append(patch[:i], t...) + i += 5 + } else { + // Update characters. + patch[i] += 1 + } + + if err = json.Unmarshal(patch, &prov.verifiedProvenance.Build.UnverifiedTextIntotoStatement); err != nil { + // If we updated a characters that make a non-string fiel invalid, like Time, unmarshalin will fail, + // and we ignore the error. + i += 1 + patch = []byte(strings.Clone(string(cpy))) + continue + } + err = prov.VerifyTextProvenance() + if !cmp.Equal(err, tt.expected, cmpopts.EquateErrors()) { + t.Errorf(cmp.Diff(err, tt.expected, cmpopts.EquateErrors())) + } + // Start with the original string value. + patch = []byte(strings.Clone(string(cpy))) + i += 1 + } + }) + } +} + +func isFieldName(i int, content []byte) (int, bool) { + j := i + for j < len(content) { + if string(content[j]) == "}" || + string(content[j]) == "," { + return i, true + } + if string(content[j:j+2]) == "\":" { + i = j + 2 + return i, false + } + j += 1 + } + return i, true +} + +func Test_VerifyBranch(t *testing.T) { + t.Parallel() + tests := []struct { + name string + path string + branch string + expected error + }{ + { + name: "valid gcb provenance", + path: "./testdata/gcloud-container-github.json", + branch: "master", + expected: serrors.ErrorNotSupported, + }, + } + 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)) + } + + prov, err := ProvenanceFromBytes(content) + if err != nil { + panic(fmt.Errorf("ProvenanceFromBytes: %w", err)) + } + + if err := setStatement(prov); err != nil { + panic(fmt.Errorf("ProvenanceFromBytes: %w", err)) + } + + err = prov.VerifyBranch(tt.branch) + if !cmp.Equal(err, tt.expected, cmpopts.EquateErrors()) { + t.Errorf(cmp.Diff(err, tt.expected, cmpopts.EquateErrors())) + } + }) + } +} + +func Test_VerifyTag(t *testing.T) { + t.Parallel() + tests := []struct { + name string + path string + tag string + expected error + }{ + { + name: "valid gcb provenance", + path: "./testdata/gcloud-container-github.json", + tag: "v1.2.3", + expected: serrors.ErrorNotSupported, + }, + } + 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)) + } + + prov, err := ProvenanceFromBytes(content) + if err != nil { + panic(fmt.Errorf("ProvenanceFromBytes: %w", err)) + } + + if err := setStatement(prov); err != nil { + panic(fmt.Errorf("ProvenanceFromBytes: %w", err)) + } + + err = prov.VerifyTag(tt.tag) + if !cmp.Equal(err, tt.expected, cmpopts.EquateErrors()) { + t.Errorf(cmp.Diff(err, tt.expected, cmpopts.EquateErrors())) + } + }) + } +} + +func Test_VerifyVersionedTag(t *testing.T) { + t.Parallel() + tests := []struct { + name string + path string + tag string + expected error + }{ + { + name: "valid gcb provenance", + path: "./testdata/gcloud-container-github.json", + tag: "v1.2.3", + expected: serrors.ErrorNotSupported, + }, + } + 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)) + } + + prov, err := ProvenanceFromBytes(content) + if err != nil { + panic(fmt.Errorf("ProvenanceFromBytes: %w", err)) + } + + if err := setStatement(prov); err != nil { + panic(fmt.Errorf("ProvenanceFromBytes: %w", err)) + } + + err = prov.VerifyVersionedTag(tt.tag) + if !cmp.Equal(err, tt.expected, cmpopts.EquateErrors()) { + t.Errorf(cmp.Diff(err, tt.expected, cmpopts.EquateErrors())) + } + }) + } +} diff --git a/verifiers/internal/gcb/verifier.go b/verifiers/internal/gcb/verifier.go index 9ea8d4a4b..a1ef8eb33 100644 --- a/verifiers/internal/gcb/verifier.go +++ b/verifiers/internal/gcb/verifier.go @@ -88,6 +88,13 @@ func (v *GCBVerifier) VerifyImage(ctx context.Context, return nil, "", err } + // Verify the text provenance. + // This is an additional structure that GCB prepends to the provenance, + // intended for humans. It reflect the DSSE payload. + if err = prov.VerifyTextProvenance(); err != nil { + return nil, "", err + } + // Verify branch. if provenanceOpts.ExpectedBranch != nil { if err = prov.VerifyBranch(*provenanceOpts.ExpectedBranch); err != nil {