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 {