Repository: sigstore/rekor
Pull request #321 (2021-06-02 11:49:10)

Attestation Transparency!
This adds an "Attestation" method to the entry interface. Entries can
return an attestation that they would like to store.

The attestations are currently stored in GCS, but it supports any blob store.
The feature is turned off with a flag, and we can set a max size as well.

TODO: 

- [x] End to End tests
- [x] Size limits for attestations
- [x] Figure out storage key (UUID of entry or of attestation? How to verify attestation hash? We store only the "statement", not the overall envelope. Maybe store both?)

Signed-off-by: Dan Lorenc <dlorenc@google.com>

<!--
Thanks for opening a pull request!

Please remember to:
- mention any issue(s) that this PR closes using a closing keyword as well as the issue number, such as "Closes #XYZ" or "Resolves sigstore/repo-name#XYZ", cf.
  [documentation](https://docs.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword)
- ensure your commits are signed-off, as sigstore uses the [DCO](https://github.com/enarx/enarx/wiki/How-to-contribute-code#developer-certificate-of-origin) using `git commit -s`, or `git commit -s --amend` if you want to amend already existing commits
- lastly, ensure there are no merge commits!
Thank you :)
-->



Patch:
diff --git a/cmd/rekor-cli/app/pflags.go b/cmd/rekor-cli/app/pflags.go
index fe33f49..61472e6 100644
--- a/cmd/rekor-cli/app/pflags.go
+++ b/cmd/rekor-cli/app/pflags.go
@@ -235,7 +235,7 @@ func CreateIntotoFromPFlags() (models.ProposedEntry, error) {
 	re := intoto_v001.V001Entry{
 		IntotoObj: models.IntotoV001Schema{
 			Content: &models.IntotoV001SchemaContent{
-				Envelope: swag.String(string(b)),
+				Envelope: string(b),
 			},
 			PublicKey: &kb,
 		},
diff --git a/cmd/rekor-server/app/root.go b/cmd/rekor-server/app/root.go
index c8f01ac..0b11aba 100644
--- a/cmd/rekor-server/app/root.go
+++ b/cmd/rekor-server/app/root.go
@@ -69,6 +69,10 @@ func init() {
 	rootCmd.PersistentFlags().String("redis_server.address", "127.0.0.1", "Redis server address")
 	rootCmd.PersistentFlags().Uint16("redis_server.port", 6379, "Redis server port")
 
+	rootCmd.PersistentFlags().Bool("enable_attestation_storage", false, "enables rich attestation storage")
+	rootCmd.PersistentFlags().String("attestation_storage_bucket", "", "url for attestation storage bucket")
+	rootCmd.PersistentFlags().Int("max_attestation_size", 100*1024, "max size for attestation storage, in bytes")
+
 	if err := viper.BindPFlags(rootCmd.PersistentFlags()); err != nil {
 		log.Logger.Fatal(err)
 	}
diff --git a/docker-compose.yml b/docker-compose.yml
index c08186f..cd75a61 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -87,9 +87,13 @@ services:
       "--redis_server.port=6379",
       "--rekor_server.address=0.0.0.0",
       "--rekor_server.signer=memory",
+      "--enable_attestation_storage",
+      "--attestation_storage_bucket=file:///var/run/attestations",
       # Uncomment this for production logging
       # "--log_type=prod",
       ]
+    volumes:
+    - "/var/run/attestations:/var/run/attestations"
     restart: always # keep the server running
     ports:
       - "3000:3000"
diff --git a/go.mod b/go.mod
index e604140..2580ef8 100644
--- a/go.mod
+++ b/go.mod
@@ -11,12 +11,13 @@ require (
 	github.com/go-chi/chi v4.1.2+incompatible
 	github.com/go-openapi/errors v0.20.0
 	github.com/go-openapi/loads v0.20.2
-	github.com/go-openapi/runtime v0.19.28
+	github.com/go-openapi/runtime v0.19.29
 	github.com/go-openapi/spec v0.20.3
 	github.com/go-openapi/strfmt v0.20.1
 	github.com/go-openapi/swag v0.19.15
 	github.com/go-openapi/validate v0.20.2
 	github.com/go-playground/validator v9.31.0+incompatible
+	github.com/google/go-cmp v0.5.5
 	github.com/google/rpmpack v0.0.0-20210107155803-d6befbf05148
 	github.com/google/trillian v1.3.14-0.20210413093047-5e12fb368c8f
 	github.com/in-toto/in-toto-golang v0.1.1-0.20210528150343-f7dc21abaccf
diff --git a/go.sum b/go.sum
index f5091b9..f113f74 100644
--- a/go.sum
+++ b/go.sum
@@ -253,8 +253,6 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
 github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
 github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
-github.com/dlorenc/in-toto-golang v0.0.0-20210526022216-b77043a23d1d h1:VOo774HqWGdZVX+Pwhb3fTbXfkZ5wDzdmdlzfq4WsO8=
-github.com/dlorenc/in-toto-golang v0.0.0-20210526022216-b77043a23d1d/go.mod h1:kOcoAhaukFZpRm6D53dd2xB++q065UxKi938k81l1aM=
 github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
 github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
@@ -378,8 +376,8 @@ github.com/go-openapi/runtime v0.19.16/go.mod h1:5P9104EJgYcizotuXhEuUrzVc+j1RiS
 github.com/go-openapi/runtime v0.19.24/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk=
 github.com/go-openapi/runtime v0.19.26/go.mod h1:BvrQtn6iVb2QmiVXRsFAm6ZCAZBpbVKFfN6QWCp582M=
 github.com/go-openapi/runtime v0.19.27/go.mod h1:BvrQtn6iVb2QmiVXRsFAm6ZCAZBpbVKFfN6QWCp582M=
-github.com/go-openapi/runtime v0.19.28 h1:9lYu6axek8LJrVkMVViVirRcpoaCxXX7+sSvmizGVnA=
-github.com/go-openapi/runtime v0.19.28/go.mod h1:BvrQtn6iVb2QmiVXRsFAm6ZCAZBpbVKFfN6QWCp582M=
+github.com/go-openapi/runtime v0.19.29 h1:5IIvCaIDbxetN674vX9eOxvoZ9mYGQ16fV1Q0VSG+NA=
+github.com/go-openapi/runtime v0.19.29/go.mod h1:BvrQtn6iVb2QmiVXRsFAm6ZCAZBpbVKFfN6QWCp582M=
 github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
 github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
 github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
diff --git a/pkg/api/api.go b/pkg/api/api.go
index dd6980c..4537b77 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -34,6 +34,7 @@ import (
 	"github.com/sigstore/rekor/pkg/log"
 	pki "github.com/sigstore/rekor/pkg/pki/x509"
 	"github.com/sigstore/rekor/pkg/signer"
+	"github.com/sigstore/rekor/pkg/storage"
 	"github.com/sigstore/sigstore/pkg/signature"
 )
 
@@ -146,8 +147,9 @@ func NewAPI() (*API, error) {
 }
 
 var (
-	api         *API
-	redisClient radix.Client
+	api           *API
+	redisClient   radix.Client
+	storageClient storage.AttestationStorage
 )
 
 func ConfigureAPI() {
@@ -163,4 +165,11 @@ func ConfigureAPI() {
 			log.Logger.Panic(err)
 		}
 	}
+
+	if viper.GetBool("enable_attestation_storage") {
+		storageClient, err = storage.NewAttestationStorage()
+		if err != nil {
+			log.Logger.Panic(err)
+		}
+	}
 }
diff --git a/pkg/api/entries.go b/pkg/api/entries.go
index 69b649a..cc8ef34 100644
--- a/pkg/api/entries.go
+++ b/pkg/api/entries.go
@@ -160,6 +160,20 @@ func CreateLogEntryHandler(params entries.CreateLogEntryParams) middleware.Respo
 		}()
 	}
 
+	if viper.GetBool("enable_attestation_storage") {
+
+		go func() {
+			typ, attestation := entry.Attestation()
+			if typ == "" {
+				log.RequestIDLogger(params.HTTPRequest).Infof("no attestation for %s", uuid)
+				return
+			}
+			if err := storeAttestation(context.Background(), uuid, typ, attestation); err != nil {
+				log.RequestIDLogger(params.HTTPRequest).Errorf("error storing attestation: %s", err)
+			}
+		}()
+	}
+
 	payload, err := logEntryAnon.MarshalBinary()
 	if err != nil {
 		return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("signing error: %v", err), signingError)
diff --git a/pkg/api/error.go b/pkg/api/error.go
index c614499..bb87015 100644
--- a/pkg/api/error.go
+++ b/pkg/api/error.go
@@ -89,9 +89,10 @@ func handleRekorAPIError(params interface{}, code int, err error, message string
 			return entries.NewGetLogEntryByUUIDDefault(code).WithPayload(errorMsg(message, code))
 		}
 	case entries.CreateLogEntryParams:
-		logMsg(params.HTTPRequest)
 		switch code {
+		// We treat "duplicate entry" as an error, but it's not really an error, so we don't need to log it as one.
 		case http.StatusBadRequest:
+			logMsg(params.HTTPRequest)
 			return entries.NewCreateLogEntryBadRequest().WithPayload(errorMsg(message, code))
 		case http.StatusConflict:
 			resp := entries.NewCreateLogEntryConflict().WithPayload(errorMsg(message, code))
diff --git a/pkg/api/index.go b/pkg/api/index.go
index 42606ca..5815545 100644
--- a/pkg/api/index.go
+++ b/pkg/api/index.go
@@ -92,3 +92,7 @@ func SearchIndexNotImplementedHandler(params index.SearchIndexParams) middleware
 func addToIndex(ctx context.Context, key, value string) error {
 	return redisClient.Do(ctx, radix.Cmd(nil, "LPUSH", key, value))
 }
+
+func storeAttestation(ctx context.Context, uuid, attestationType string, attestation []byte) error {
+	return storageClient.StoreAttestation(ctx, uuid, attestationType, attestation)
+}
diff --git a/pkg/generated/models/intoto_v001_schema.go b/pkg/generated/models/intoto_v001_schema.go
index 22df0d1..7d2a036 100644
--- a/pkg/generated/models/intoto_v001_schema.go
+++ b/pkg/generated/models/intoto_v001_schema.go
@@ -148,8 +148,7 @@ func (m *IntotoV001Schema) UnmarshalBinary(b []byte) error {
 type IntotoV001SchemaContent struct {
 
 	// envelope
-	// Required: true
-	Envelope *string `json:"envelope"`
+	Envelope string `json:"envelope,omitempty"`
 
 	// hash
 	Hash *IntotoV001SchemaContentHash `json:"hash,omitempty"`
@@ -159,10 +158,6 @@ type IntotoV001SchemaContent struct {
 func (m *IntotoV001SchemaContent) Validate(formats strfmt.Registry) error {
 	var res []error
 
-	if err := m.validateEnvelope(formats); err != nil {
-		res = append(res, err)
-	}
-
 	if err := m.validateHash(formats); err != nil {
 		res = append(res, err)
 	}
@@ -173,15 +168,6 @@ func (m *IntotoV001SchemaContent) Validate(formats strfmt.Registry) error {
 	return nil
 }
 
-func (m *IntotoV001SchemaContent) validateEnvelope(formats strfmt.Registry) error {
-
-	if err := validate.Required("content"+"."+"envelope", "body", m.Envelope); err != nil {
-		return err
-	}
-
-	return nil
-}
-
 func (m *IntotoV001SchemaContent) validateHash(formats strfmt.Registry) error {
 	if swag.IsZero(m.Hash) { // not required
 		return nil
diff --git a/pkg/generated/restapi/embedded_spec.go b/pkg/generated/restapi/embedded_spec.go
index ed0968a..0a1bbe1 100644
--- a/pkg/generated/restapi/embedded_spec.go
+++ b/pkg/generated/restapi/embedded_spec.go
@@ -1260,9 +1260,6 @@ func init() {
     },
     "IntotoV001SchemaContent": {
       "type": "object",
-      "required": [
-        "envelope"
-      ],
       "properties": {
         "envelope": {
           "description": "envelope",
@@ -1978,9 +1975,6 @@ func init() {
       "properties": {
         "content": {
           "type": "object",
-          "required": [
-            "envelope"
-          ],
           "properties": {
             "envelope": {
               "description": "envelope",
diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go
new file mode 100644
index 0000000..ad8d13d
--- /dev/null
+++ b/pkg/storage/storage.go
@@ -0,0 +1,67 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package storage
+
+import (
+	"context"
+	"errors"
+
+	"github.com/sigstore/rekor/pkg/log"
+
+	"github.com/spf13/viper"
+	"gocloud.dev/blob"
+
+	// Blank imports to register storage
+	_ "gocloud.dev/blob/fileblob"
+	_ "gocloud.dev/blob/gcsblob"
+	_ "gocloud.dev/blob/memblob"
+)
+
+type AttestationStorage interface {
+	StoreAttestation(ctx context.Context, key string, attestationType string, attestation []byte) error
+}
+
+func NewAttestationStorage() (AttestationStorage, error) {
+	if url := viper.GetString("attestation_storage_bucket"); url != "" {
+		log.Logger.Infof("Configuring attestation storage at %s", url)
+		bucket, err := blob.OpenBucket(context.Background(), url)
+		if err != nil {
+			return nil, err
+		}
+		return &Blob{
+			bucket: bucket,
+		}, nil
+	}
+	return nil, errors.New("no storage configured")
+}
+
+type Blob struct {
+	bucket *blob.Bucket
+}
+
+func (b *Blob) StoreAttestation(ctx context.Context, key, attestationType string, attestation []byte) error {
+	log.Logger.Infof("storing attestation of type %s at %s", attestationType, key)
+	w, err := b.bucket.NewWriter(ctx, key, &blob.WriterOptions{
+		ContentType: attestationType,
+	})
+	if err != nil {
+		return err
+	}
+	if _, err := w.Write(attestation); err != nil {
+		return err
+	}
+	return w.Close()
+}
diff --git a/pkg/types/entries.go b/pkg/types/entries.go
index 1b38a4f..a22f790 100644
--- a/pkg/types/entries.go
+++ b/pkg/types/entries.go
@@ -35,6 +35,7 @@ type EntryImpl interface {
 	HasExternalEntities() bool                        // indicates whether there is a need fetch any additional external content required to process the entry
 	Unmarshal(e models.ProposedEntry) error           // unmarshal the abstract entry into the specific struct for this versioned type
 	Validate() error                                  // performs any cross-field validation that is not expressed in the OpenAPI spec
+	Attestation() (string, []byte)
 }
 
 // EntryFactory describes a factory function that can generate structs for a specific versioned type
diff --git a/pkg/types/intoto/v0.0.1/entry.go b/pkg/types/intoto/v0.0.1/entry.go
index ac48263..3ebd86b 100644
--- a/pkg/types/intoto/v0.0.1/entry.go
+++ b/pkg/types/intoto/v0.0.1/entry.go
@@ -26,6 +26,7 @@ import (
 	"errors"
 
 	"github.com/in-toto/in-toto-golang/pkg/ssl"
+	"github.com/spf13/viper"
 
 	"github.com/go-openapi/strfmt"
 	"github.com/go-openapi/swag"
@@ -93,13 +94,7 @@ func (v *V001Entry) Unmarshal(pe models.ProposedEntry) error {
 		return err
 	}
 
-	if err := json.Unmarshal([]byte(*v.IntotoObj.Content.Envelope), &v.env); err != nil {
-		return err
-	}
-
-	// cross field validation
-	return nil
-
+	return v.Validate()
 }
 
 func (v V001Entry) HasExternalEntities() bool {
@@ -120,7 +115,7 @@ func (v *V001Entry) Canonicalize(ctx context.Context) ([]byte, error) {
 	}
 	pkb := strfmt.Base64(pk)
 
-	h := sha256.Sum256([]byte(*v.IntotoObj.Content.Envelope))
+	h := sha256.Sum256([]byte(v.IntotoObj.Content.Envelope))
 
 	canonicalEntry := models.IntotoV001Schema{
 		PublicKey: &pkb,
@@ -132,9 +127,6 @@ func (v *V001Entry) Canonicalize(ctx context.Context) ([]byte, error) {
 		},
 	}
 
-	// TODO: if the envelope kind is correct (application/vnd..), inspect the payload itself too
-	// We can use the Subject/Materials fields to build up an index.
-
 	itObj := models.Intoto{}
 	itObj.APIVersion = swag.String(APIVERSION)
 	itObj.Spec = &canonicalEntry
@@ -148,24 +140,18 @@ func (v *V001Entry) Canonicalize(ctx context.Context) ([]byte, error) {
 }
 
 // Validate performs cross-field validation for fields in object
-func (v V001Entry) Validate() error {
+func (v *V001Entry) Validate() error {
 	// TODO handle multiple
 	sslVerifier, err := ssl.NewEnvelopeSigner(&verifier{pub: v.keyObj})
 	if err != nil {
 		return err
 	}
 
-	sslEnv := &ssl.Envelope{
-		PayloadType: v.env.PayloadType,
-		Payload:     string(v.env.Payload),
-		Signatures: []ssl.Signature{
-			{
-				Sig: string(v.env.Signatures[0].Sig),
-			},
-		},
+	if err := json.Unmarshal([]byte(v.IntotoObj.Content.Envelope), &v.env); err != nil {
+		return err
 	}
 
-	ok, err := sslVerifier.Verify(sslEnv)
+	ok, err := sslVerifier.Verify(&v.env)
 	if err != nil {
 		return err
 	}
@@ -176,6 +162,14 @@ func (v V001Entry) Validate() error {
 	return nil
 }
 
+func (v *V001Entry) Attestation() (string, []byte) {
+	if len(v.env.Payload) > viper.GetInt("max_attestation_size") {
+		log.Logger.Infof("Skipping attestation storage, size %d is greater than max %d", len(v.env.Payload), viper.GetInt("max_attestation_size"))
+		return "", nil
+	}
+	return v.env.PayloadType, []byte(v.env.Payload)
+}
+
 type verifier struct {
 	pub    pki.PublicKey
 	signer crypto.Signer
diff --git a/pkg/types/intoto/v0.0.1/intoto_v0_0_1_schema.json b/pkg/types/intoto/v0.0.1/intoto_v0_0_1_schema.json
index 33d2b90..9a1b8c5 100644
--- a/pkg/types/intoto/v0.0.1/intoto_v0_0_1_schema.json
+++ b/pkg/types/intoto/v0.0.1/intoto_v0_0_1_schema.json
@@ -33,10 +33,7 @@
                         "value"
                     ]
                 }
-            },
-            "required": [
-                "envelope"
-            ]
+            }
         },
         "publicKey": {
             "description": "The public key that can verify the signature",
diff --git a/pkg/types/jar/v0.0.1/entry.go b/pkg/types/jar/v0.0.1/entry.go
index aa30853..1f4f821 100644
--- a/pkg/types/jar/v0.0.1/entry.go
+++ b/pkg/types/jar/v0.0.1/entry.go
@@ -312,3 +312,7 @@ func extractPKCS7SignatureFromJAR(inz *zip.Reader) ([]byte, error) {
 	}
 	return nil, errors.New("unable to locate signature in JAR file")
 }
+
+func (v V001Entry) Attestation() (string, []byte) {
+	return "", nil
+}
diff --git a/pkg/types/rekord/v0.0.1/entry.go b/pkg/types/rekord/v0.0.1/entry.go
index 77c2abe..5373f13 100644
--- a/pkg/types/rekord/v0.0.1/entry.go
+++ b/pkg/types/rekord/v0.0.1/entry.go
@@ -379,3 +379,7 @@ func (v V001Entry) Validate() error {
 
 	return nil
 }
+
+func (v V001Entry) Attestation() (string, []byte) {
+	return "", nil
+}
diff --git a/pkg/types/rpm/v0.0.1/entry.go b/pkg/types/rpm/v0.0.1/entry.go
index 29a17c7..f8d31cd 100644
--- a/pkg/types/rpm/v0.0.1/entry.go
+++ b/pkg/types/rpm/v0.0.1/entry.go
@@ -359,3 +359,7 @@ func (v V001Entry) Validate() error {
 
 	return nil
 }
+
+func (v V001Entry) Attestation() (string, []byte) {
+	return "", nil
+}


Test patch:
diff --git a/pkg/types/intoto/intoto_test.go b/pkg/types/intoto/intoto_test.go
index 4858f90..01b91d1 100644
--- a/pkg/types/intoto/intoto_test.go
+++ b/pkg/types/intoto/intoto_test.go
@@ -62,6 +62,10 @@ func (u UnmarshalTester) Unmarshal(pe models.ProposedEntry) error {
 	return nil
 }
 
+func (u UnmarshalTester) Attestation() (string, []byte) {
+	return "", nil
+}
+
 type UnmarshalFailsTester struct {
 	UnmarshalTester
 }
diff --git a/pkg/types/intoto/v0.0.1/entry_test.go b/pkg/types/intoto/v0.0.1/entry_test.go
index d8155a8..dfa3e4b 100644
--- a/pkg/types/intoto/v0.0.1/entry_test.go
+++ b/pkg/types/intoto/v0.0.1/entry_test.go
@@ -28,7 +28,6 @@ import (
 	"testing"
 
 	"github.com/go-openapi/strfmt"
-	"github.com/go-openapi/swag"
 	"github.com/in-toto/in-toto-golang/in_toto"
 	"github.com/in-toto/in-toto-golang/pkg/ssl"
 	"github.com/sigstore/rekor/pkg/generated/models"
@@ -51,7 +50,7 @@ func p(b []byte) *strfmt.Base64 {
 	return &b64
 }
 
-func envelope(t *testing.T, k *ecdsa.PrivateKey, payload, payloadType string) *string {
+func envelope(t *testing.T, k *ecdsa.PrivateKey, payload, payloadType string) string {
 
 	signer, err := in_toto.NewSSLSigner(&verifier{
 		signer: k,
@@ -68,8 +67,7 @@ func envelope(t *testing.T, k *ecdsa.PrivateKey, payload, payloadType string) *s
 		t.Fatal(err)
 	}
 
-	s := string(b)
-	return &s
+	return string(b)
 }
 
 func TestV001Entry_Unmarshal(t *testing.T) {
@@ -140,7 +138,7 @@ func TestV001Entry_Unmarshal(t *testing.T) {
 			it: &models.IntotoV001Schema{
 				PublicKey: p(pub),
 				Content: &models.IntotoV001SchemaContent{
-					Envelope: swag.String(string(invalid)),
+					Envelope: string(invalid),
 				},
 			},
 			wantErr: true,
diff --git a/pkg/types/jar/jar_test.go b/pkg/types/jar/jar_test.go
index e901bc0..9c3a609 100644
--- a/pkg/types/jar/jar_test.go
+++ b/pkg/types/jar/jar_test.go
@@ -61,6 +61,10 @@ func (u UnmarshalTester) Validate() error {
 	return nil
 }
 
+func (u UnmarshalTester) Attestation() (string, []byte) {
+	return "", nil
+}
+
 type UnmarshalFailsTester struct {
 	UnmarshalTester
 }
diff --git a/pkg/types/rekord/rekord_test.go b/pkg/types/rekord/rekord_test.go
index fdb7e99..9463bca 100644
--- a/pkg/types/rekord/rekord_test.go
+++ b/pkg/types/rekord/rekord_test.go
@@ -62,6 +62,10 @@ func (u UnmarshalTester) Unmarshal(pe models.ProposedEntry) error {
 	return nil
 }
 
+func (u UnmarshalTester) Attestation() (string, []byte) {
+	return "", nil
+}
+
 type UnmarshalFailsTester struct {
 	UnmarshalTester
 }
diff --git a/pkg/types/rpm/rpm_test.go b/pkg/types/rpm/rpm_test.go
index 3a9670b..e1ff77b 100644
--- a/pkg/types/rpm/rpm_test.go
+++ b/pkg/types/rpm/rpm_test.go
@@ -62,6 +62,10 @@ func (u UnmarshalTester) Validate() error {
 	return nil
 }
 
+func (u UnmarshalTester) Attestation() (string, []byte) {
+	return "", nil
+}
+
 type UnmarshalFailsTester struct {
 	UnmarshalTester
 }
diff --git a/tests/e2e_test.go b/tests/e2e_test.go
index f7a33b7..b42229d 100644
--- a/tests/e2e_test.go
+++ b/tests/e2e_test.go
@@ -23,6 +23,7 @@ import (
 	"crypto/ecdsa"
 	"crypto/sha256"
 	"crypto/x509"
+	"encoding/base64"
 	"encoding/hex"
 	"encoding/json"
 	"encoding/pem"
@@ -39,6 +40,7 @@ import (
 	"github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer"
 	"github.com/go-openapi/strfmt"
 	"github.com/go-openapi/swag"
+	"github.com/google/go-cmp/cmp"
 	"github.com/in-toto/in-toto-golang/in_toto"
 	"github.com/in-toto/in-toto-golang/pkg/ssl"
 	"github.com/sigstore/rekor/cmd/rekor-cli/app"
@@ -291,7 +293,11 @@ func TestIntoto(t *testing.T) {
 	attestationPath := filepath.Join(td, "attestation.json")
 	pubKeyPath := filepath.Join(td, "pub.pem")
 
-	it := in_toto.Statement{
+	// Get some random data so it's unique each run
+	d := randomData(t, 10)
+	id := base64.StdEncoding.EncodeToString(d)
+
+	it := in_toto.ProvenanceStatement{
 		StatementHeader: in_toto.StatementHeader{
 			Type:          in_toto.StatementInTotoV01,
 			PredicateType: in_toto.PredicateProvenanceV01,
@@ -306,7 +312,7 @@ func TestIntoto(t *testing.T) {
 		},
 		Predicate: in_toto.ProvenancePredicate{
 			Builder: in_toto.ProvenanceBuilder{
-				ID: "foo",
+				ID: "foo" + id,
 			},
 		},
 	}
@@ -344,8 +350,32 @@ func TestIntoto(t *testing.T) {
 	// If we do it twice, it should already exist
 	out := runCli(t, "upload", "--artifact", attestationPath, "--type", "intoto", "--public-key", pubKeyPath)
 	outputContains(t, out, "Created entry at")
+	uuid := getUUIDFromUploadOutput(t, out)
+
+	// The atteestation should be stored at /var/run/attestations/$uuid
+	cmd := exec.Command("docker", "run", "-v", "/var/run/attestations:/var/run/attestations", "alpine", "cat", "/var/run/attestations/"+uuid)
+	b64, err := cmd.Output()
+	if err != nil {
+		t.Fatal(string(b64), err)
+	}
+	t.Logf("Read file contents: %s", string(b64))
+
+	attStr, err := base64.StdEncoding.DecodeString(string(b64))
+	if err != nil {
+		t.Fatal(err)
+	}
+	stored := in_toto.ProvenanceStatement{}
+	if err := json.Unmarshal([]byte(attStr), &stored); err != nil {
+		t.Error(err)
+	}
+
+	if diff := cmp.Diff(it, stored); diff != "" {
+		t.Errorf("diff: %s", diff)
+	}
+
 	out = runCli(t, "upload", "--artifact", attestationPath, "--type", "intoto", "--public-key", pubKeyPath)
 	outputContains(t, out, "Entry already exists")
+
 }
 
 func TestJARURL(t *testing.T) {
diff --git a/tests/rpm.go b/tests/rpm.go
index 5df862a..db63ce0 100644
--- a/tests/rpm.go
+++ b/tests/rpm.go
@@ -54,10 +54,7 @@ func createSignedRpm(t *testing.T, artifactPath string) {
 
 	rpm.SetPGPSigner(SignPGP)
 
-	data, err := randomData(100)
-	if err != nil {
-		t.Fatal(err)
-	}
+	data := randomData(t, 100)
 
 	rpm.AddFile(rpmpack.RPMFile{
 		Name:  randomRpmSuffix(),
diff --git a/tests/util.go b/tests/util.go
index 3f2659f..d0c484d 100644
--- a/tests/util.go
+++ b/tests/util.go
@@ -95,22 +95,20 @@ func readFile(t *testing.T, p string) string {
 	return strings.TrimSpace(string(b))
 }
 
-func randomData(n int) ([]byte, error) {
+func randomData(t *testing.T, n int) []byte {
+	t.Helper()
 	rand.Seed(time.Now().UnixNano())
 	data := make([]byte, n)
 	if _, err := rand.Read(data[:]); err != nil {
-		return nil, err
+		t.Fatal(err)
 	}
-	return data, nil
+	return data
 }
 
 func createArtifact(t *testing.T, artifactPath string) string {
 	t.Helper()
 	// First let's generate some random data so we don't have to worry about dupes.
-	data, err := randomData(100)
-	if err != nil {
-		t.Fatal(err)
-	}
+	data := randomData(t, 100)
 
 	artifact := base64.StdEncoding.EncodeToString(data[:])
 	// Write this to a file