Repository: sigstore/rekor
Pull request #314 (2021-05-25 14:54:42)

Support in-toto attestations
This adds support for storing ITE-6 Attestations inside a signed ITE-5 envelope in the transparency log!

This uses a custom fork of in-toto-golang because not all the changes are merged
in one place.

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 13864dd..fe33f49 100644
--- a/cmd/rekor-cli/app/pflags.go
+++ b/cmd/rekor-cli/app/pflags.go
@@ -36,6 +36,7 @@ import (
 
 	"github.com/go-playground/validator"
 	"github.com/sigstore/rekor/pkg/generated/models"
+	intoto_v001 "github.com/sigstore/rekor/pkg/types/intoto/v0.0.1"
 	jar_v001 "github.com/sigstore/rekor/pkg/types/jar/v0.0.1"
 	rekord_v001 "github.com/sigstore/rekor/pkg/types/rekord/v0.0.1"
 	rpm_v001 "github.com/sigstore/rekor/pkg/types/rpm/v0.0.1"
@@ -215,6 +216,37 @@ func CreateJarFromPFlags() (models.ProposedEntry, error) {
 	return &returnVal, nil
 }
 
+func CreateIntotoFromPFlags() (models.ProposedEntry, error) {
+	//TODO: how to select version of item to create
+	returnVal := models.Intoto{}
+
+	intoto := viper.GetString("artifact")
+	b, err := ioutil.ReadFile(filepath.Clean(intoto))
+	if err != nil {
+		return nil, err
+	}
+	publicKey := viper.GetString("public-key")
+	keyBytes, err := ioutil.ReadFile(filepath.Clean(publicKey))
+	if err != nil {
+		return nil, fmt.Errorf("error reading public key file: %w", err)
+	}
+	kb := strfmt.Base64(keyBytes)
+
+	re := intoto_v001.V001Entry{
+		IntotoObj: models.IntotoV001Schema{
+			Content: &models.IntotoV001SchemaContent{
+				Envelope: swag.String(string(b)),
+			},
+			PublicKey: &kb,
+		},
+	}
+
+	returnVal.Spec = re.IntotoObj
+	returnVal.APIVersion = swag.String(re.APIVersion())
+
+	return &returnVal, nil
+}
+
 func CreateRpmFromPFlags() (models.ProposedEntry, error) {
 	//TODO: how to select version of item to create
 	returnVal := models.Rpm{}
@@ -435,12 +467,13 @@ func (t *typeFlag) Set(s string) error {
 		"rekord": {},
 		"rpm":    {},
 		"jar":    {},
+		"intoto": {},
 	}
 	if _, ok := set[s]; ok {
 		t.value = s
 		return nil
 	}
-	return fmt.Errorf("value specified is invalid: [%s] supported values are: [rekord, rpm]", s)
+	return fmt.Errorf("value specified is invalid: [%s] supported values are: [rekord, rpm, jar, intoto]", s)
 }
 
 type pkiFormatFlag struct {
diff --git a/cmd/rekor-cli/app/upload.go b/cmd/rekor-cli/app/upload.go
index bce1542..665eebd 100644
--- a/cmd/rekor-cli/app/upload.go
+++ b/cmd/rekor-cli/app/upload.go
@@ -90,6 +90,11 @@ var uploadCmd = &cobra.Command{
 			if err != nil {
 				return nil, err
 			}
+		case "intoto":
+			entry, err = CreateIntotoFromPFlags()
+			if err != nil {
+				return nil, err
+			}
 		default:
 			return nil, errors.New("unknown type specified")
 		}
diff --git a/cmd/rekor-server/app/serve.go b/cmd/rekor-server/app/serve.go
index 7d81936..bb282b5 100644
--- a/cmd/rekor-server/app/serve.go
+++ b/cmd/rekor-server/app/serve.go
@@ -28,6 +28,8 @@ import (
 	"github.com/sigstore/rekor/pkg/generated/restapi"
 	"github.com/sigstore/rekor/pkg/generated/restapi/operations"
 	"github.com/sigstore/rekor/pkg/log"
+	"github.com/sigstore/rekor/pkg/types/intoto"
+	intoto_v001 "github.com/sigstore/rekor/pkg/types/intoto/v0.0.1"
 	"github.com/sigstore/rekor/pkg/types/jar"
 	jar_v001 "github.com/sigstore/rekor/pkg/types/jar/v0.0.1"
 	"github.com/sigstore/rekor/pkg/types/rekord"
@@ -73,6 +75,7 @@ var serveCmd = &cobra.Command{
 			rekord.KIND: rekord_v001.APIVERSION,
 			rpm.KIND:    rpm_v001.APIVERSION,
 			jar.KIND:    jar_v001.APIVERSION,
+			intoto.KIND: intoto_v001.APIVERSION,
 		}
 
 		for k, v := range pluggableTypeMap {
diff --git a/go.mod b/go.mod
index 6e0ab4a..e604140 100644
--- a/go.mod
+++ b/go.mod
@@ -19,6 +19,7 @@ require (
 	github.com/go-playground/validator v9.31.0+incompatible
 	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
 	github.com/jedisct1/go-minisign v0.0.0-20210106175330-e54e81d562c7
 	github.com/mediocregopher/radix/v4 v4.0.0-beta.1
 	github.com/mitchellh/go-homedir v1.1.0
@@ -46,5 +47,3 @@ require (
 	google.golang.org/protobuf v1.26.0
 	gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
 )
-
-// replace github.com/sassoftware/relic => ../relic-7.2.6
diff --git a/go.sum b/go.sum
index 88d18c9..f5091b9 100644
--- a/go.sum
+++ b/go.sum
@@ -214,6 +214,8 @@ github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h
 github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA=
 github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI=
 github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
+github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
+github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
 github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
 github.com/containerd/stargz-snapshotter/estargz v0.4.1/go.mod h1:x7Q9dg9QYb4+ELgxmo4gBUeJB0tl5dqH1Sdz0nJU1QM=
 github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
@@ -251,6 +253,8 @@ 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=
@@ -687,6 +691,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:
 github.com/imdario/mergo v0.3.4/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/in-toto/in-toto-golang v0.1.1-0.20210528150343-f7dc21abaccf h1:yysOUUcpkuGZ0BZUtL+whU22H56Hqya/p636tGceacc=
+github.com/in-toto/in-toto-golang v0.1.1-0.20210528150343-f7dc21abaccf/go.mod h1:kOcoAhaukFZpRm6D53dd2xB++q065UxKi938k81l1aM=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
@@ -986,6 +992,8 @@ github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/segmentio/ksuid v1.0.3/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
 github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+github.com/shibumi/go-pathspec v1.2.0 h1:KVKEDHYk7bQolRMs7nfzjT3SBOCgcXFJzccnj9bsGbA=
+github.com/shibumi/go-pathspec v1.2.0/go.mod h1:bDxCftD0fST3qXIlHoQ/fChsU4mWMVklXp1yPErQaaY=
 github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
 github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
 github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
diff --git a/openapi.yaml b/openapi.yaml
index 5953bbe..2827d95 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -318,7 +318,22 @@ definitions:
         - apiVersion
         - spec
       additionalProperties: false
-
+  intoto:
+    type: object
+    description: Intoto object
+    allOf:
+    - $ref: '#/definitions/ProposedEntry'
+    - properties:
+        apiVersion:
+          type: string
+          pattern: ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$
+        spec:
+          type: object
+          $ref: 'pkg/types/intoto/intoto_schema.json'
+      required:
+        - apiVersion
+        - spec
+      additionalProperties: false
   jar:
     type: object
     description: Java Archive (JAR)
diff --git a/pkg/generated/models/intoto.go b/pkg/generated/models/intoto.go
new file mode 100644
index 0000000..4f208de
--- /dev/null
+++ b/pkg/generated/models/intoto.go
@@ -0,0 +1,210 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// 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 models
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+
+	"github.com/go-openapi/errors"
+	"github.com/go-openapi/strfmt"
+	"github.com/go-openapi/swag"
+	"github.com/go-openapi/validate"
+)
+
+// Intoto Intoto object
+//
+// swagger:model intoto
+type Intoto struct {
+
+	// api version
+	// Required: true
+	// Pattern: ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$
+	APIVersion *string `json:"apiVersion"`
+
+	// spec
+	// Required: true
+	Spec IntotoSchema `json:"spec"`
+}
+
+// Kind gets the kind of this subtype
+func (m *Intoto) Kind() string {
+	return "intoto"
+}
+
+// SetKind sets the kind of this subtype
+func (m *Intoto) SetKind(val string) {
+}
+
+// UnmarshalJSON unmarshals this object with a polymorphic type from a JSON structure
+func (m *Intoto) UnmarshalJSON(raw []byte) error {
+	var data struct {
+
+		// api version
+		// Required: true
+		// Pattern: ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$
+		APIVersion *string `json:"apiVersion"`
+
+		// spec
+		// Required: true
+		Spec IntotoSchema `json:"spec"`
+	}
+	buf := bytes.NewBuffer(raw)
+	dec := json.NewDecoder(buf)
+	dec.UseNumber()
+
+	if err := dec.Decode(&data); err != nil {
+		return err
+	}
+
+	var base struct {
+		/* Just the base type fields. Used for unmashalling polymorphic types.*/
+
+		Kind string `json:"kind"`
+	}
+	buf = bytes.NewBuffer(raw)
+	dec = json.NewDecoder(buf)
+	dec.UseNumber()
+
+	if err := dec.Decode(&base); err != nil {
+		return err
+	}
+
+	var result Intoto
+
+	if base.Kind != result.Kind() {
+		/* Not the type we're looking for. */
+		return errors.New(422, "invalid kind value: %q", base.Kind)
+	}
+
+	result.APIVersion = data.APIVersion
+	result.Spec = data.Spec
+
+	*m = result
+
+	return nil
+}
+
+// MarshalJSON marshals this object with a polymorphic type to a JSON structure
+func (m Intoto) MarshalJSON() ([]byte, error) {
+	var b1, b2, b3 []byte
+	var err error
+	b1, err = json.Marshal(struct {
+
+		// api version
+		// Required: true
+		// Pattern: ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$
+		APIVersion *string `json:"apiVersion"`
+
+		// spec
+		// Required: true
+		Spec IntotoSchema `json:"spec"`
+	}{
+
+		APIVersion: m.APIVersion,
+
+		Spec: m.Spec,
+	})
+	if err != nil {
+		return nil, err
+	}
+	b2, err = json.Marshal(struct {
+		Kind string `json:"kind"`
+	}{
+
+		Kind: m.Kind(),
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return swag.ConcatJSON(b1, b2, b3), nil
+}
+
+// Validate validates this intoto
+func (m *Intoto) Validate(formats strfmt.Registry) error {
+	var res []error
+
+	if err := m.validateAPIVersion(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if err := m.validateSpec(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+func (m *Intoto) validateAPIVersion(formats strfmt.Registry) error {
+
+	if err := validate.Required("apiVersion", "body", m.APIVersion); err != nil {
+		return err
+	}
+
+	if err := validate.Pattern("apiVersion", "body", *m.APIVersion, `^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (m *Intoto) validateSpec(formats strfmt.Registry) error {
+
+	if m.Spec == nil {
+		return errors.Required("spec", "body", nil)
+	}
+
+	return nil
+}
+
+// ContextValidate validate this intoto based on the context it is used
+func (m *Intoto) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
+	var res []error
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+// MarshalBinary interface implementation
+func (m *Intoto) MarshalBinary() ([]byte, error) {
+	if m == nil {
+		return nil, nil
+	}
+	return swag.WriteJSON(m)
+}
+
+// UnmarshalBinary interface implementation
+func (m *Intoto) UnmarshalBinary(b []byte) error {
+	var res Intoto
+	if err := swag.ReadJSON(b, &res); err != nil {
+		return err
+	}
+	*m = res
+	return nil
+}
diff --git a/pkg/generated/models/intoto_schema.go b/pkg/generated/models/intoto_schema.go
new file mode 100644
index 0000000..3533f9d
--- /dev/null
+++ b/pkg/generated/models/intoto_schema.go
@@ -0,0 +1,29 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// 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 models
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+// IntotoSchema Intoto Schema
+//
+// Intoto for Rekord objects
+//
+// swagger:model intotoSchema
+type IntotoSchema interface{}
diff --git a/pkg/generated/models/intoto_v001_schema.go b/pkg/generated/models/intoto_v001_schema.go
new file mode 100644
index 0000000..22df0d1
--- /dev/null
+++ b/pkg/generated/models/intoto_v001_schema.go
@@ -0,0 +1,351 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// 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 models
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"context"
+	"encoding/json"
+
+	"github.com/go-openapi/errors"
+	"github.com/go-openapi/strfmt"
+	"github.com/go-openapi/swag"
+	"github.com/go-openapi/validate"
+)
+
+// IntotoV001Schema intoto v0.0.1 Schema
+//
+// Schema for intoto object
+//
+// swagger:model intotoV001Schema
+type IntotoV001Schema struct {
+
+	// content
+	// Required: true
+	Content *IntotoV001SchemaContent `json:"content"`
+
+	// Arbitrary content to be included in the verifiable entry in the transparency log
+	ExtraData interface{} `json:"extraData,omitempty"`
+
+	// The public key that can verify the signature
+	// Required: true
+	// Format: byte
+	PublicKey *strfmt.Base64 `json:"publicKey"`
+}
+
+// Validate validates this intoto v001 schema
+func (m *IntotoV001Schema) Validate(formats strfmt.Registry) error {
+	var res []error
+
+	if err := m.validateContent(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if err := m.validatePublicKey(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+func (m *IntotoV001Schema) validateContent(formats strfmt.Registry) error {
+
+	if err := validate.Required("content", "body", m.Content); err != nil {
+		return err
+	}
+
+	if m.Content != nil {
+		if err := m.Content.Validate(formats); err != nil {
+			if ve, ok := err.(*errors.Validation); ok {
+				return ve.ValidateName("content")
+			}
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (m *IntotoV001Schema) validatePublicKey(formats strfmt.Registry) error {
+
+	if err := validate.Required("publicKey", "body", m.PublicKey); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// ContextValidate validate this intoto v001 schema based on the context it is used
+func (m *IntotoV001Schema) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
+	var res []error
+
+	if err := m.contextValidateContent(ctx, formats); err != nil {
+		res = append(res, err)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+func (m *IntotoV001Schema) contextValidateContent(ctx context.Context, formats strfmt.Registry) error {
+
+	if m.Content != nil {
+		if err := m.Content.ContextValidate(ctx, formats); err != nil {
+			if ve, ok := err.(*errors.Validation); ok {
+				return ve.ValidateName("content")
+			}
+			return err
+		}
+	}
+
+	return nil
+}
+
+// MarshalBinary interface implementation
+func (m *IntotoV001Schema) MarshalBinary() ([]byte, error) {
+	if m == nil {
+		return nil, nil
+	}
+	return swag.WriteJSON(m)
+}
+
+// UnmarshalBinary interface implementation
+func (m *IntotoV001Schema) UnmarshalBinary(b []byte) error {
+	var res IntotoV001Schema
+	if err := swag.ReadJSON(b, &res); err != nil {
+		return err
+	}
+	*m = res
+	return nil
+}
+
+// IntotoV001SchemaContent intoto v001 schema content
+//
+// swagger:model IntotoV001SchemaContent
+type IntotoV001SchemaContent struct {
+
+	// envelope
+	// Required: true
+	Envelope *string `json:"envelope"`
+
+	// hash
+	Hash *IntotoV001SchemaContentHash `json:"hash,omitempty"`
+}
+
+// Validate validates this intoto v001 schema content
+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)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	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
+	}
+
+	if m.Hash != nil {
+		if err := m.Hash.Validate(formats); err != nil {
+			if ve, ok := err.(*errors.Validation); ok {
+				return ve.ValidateName("content" + "." + "hash")
+			}
+			return err
+		}
+	}
+
+	return nil
+}
+
+// ContextValidate validate this intoto v001 schema content based on the context it is used
+func (m *IntotoV001SchemaContent) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
+	var res []error
+
+	if err := m.contextValidateHash(ctx, formats); err != nil {
+		res = append(res, err)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+func (m *IntotoV001SchemaContent) contextValidateHash(ctx context.Context, formats strfmt.Registry) error {
+
+	if m.Hash != nil {
+		if err := m.Hash.ContextValidate(ctx, formats); err != nil {
+			if ve, ok := err.(*errors.Validation); ok {
+				return ve.ValidateName("content" + "." + "hash")
+			}
+			return err
+		}
+	}
+
+	return nil
+}
+
+// MarshalBinary interface implementation
+func (m *IntotoV001SchemaContent) MarshalBinary() ([]byte, error) {
+	if m == nil {
+		return nil, nil
+	}
+	return swag.WriteJSON(m)
+}
+
+// UnmarshalBinary interface implementation
+func (m *IntotoV001SchemaContent) UnmarshalBinary(b []byte) error {
+	var res IntotoV001SchemaContent
+	if err := swag.ReadJSON(b, &res); err != nil {
+		return err
+	}
+	*m = res
+	return nil
+}
+
+// IntotoV001SchemaContentHash Specifies the hash algorithm and value encompassing the entire signed archive
+//
+// swagger:model IntotoV001SchemaContentHash
+type IntotoV001SchemaContentHash struct {
+
+	// The hashing function used to compute the hash value
+	// Required: true
+	// Enum: [sha256]
+	Algorithm *string `json:"algorithm"`
+
+	// The hash value for the archive
+	// Required: true
+	Value *string `json:"value"`
+}
+
+// Validate validates this intoto v001 schema content hash
+func (m *IntotoV001SchemaContentHash) Validate(formats strfmt.Registry) error {
+	var res []error
+
+	if err := m.validateAlgorithm(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if err := m.validateValue(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+var intotoV001SchemaContentHashTypeAlgorithmPropEnum []interface{}
+
+func init() {
+	var res []string
+	if err := json.Unmarshal([]byte(`["sha256"]`), &res); err != nil {
+		panic(err)
+	}
+	for _, v := range res {
+		intotoV001SchemaContentHashTypeAlgorithmPropEnum = append(intotoV001SchemaContentHashTypeAlgorithmPropEnum, v)
+	}
+}
+
+const (
+
+	// IntotoV001SchemaContentHashAlgorithmSha256 captures enum value "sha256"
+	IntotoV001SchemaContentHashAlgorithmSha256 string = "sha256"
+)
+
+// prop value enum
+func (m *IntotoV001SchemaContentHash) validateAlgorithmEnum(path, location string, value string) error {
+	if err := validate.EnumCase(path, location, value, intotoV001SchemaContentHashTypeAlgorithmPropEnum, true); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (m *IntotoV001SchemaContentHash) validateAlgorithm(formats strfmt.Registry) error {
+
+	if err := validate.Required("content"+"."+"hash"+"."+"algorithm", "body", m.Algorithm); err != nil {
+		return err
+	}
+
+	// value enum
+	if err := m.validateAlgorithmEnum("content"+"."+"hash"+"."+"algorithm", "body", *m.Algorithm); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (m *IntotoV001SchemaContentHash) validateValue(formats strfmt.Registry) error {
+
+	if err := validate.Required("content"+"."+"hash"+"."+"value", "body", m.Value); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// ContextValidate validates this intoto v001 schema content hash based on context it is used
+func (m *IntotoV001SchemaContentHash) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
+	return nil
+}
+
+// MarshalBinary interface implementation
+func (m *IntotoV001SchemaContentHash) MarshalBinary() ([]byte, error) {
+	if m == nil {
+		return nil, nil
+	}
+	return swag.WriteJSON(m)
+}
+
+// UnmarshalBinary interface implementation
+func (m *IntotoV001SchemaContentHash) UnmarshalBinary(b []byte) error {
+	var res IntotoV001SchemaContentHash
+	if err := swag.ReadJSON(b, &res); err != nil {
+		return err
+	}
+	*m = res
+	return nil
+}
diff --git a/pkg/generated/models/proposed_entry.go b/pkg/generated/models/proposed_entry.go
index 4643b52..0929ab3 100644
--- a/pkg/generated/models/proposed_entry.go
+++ b/pkg/generated/models/proposed_entry.go
@@ -115,6 +115,12 @@ func unmarshalProposedEntry(data []byte, consumer runtime.Consumer) (ProposedEnt
 			return nil, err
 		}
 		return &result, nil
+	case "intoto":
+		var result Intoto
+		if err := consumer.Consume(buf2, &result); err != nil {
+			return nil, err
+		}
+		return &result, nil
 	case "jar":
 		var result Jar
 		if err := consumer.Consume(buf2, &result); err != nil {
diff --git a/pkg/generated/models/signature.go b/pkg/generated/models/signature.go
new file mode 100644
index 0000000..6ce4dd8
--- /dev/null
+++ b/pkg/generated/models/signature.go
@@ -0,0 +1,91 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// 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 models
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"context"
+
+	"github.com/go-openapi/errors"
+	"github.com/go-openapi/strfmt"
+	"github.com/go-openapi/swag"
+	"github.com/go-openapi/validate"
+)
+
+// Signature signature
+//
+// swagger:model signature
+type Signature struct {
+
+	// key hint
+	KeyID string `json:"keyId,omitempty"`
+
+	// base64 encoded signature
+	// Required: true
+	// Format: byte
+	Sig *strfmt.Base64 `json:"sig"`
+}
+
+// Validate validates this signature
+func (m *Signature) Validate(formats strfmt.Registry) error {
+	var res []error
+
+	if err := m.validateSig(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+func (m *Signature) validateSig(formats strfmt.Registry) error {
+
+	if err := validate.Required("sig", "body", m.Sig); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// ContextValidate validates this signature based on context it is used
+func (m *Signature) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
+	return nil
+}
+
+// MarshalBinary interface implementation
+func (m *Signature) MarshalBinary() ([]byte, error) {
+	if m == nil {
+		return nil, nil
+	}
+	return swag.WriteJSON(m)
+}
+
+// UnmarshalBinary interface implementation
+func (m *Signature) UnmarshalBinary(b []byte) error {
+	var res Signature
+	if err := swag.ReadJSON(b, &res); err != nil {
+		return err
+	}
+	*m = res
+	return nil
+}
diff --git a/pkg/generated/restapi/embedded_spec.go b/pkg/generated/restapi/embedded_spec.go
index d5ce2d9..ed0968a 100644
--- a/pkg/generated/restapi/embedded_spec.go
+++ b/pkg/generated/restapi/embedded_spec.go
@@ -631,6 +631,32 @@ func init() {
         }
       }
     },
+    "intoto": {
+      "description": "Intoto object",
+      "type": "object",
+      "allOf": [
+        {
+          "$ref": "#/definitions/ProposedEntry"
+        },
+        {
+          "required": [
+            "apiVersion",
+            "spec"
+          ],
+          "properties": {
+            "apiVersion": {
+              "type": "string",
+              "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"
+            },
+            "spec": {
+              "type": "object",
+              "$ref": "pkg/types/intoto/intoto_schema.json"
+            }
+          },
+          "additionalProperties": false
+        }
+      ]
+    },
     "jar": {
       "description": "Java Archive (JAR)",
       "type": "object",
@@ -1232,6 +1258,60 @@ func init() {
         }
       }
     },
+    "IntotoV001SchemaContent": {
+      "type": "object",
+      "required": [
+        "envelope"
+      ],
+      "properties": {
+        "envelope": {
+          "description": "envelope",
+          "type": "string"
+        },
+        "hash": {
+          "description": "Specifies the hash algorithm and value encompassing the entire signed archive",
+          "type": "object",
+          "required": [
+            "algorithm",
+            "value"
+          ],
+          "properties": {
+            "algorithm": {
+              "description": "The hashing function used to compute the hash value",
+              "type": "string",
+              "enum": [
+                "sha256"
+              ]
+            },
+            "value": {
+              "description": "The hash value for the archive",
+              "type": "string"
+            }
+          }
+        }
+      }
+    },
+    "IntotoV001SchemaContentHash": {
+      "description": "Specifies the hash algorithm and value encompassing the entire signed archive",
+      "type": "object",
+      "required": [
+        "algorithm",
+        "value"
+      ],
+      "properties": {
+        "algorithm": {
+          "description": "The hashing function used to compute the hash value",
+          "type": "string",
+          "enum": [
+            "sha256"
+          ]
+        },
+        "value": {
+          "description": "The hash value for the archive",
+          "type": "string"
+        }
+      }
+    },
     "JarV001SchemaArchive": {
       "description": "Information about the archive associated with the entry",
       "type": "object",
@@ -1850,6 +1930,99 @@ func init() {
         }
       }
     },
+    "intoto": {
+      "description": "Intoto object",
+      "type": "object",
+      "allOf": [
+        {
+          "$ref": "#/definitions/ProposedEntry"
+        },
+        {
+          "required": [
+            "apiVersion",
+            "spec"
+          ],
+          "properties": {
+            "apiVersion": {
+              "type": "string",
+              "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"
+            },
+            "spec": {
+              "$ref": "#/definitions/intotoSchema"
+            }
+          },
+          "additionalProperties": false
+        }
+      ]
+    },
+    "intotoSchema": {
+      "description": "Intoto for Rekord objects",
+      "type": "object",
+      "title": "Intoto Schema",
+      "oneOf": [
+        {
+          "$ref": "#/definitions/intotoV001Schema"
+        }
+      ],
+      "$schema": "http://json-schema.org/draft-07/schema",
+      "$id": "http://rekor.sigstore.dev/types/intoto/intoto_schema.json"
+    },
+    "intotoV001Schema": {
+      "description": "Schema for intoto object",
+      "type": "object",
+      "title": "intoto v0.0.1 Schema",
+      "required": [
+        "publicKey",
+        "content"
+      ],
+      "properties": {
+        "content": {
+          "type": "object",
+          "required": [
+            "envelope"
+          ],
+          "properties": {
+            "envelope": {
+              "description": "envelope",
+              "type": "string"
+            },
+            "hash": {
+              "description": "Specifies the hash algorithm and value encompassing the entire signed archive",
+              "type": "object",
+              "required": [
+                "algorithm",
+                "value"
+              ],
+              "properties": {
+                "algorithm": {
+                  "description": "The hashing function used to compute the hash value",
+                  "type": "string",
+                  "enum": [
+                    "sha256"
+                  ]
+                },
+                "value": {
+                  "description": "The hash value for the archive",
+                  "type": "string"
+                }
+              }
+            }
+          }
+        },
+        "extraData": {
+          "description": "Arbitrary content to be included in the verifiable entry in the transparency log",
+          "type": "object",
+          "additionalProperties": true
+        },
+        "publicKey": {
+          "description": "The public key that can verify the signature",
+          "type": "string",
+          "format": "byte"
+        }
+      },
+      "$schema": "http://json-schema.org/draft-07/schema",
+      "$id": "http://rekor.sigstore.dev/types/intoto/intoto_v0_0_1_schema.json"
+    },
     "jar": {
       "description": "Java Archive (JAR)",
       "type": "object",
diff --git a/pkg/types/intoto/intoto.go b/pkg/types/intoto/intoto.go
new file mode 100644
index 0000000..1b2138e
--- /dev/null
+++ b/pkg/types/intoto/intoto.go
@@ -0,0 +1,57 @@
+//
+// 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 intoto
+
+import (
+	"errors"
+
+	"github.com/sigstore/rekor/pkg/generated/models"
+	"github.com/sigstore/rekor/pkg/types"
+)
+
+const (
+	KIND = "intoto"
+)
+
+type BaseIntotoType struct {
+	types.RekorType
+}
+
+func init() {
+	types.TypeMap.Store(KIND, New)
+}
+
+func New() types.TypeImpl {
+	bit := BaseIntotoType{}
+	bit.Kind = KIND
+	bit.VersionMap = VersionMap
+	return &bit
+}
+
+var VersionMap = types.NewSemVerEntryFactoryMap()
+
+func (it BaseIntotoType) UnmarshalEntry(pe models.ProposedEntry) (types.EntryImpl, error) {
+	if pe == nil {
+		return nil, errors.New("proposed entry cannot be nil")
+	}
+
+	in, ok := pe.(*models.Intoto)
+	if !ok {
+		return nil, errors.New("cannot unmarshal non-Rekord types")
+	}
+
+	return it.VersionedUnmarshal(in, *in.APIVersion)
+}
diff --git a/pkg/types/intoto/intoto_schema.json b/pkg/types/intoto/intoto_schema.json
new file mode 100644
index 0000000..b99b3c2
--- /dev/null
+++ b/pkg/types/intoto/intoto_schema.json
@@ -0,0 +1,12 @@
+{
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "$id": "http://rekor.sigstore.dev/types/intoto/intoto_schema.json",
+    "title": "Intoto Schema",
+    "description": "Intoto for Rekord objects",
+    "type": "object",
+    "oneOf": [
+        {
+            "$ref": "v0.0.1/intoto_v0_0_1_schema.json"
+        }
+    ]
+}
diff --git a/pkg/types/intoto/v0.0.1/entry.go b/pkg/types/intoto/v0.0.1/entry.go
new file mode 100644
index 0000000..2d52362
--- /dev/null
+++ b/pkg/types/intoto/v0.0.1/entry.go
@@ -0,0 +1,202 @@
+//
+// 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 intoto
+
+import (
+	"bytes"
+	"context"
+	"crypto"
+	"crypto/rand"
+	"crypto/sha256"
+	"encoding/hex"
+	"encoding/json"
+	"errors"
+
+	"github.com/in-toto/in-toto-golang/pkg/ssl"
+
+	"github.com/go-openapi/strfmt"
+	"github.com/go-openapi/swag"
+
+	"github.com/sigstore/rekor/pkg/generated/models"
+	"github.com/sigstore/rekor/pkg/log"
+	"github.com/sigstore/rekor/pkg/pki"
+	"github.com/sigstore/rekor/pkg/types"
+	"github.com/sigstore/rekor/pkg/types/intoto"
+)
+
+const (
+	APIVERSION = "0.0.1"
+)
+
+func init() {
+	if err := intoto.VersionMap.SetEntryFactory(APIVERSION, NewEntry); err != nil {
+		log.Logger.Panic(err)
+	}
+}
+
+type V001Entry struct {
+	IntotoObj models.IntotoV001Schema
+	keyObj    pki.PublicKey
+	env       ssl.Envelope
+}
+
+func (v V001Entry) APIVersion() string {
+	return APIVERSION
+}
+
+func NewEntry() types.EntryImpl {
+	return &V001Entry{}
+}
+
+func (v V001Entry) IndexKeys() []string {
+	var result []string
+	return result
+}
+
+func (v *V001Entry) Unmarshal(pe models.ProposedEntry) error {
+	it, ok := pe.(*models.Intoto)
+	if !ok {
+		return errors.New("cannot unmarshal non Intoto v0.0.1 type")
+	}
+
+	var err error
+	if err := types.DecodeEntry(it.Spec, &v.IntotoObj); err != nil {
+		return err
+	}
+
+	// field validation
+	if err := v.IntotoObj.Validate(strfmt.Default); err != nil {
+		return err
+	}
+
+	// Only support x509 signatures for intoto attestations
+	af := pki.NewArtifactFactory("x509")
+	v.keyObj, err = af.NewPublicKey(bytes.NewReader(*v.IntotoObj.PublicKey))
+	if err != nil {
+		return err
+	}
+
+	if err := json.Unmarshal([]byte(*v.IntotoObj.Content.Envelope), &v.env); err != nil {
+		return err
+	}
+
+	// cross field validation
+	return nil
+
+}
+
+func (v V001Entry) HasExternalEntities() bool {
+	return false
+}
+
+func (v *V001Entry) FetchExternalEntities(ctx context.Context) error {
+	return nil
+}
+
+func (v *V001Entry) Canonicalize(ctx context.Context) ([]byte, error) {
+	if v.keyObj == nil {
+		return nil, errors.New("cannot canonicalze empty key")
+	}
+	pk, err := v.keyObj.CanonicalValue()
+	if err != nil {
+		return nil, err
+	}
+	pkb := strfmt.Base64(pk)
+
+	h := sha256.Sum256([]byte(*v.IntotoObj.Content.Envelope))
+
+	canonicalEntry := models.IntotoV001Schema{
+		PublicKey: &pkb,
+		Content: &models.IntotoV001SchemaContent{
+			Hash: &models.IntotoV001SchemaContentHash{
+				Algorithm: swag.String(models.IntotoV001SchemaContentHashAlgorithmSha256),
+				Value:     swag.String(hex.EncodeToString(h[:])),
+			},
+		},
+	}
+
+	// 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
+
+	bytes, err := json.Marshal(&itObj)
+	if err != nil {
+		return nil, err
+	}
+
+	return bytes, nil
+}
+
+// Validate performs cross-field validation for fields in object
+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),
+			},
+		},
+	}
+
+	ok, err := sslVerifier.Verify(sslEnv)
+	if err != nil {
+		return err
+	}
+	if !ok {
+		return errors.New("invalid signature")
+	}
+
+	return nil
+}
+
+type verifier struct {
+	pub    pki.PublicKey
+	signer crypto.Signer
+}
+
+func (v *verifier) Sign(d []byte) ([]byte, string, error) {
+	if v.signer == nil {
+		return nil, "", errors.New("nil signer")
+	}
+	h := sha256.Sum256(d)
+	sig, err := v.signer.Sign(rand.Reader, h[:], crypto.SHA256)
+	if err != nil {
+		return nil, "", err
+	}
+	return sig, "", nil
+}
+
+func (v *verifier) Verify(keyID string, data, sig []byte) (bool, error) {
+	af := pki.NewArtifactFactory("x509")
+	s, err := af.NewSignature(bytes.NewReader(sig))
+	if err != nil {
+		return false, err
+	}
+	if err := s.Verify(bytes.NewReader(data), v.pub); err != nil {
+		return false, err
+	}
+	return true, nil
+}
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
new file mode 100644
index 0000000..33d2b90
--- /dev/null
+++ b/pkg/types/intoto/v0.0.1/intoto_v0_0_1_schema.json
@@ -0,0 +1,56 @@
+{
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "$id": "http://rekor.sigstore.dev/types/intoto/intoto_v0_0_1_schema.json",
+    "title": "intoto v0.0.1 Schema",
+    "description": "Schema for intoto object",
+    "type": "object",
+    "properties": {
+        "content": {
+            "type": "object",
+            "properties": {
+                "envelope": {
+                    "description": "envelope",
+                    "type": "string"
+                },
+                "hash": {
+                    "description": "Specifies the hash algorithm and value encompassing the entire signed archive",
+                    "type": "object",
+                    "properties": {
+                        "algorithm": {
+                            "description": "The hashing function used to compute the hash value",
+                            "type": "string",
+                            "enum": [
+                                "sha256"
+                            ]
+                        },
+                        "value": {
+                            "description": "The hash value for the archive",
+                            "type": "string"
+                        }
+                    },
+                    "required": [
+                        "algorithm",
+                        "value"
+                    ]
+                }
+            },
+            "required": [
+                "envelope"
+            ]
+        },
+        "publicKey": {
+            "description": "The public key that can verify the signature",
+            "type": "string",
+            "format": "byte"
+        },
+        "extraData": {
+            "description": "Arbitrary content to be included in the verifiable entry in the transparency log",
+            "type": "object",
+            "additionalProperties": true
+        }
+    },
+    "required": [
+        "publicKey",
+        "content"
+    ]
+}
\ No newline at end of file


Test patch:
diff --git a/pkg/types/intoto/intoto_test.go b/pkg/types/intoto/intoto_test.go
new file mode 100644
index 0000000..4858f90
--- /dev/null
+++ b/pkg/types/intoto/intoto_test.go
@@ -0,0 +1,124 @@
+//
+// 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 intoto
+
+import (
+	"context"
+	"errors"
+	"testing"
+
+	"github.com/go-openapi/swag"
+
+	"github.com/sigstore/rekor/pkg/generated/models"
+	"github.com/sigstore/rekor/pkg/types"
+)
+
+type UnmarshalTester struct {
+	models.Intoto
+}
+
+func (u UnmarshalTester) NewEntry() types.EntryImpl {
+	return &UnmarshalTester{}
+}
+
+func (u UnmarshalTester) Validate() error {
+	return nil
+}
+
+func (u UnmarshalTester) APIVersion() string {
+	return "2.0.1"
+}
+
+func (u UnmarshalTester) IndexKeys() []string {
+	return []string{}
+}
+
+func (u UnmarshalTester) Canonicalize(ctx context.Context) ([]byte, error) {
+	return nil, nil
+}
+
+func (u UnmarshalTester) HasExternalEntities() bool {
+	return false
+}
+
+func (u *UnmarshalTester) FetchExternalEntities(ctx context.Context) error {
+	return nil
+}
+
+func (u UnmarshalTester) Unmarshal(pe models.ProposedEntry) error {
+	return nil
+}
+
+type UnmarshalFailsTester struct {
+	UnmarshalTester
+}
+
+func (u UnmarshalFailsTester) NewEntry() types.EntryImpl {
+	return &UnmarshalFailsTester{}
+}
+
+func (u UnmarshalFailsTester) Unmarshal(pe models.ProposedEntry) error {
+	return errors.New("error")
+}
+
+func TestIntotoType(t *testing.T) {
+	// empty to start
+	if VersionMap.Count() != 0 {
+		t.Error("semver range was not blank at start of test")
+	}
+
+	u := UnmarshalTester{}
+	// ensure semver range parser is working
+	invalidSemVerRange := "not a valid semver range"
+	err := VersionMap.SetEntryFactory(invalidSemVerRange, u.NewEntry)
+	if err == nil || VersionMap.Count() > 0 {
+		t.Error("invalid semver range was incorrectly added to SemVerToFacFnMap")
+	}
+
+	// valid semver range can be parsed
+	err = VersionMap.SetEntryFactory(">= 1.2.3", u.NewEntry)
+	if err != nil || VersionMap.Count() != 1 {
+		t.Error("valid semver range was not added to SemVerToFacFnMap")
+	}
+
+	u.Intoto.APIVersion = swag.String("2.0.1")
+	brt := New()
+
+	// version requested matches implementation in map
+	if _, err := brt.UnmarshalEntry(&u.Intoto); err != nil {
+		t.Errorf("unexpected error in Unmarshal: %v", err)
+	}
+
+	// version requested fails to match implementation in map
+	u.Intoto.APIVersion = swag.String("1.2.2")
+	if _, err := brt.UnmarshalEntry(&u.Intoto); err == nil {
+		t.Error("unexpected success in Unmarshal for non-matching version")
+	}
+
+	// error in Unmarshal call is raised appropriately
+	u.Intoto.APIVersion = swag.String("2.2.0")
+	u2 := UnmarshalFailsTester{}
+	_ = VersionMap.SetEntryFactory(">= 1.2.3", u2.NewEntry)
+	if _, err := brt.UnmarshalEntry(&u.Intoto); err == nil {
+		t.Error("unexpected success in Unmarshal when error is thrown")
+	}
+
+	// version requested fails to match implementation in map
+	u.Intoto.APIVersion = swag.String("not_a_version")
+	if _, err := brt.UnmarshalEntry(&u.Intoto); err == nil {
+		t.Error("unexpected success in Unmarshal for invalid version")
+	}
+}
diff --git a/pkg/types/intoto/v0.0.1/entry_test.go b/pkg/types/intoto/v0.0.1/entry_test.go
new file mode 100644
index 0000000..f04fe7a
--- /dev/null
+++ b/pkg/types/intoto/v0.0.1/entry_test.go
@@ -0,0 +1,174 @@
+//
+// 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 intoto
+
+import (
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/x509"
+	"encoding/json"
+	"encoding/pem"
+	"reflect"
+	"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"
+	"go.uber.org/goleak"
+)
+
+func TestMain(m *testing.M) {
+	goleak.VerifyTestMain(m)
+}
+
+func TestNewEntryReturnType(t *testing.T) {
+	entry := NewEntry()
+	if reflect.TypeOf(entry) != reflect.ValueOf(&V001Entry{}).Type() {
+		t.Errorf("invalid type returned from NewEntry: %T", entry)
+	}
+}
+
+func p(b []byte) *strfmt.Base64 {
+	b64 := strfmt.Base64(b)
+	return &b64
+}
+
+func envelope(t *testing.T, k *ecdsa.PrivateKey, payload, payloadType string) *string {
+
+	signer, err := in_toto.NewSSLSigner(&verifier{
+		signer: k,
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	sslEnv, err := signer.SignPayload([]byte(payload))
+	if err != nil {
+		t.Fatal(err)
+	}
+	b, err := json.Marshal(sslEnv)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	s := string(b)
+	return &s
+}
+
+func TestV001Entry_Unmarshal(t *testing.T) {
+	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		t.Fatal(err)
+	}
+	der, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
+	if err != nil {
+		t.Fatal(err)
+	}
+	pub := pem.EncodeToMemory(&pem.Block{
+		Bytes: der,
+		Type:  "PUBLIC KEY",
+	})
+
+	invalid, err := json.Marshal(ssl.Envelope{
+		Payload: "hello",
+		Signatures: []ssl.Signature{
+			{
+				Sig: string(strfmt.Base64("foobar")),
+			},
+		},
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	tests := []struct {
+		name    string
+		want    models.IntotoV001Schema
+		it      *models.IntotoV001Schema
+		wantErr bool
+	}{
+		{
+			name:    "empty",
+			it:      &models.IntotoV001Schema{},
+			wantErr: true,
+		},
+		{
+			name: "missing envelope",
+			it: &models.IntotoV001Schema{
+				PublicKey: p(pub),
+			},
+			wantErr: true,
+		},
+		{
+			name: "missing envelope",
+			it: &models.IntotoV001Schema{
+				PublicKey: p([]byte("hello")),
+			},
+			wantErr: true,
+		},
+		{
+			name: "valid",
+			it: &models.IntotoV001Schema{
+				PublicKey: p(pub),
+				Content: &models.IntotoV001SchemaContent{
+					Envelope: envelope(t, key, "hello", "text"),
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "invalid",
+			it: &models.IntotoV001Schema{
+				PublicKey: p(pub),
+				Content: &models.IntotoV001SchemaContent{
+					Envelope: swag.String(string(invalid)),
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid key",
+			it: &models.IntotoV001Schema{
+				PublicKey: p([]byte("notavalidkey")),
+				Content: &models.IntotoV001SchemaContent{
+					Envelope: envelope(t, key, "hello", "text"),
+				},
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			v := &V001Entry{}
+			it := &models.Intoto{
+				Spec: tt.it,
+			}
+			var uv = func() error {
+				if err := v.Unmarshal(it); err != nil {
+					return err
+				}
+				if err := v.Validate(); err != nil {
+					return err
+				}
+				return nil
+			}
+			if err := uv(); (err != nil) != tt.wantErr {
+				t.Errorf("V001Entry.Unmarshal() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}
diff --git a/tests/e2e-test.sh b/tests/e2e-test.sh
index eeed7ac..f930e4b 100755
--- a/tests/e2e-test.sh
+++ b/tests/e2e-test.sh
@@ -41,10 +41,10 @@ done
 
 echo
 echo "running tests"
-TMPDIR="$(mktemp -d -t rekor_test.XXXXXX)"
-touch $TMPDIR.rekor.yaml
-trap "rm -rf $TMPDIR" EXIT
-if ! TMPDIR=$TMPDIR go test -tags=e2e ./tests/; then 
+REKORTMPDIR="$(mktemp -d -t rekor_test.XXXXXX)"
+touch $REKORTMPDIR.rekor.yaml
+trap "rm -rf $REKORTMPDIR" EXIT
+if ! REKORTMPDIR=$REKORTMPDIR go test -tags=e2e ./tests/; then 
    docker-compose logs --no-color > /tmp/docker-compose.log
    exit 1
 fi
diff --git a/tests/e2e_test.go b/tests/e2e_test.go
index 9fc380b..f7a33b7 100644
--- a/tests/e2e_test.go
+++ b/tests/e2e_test.go
@@ -39,6 +39,8 @@ import (
 	"github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer"
 	"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/cmd/rekor-cli/app"
 	"github.com/sigstore/rekor/pkg/generated/client"
 	"github.com/sigstore/rekor/pkg/generated/client/entries"
@@ -284,6 +286,68 @@ func TestJAR(t *testing.T) {
 	outputContains(t, out, "Entry already exists")
 }
 
+func TestIntoto(t *testing.T) {
+	td := t.TempDir()
+	attestationPath := filepath.Join(td, "attestation.json")
+	pubKeyPath := filepath.Join(td, "pub.pem")
+
+	it := in_toto.Statement{
+		StatementHeader: in_toto.StatementHeader{
+			Type:          in_toto.StatementInTotoV01,
+			PredicateType: in_toto.PredicateProvenanceV01,
+			Subject: []in_toto.Subject{
+				{
+					Name: "foobar",
+					Digest: in_toto.DigestSet{
+						"foo": "bar",
+					},
+				},
+			},
+		},
+		Predicate: in_toto.ProvenancePredicate{
+			Builder: in_toto.ProvenanceBuilder{
+				ID: "foo",
+			},
+		},
+	}
+
+	b, err := json.Marshal(it)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	pb, _ := pem.Decode([]byte(ecdsaPriv))
+	priv, err := x509.ParsePKCS8PrivateKey(pb.Bytes)
+	if err != nil {
+		t.Fatal(err)
+	}
+	signer, err := ssl.NewEnvelopeSigner(&IntotoSigner{
+		priv: priv.(*ecdsa.PrivateKey),
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	env, err := signer.SignPayload("application/vnd.in-toto+json", b)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	eb, err := json.Marshal(env)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	write(t, string(eb), attestationPath)
+	write(t, ecdsaPub, pubKeyPath)
+
+	// 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")
+	out = runCli(t, "upload", "--artifact", attestationPath, "--type", "intoto", "--public-key", pubKeyPath)
+	outputContains(t, out, "Entry already exists")
+}
+
 func TestJARURL(t *testing.T) {
 	out := runCli(t, "upload", "--artifact", "https://get.jenkins.io/war-stable/2.277.3/jenkins.war", "--type", "jar", "--artifact-hash=3e22c7e8cd7c8ee1e92cbaa8d0d303a7b53e07bc2a152ddc66f8ce55caea91ab")
 	outputContains(t, out, "Created entry at")
diff --git a/tests/util.go b/tests/util.go
index 6ea6d12..3f2659f 100644
--- a/tests/util.go
+++ b/tests/util.go
@@ -49,9 +49,9 @@ func run(t *testing.T, stdin, cmd string, arg ...string) string {
 	if stdin != "" {
 		c.Stdin = strings.NewReader(stdin)
 	}
-	if os.Getenv("TMPDIR") != "" {
+	if os.Getenv("REKORTMPDIR") != "" {
 		// ensure that we use a clean state.json file for each run
-		c.Env = append(c.Env, "HOME="+os.Getenv("TMPDIR"))
+		c.Env = append(c.Env, "HOME="+os.Getenv("REKORTMPDIR"))
 	}
 	b, err := c.CombinedOutput()
 	if err != nil {
@@ -65,8 +65,8 @@ func runCli(t *testing.T, arg ...string) string {
 	t.Helper()
 	arg = append(arg, "--rekor_server=http://localhost:3000")
 	// use a blank config file to ensure no collision
-	if os.Getenv("TMPDIR") != "" {
-		arg = append(arg, "--config="+os.Getenv("TMPDIR")+".rekor.yaml")
+	if os.Getenv("REKORTMPDIR") != "" {
+		arg = append(arg, "--config="+os.Getenv("REKORTMPDIR")+".rekor.yaml")
 	}
 	return run(t, "", cli, arg...)
 }
@@ -75,8 +75,8 @@ func runCliErr(t *testing.T, arg ...string) string {
 	t.Helper()
 	arg = append(arg, "--rekor_server=http://localhost:3000")
 	// use a blank config file to ensure no collision
-	if os.Getenv("TMPDIR") != "" {
-		arg = append(arg, "--config="+os.Getenv("TMPDIR")+".rekor.yaml")
+	if os.Getenv("REKORTMPDIR") != "" {
+		arg = append(arg, "--config="+os.Getenv("REKORTMPDIR")+".rekor.yaml")
 	}
 	cmd := exec.Command(cli, arg...)
 	b, err := cmd.CombinedOutput()
@@ -114,9 +114,7 @@ func createArtifact(t *testing.T, artifactPath string) string {
 
 	artifact := base64.StdEncoding.EncodeToString(data[:])
 	// Write this to a file
-	if err := ioutil.WriteFile(artifactPath, []byte(artifact), 0644); err != nil {
-		t.Fatal(err)
-	}
+	write(t, artifact, artifactPath)
 	return artifact
 }
 
@@ -132,3 +130,10 @@ func extractLogEntry(t *testing.T, le models.LogEntry) models.LogEntryAnon {
 	// this should never happen
 	return models.LogEntryAnon{}
 }
+
+func write(t *testing.T, data string, path string) {
+	t.Helper()
+	if err := ioutil.WriteFile(path, []byte(data), 0644); err != nil {
+		t.Fatal(err)
+	}
+}
diff --git a/tests/x509.go b/tests/x509.go
index 9c37625..fde442f 100644
--- a/tests/x509.go
+++ b/tests/x509.go
@@ -19,6 +19,7 @@ package e2e
 
 import (
 	"crypto"
+	"crypto/ecdsa"
 	"crypto/rand"
 	"crypto/rsa"
 	"crypto/sha256"
@@ -26,6 +27,8 @@ import (
 	"encoding/pem"
 	"io/ioutil"
 	"testing"
+
+	"github.com/in-toto/in-toto-golang/pkg/ssl"
 )
 
 // Generated with:
@@ -150,3 +153,25 @@ func createdX509SignedArtifact(t *testing.T, artifactPath, sigPath string) {
 		t.Fatal(err)
 	}
 }
+
+type IntotoSigner struct {
+	priv *ecdsa.PrivateKey
+}
+
+var _ ssl.SignVerifier = &IntotoSigner{}
+
+func (it *IntotoSigner) Sign(data []byte) ([]byte, string, error) {
+	h := sha256.Sum256(data)
+	sig, err := it.priv.Sign(rand.Reader, h[:], crypto.SHA256)
+	if err != nil {
+		return nil, "", err
+	}
+	return sig, "", nil
+}
+
+func (it *IntotoSigner) Verify(_ string, data, sig []byte) (bool, error) {
+	h := sha256.Sum256(data)
+
+	ok := ecdsa.VerifyASN1(&it.priv.PublicKey, h[:], sig)
+	return ok, nil
+}