Repository: sigstore/rekor
Pull request #1486 (2023-05-12 23:19:24)

omit informational message if machine-parseable output has been requested
Fixes: #1395 

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/sigstore/rekor/pull/1486?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore) Report
> Merging [#1486](https://app.codecov.io/gh/sigstore/rekor/pull/1486?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore) (9487208) into [main](https://app.codecov.io/gh/sigstore/rekor/commit/3282e1ab3e9b8f773e8a44861a31a3df9adf01ce?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore) (3282e1a) will **decrease** coverage by `19.66%`.
> The diff coverage is `0.00%`.

```diff
@@             Coverage Diff             @@
##             main    #1486       +/-   ##
===========================================
- Coverage   64.33%   44.67%   -19.66%     
===========================================
  Files          80       71        -9     
  Lines        7789     7066      -723     
===========================================
- Hits         5011     3157     -1854     
- Misses       2129     3570     +1441     
+ Partials      649      339      -310     
```

| Flag | Coverage Δ | |
|---|---|---|
| e2etests | `?` | |
| unittests | `44.67% <0.00%> (+0.02%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://app.codecov.io/gh/sigstore/rekor/pull/1486?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore) | Coverage Δ | |
|---|---|---|
| [cmd/rekor-cli/app/search.go](https://app.codecov.io/gh/sigstore/rekor/pull/1486?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore#diff-Y21kL3Jla29yLWNsaS9hcHAvc2VhcmNoLmdv) | `19.85% <0.00%> (-38.00%)` | :arrow_down: |

... and [54 files with indirect coverage changes](https://app.codecov.io/gh/sigstore/rekor/pull/1486/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore)



Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index ae237fd..1d1d5e8 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -35,12 +35,12 @@ jobs:
 
     steps:
       - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
-      - uses: sigstore/cosign-installer@204a51a57a74d190b284a0ce69b44bc37201f343 # v3.0.3
+      - uses: sigstore/cosign-installer@dd6b2e2b610a11fd73dd187a43d57cc1394e35f9 # v3.0.5
 
       - name: Extract version of Go to use
         run: echo "GOVERSION=$(cat Dockerfile|grep golang | awk ' { print $2 } ' | cut -d '@' -f 1 | cut -d ':' -f 2 | uniq)" >> $GITHUB_ENV
 
-      - uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0
+      - uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1
         with:
           go-version: ${{ env.GOVERSION }}
 
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 173e0d9..b8d485a 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -36,7 +36,7 @@ jobs:
       - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
       - name: Extract version of Go to use
         run: echo "GOVERSION=$(cat Dockerfile|grep golang | awk ' { print $2 } ' | cut -d '@' -f 1 | cut -d ':' -f 2 | uniq)" >> $GITHUB_ENV
-      - uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0
+      - uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1
         with:
           go-version: ${{ env.GOVERSION }}
 
@@ -47,7 +47,7 @@ jobs:
       - name: Test
         run: go test -v -coverprofile=coverage.txt -covermode=atomic ./...
       - name: Upload Coverage Report
-        uses: codecov/codecov-action@894ff025c7b54547a9a2a1e9f228beae737ad3c2 # v3.1.3
+        uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4
         with:
           flags: unittests
       - name: Ensure no files were modified as a result of the build
@@ -59,7 +59,7 @@ jobs:
       - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
       - name: Extract version of Go to use
         run: echo "GOVERSION=$(cat Dockerfile|grep golang | awk ' { print $2 } ' | cut -d '@' -f 1 | cut -d ':' -f 2 | uniq)" >> $GITHUB_ENV
-      - uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0
+      - uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1
         with:
           go-version: ${{ env.GOVERSION }}
 
@@ -82,7 +82,7 @@ jobs:
       - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
       - name: Extract version of Go to use
         run: echo "GOVERSION=$(cat Dockerfile|grep golang | awk ' { print $2 } ' | cut -d '@' -f 1 | cut -d ':' -f 2 | uniq)" >> $GITHUB_ENV
-      - uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0
+      - uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1
         with:
           go-version: ${{ env.GOVERSION }}
       - name: install gocovmerge
@@ -99,7 +99,7 @@ jobs:
           name: E2E Docker Compose logs
           path: /tmp/docker-compose.log
       - name: Upload Coverage Report
-        uses: codecov/codecov-action@894ff025c7b54547a9a2a1e9f228beae737ad3c2 # v3.1.3
+        uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4
         with:
           files: /tmp/rekor-merged.cov,/tmp/pkg-rekor-merged.cov
           flags: e2etests
@@ -116,7 +116,7 @@ jobs:
         run: docker-compose build
       - name: Extract version of Go to use
         run: echo "GOVERSION=$(cat Dockerfile|grep golang | awk ' { print $2 } ' | cut -d '@' -f 1 | cut -d ':' -f 2 | uniq)" >> $GITHUB_ENV
-      - uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0
+      - uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1
         with:
           go-version: ${{ env.GOVERSION }}
 
@@ -139,7 +139,7 @@ jobs:
         run: docker-compose build
       - name: Extract version of Go to use
         run: echo "GOVERSION=$(cat Dockerfile|grep golang | awk ' { print $2 } ' | cut -d '@' -f 1 | cut -d ':' -f 2 | uniq)" >> $GITHUB_ENV
-      - uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0
+      - uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1
         with:
           go-version: ${{ env.GOVERSION }}
 
@@ -161,7 +161,7 @@ jobs:
         run: git switch -c harness-test-branch
       - name: Extract version of Go to use
         run: echo "GOVERSION=$(cat Dockerfile|grep golang | awk ' { print $2 } ' | cut -d '@' -f 1 | cut -d ':' -f 2 | uniq)" >> $GITHUB_ENV
-      - uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0
+      - uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1
         with:
           go-version: ${{ env.GOVERSION }}
 
diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml
index 263e2eb..cf7bc4a 100644
--- a/.github/workflows/verify.yml
+++ b/.github/workflows/verify.yml
@@ -32,7 +32,7 @@ jobs:
       - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
       - name: Extract version of Go to use
         run: echo "GOVERSION=$(cat Dockerfile|grep golang | awk ' { print $2 } ' | cut -d '@' -f 1 | cut -d ':' -f 2 | uniq)" >> $GITHUB_ENV
-      - uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0
+      - uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1
         with:
           go-version: ${{ env.GOVERSION }}
 
@@ -51,7 +51,7 @@ jobs:
       - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
       - name: Extract version of Go to use
         run: echo "GOVERSION=$(cat Dockerfile|grep golang | awk ' { print $2 } ' | cut -d '@' -f 1 | cut -d ':' -f 2 | uniq)" >> $GITHUB_ENV
-      - uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0
+      - uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1
         with:
           go-version: ${{ env.GOVERSION }}
 
@@ -59,4 +59,4 @@ jobs:
         uses: golangci/golangci-lint-action@08e2f20817b15149a52b5b3ebe7de50aff2ba8c5 # v3.4.0
         timeout-minutes: 5
         with:
-          version: v1.51
+          version: v1.52
diff --git a/Dockerfile b/Dockerfile
index cec63c5..2514d03 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -13,7 +13,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-FROM golang:1.20.4@sha256:685a22e459f9516f27d975c5cc6accc11223ee81fdfbbae60e39cc3b87357306 AS builder
+FROM golang:1.20.4@sha256:c6447e16fb7bada7698fb395ab3c7df19896f1824fe72165441b3756d3576856 AS builder
 ENV APP_ROOT=/opt/app-root
 ENV GOPATH=$APP_ROOT
 
@@ -31,7 +31,7 @@ RUN CGO_ENABLED=0 go build -gcflags "all=-N -l" -ldflags "${SERVER_LDFLAGS}" -o
 RUN go test -c -ldflags "${SERVER_LDFLAGS}" -cover -covermode=count -coverpkg=./... -o rekor-server_test ./cmd/rekor-server
 
 # Multi-Stage production build
-FROM golang:1.20.4@sha256:685a22e459f9516f27d975c5cc6accc11223ee81fdfbbae60e39cc3b87357306 as deploy
+FROM golang:1.20.4@sha256:c6447e16fb7bada7698fb395ab3c7df19896f1824fe72165441b3756d3576856 as deploy
 
 # Retrieve the binary from the previous stage
 COPY --from=builder /opt/app-root/src/rekor-server /usr/local/bin/rekor-server
diff --git a/cmd/rekor-cli/app/search.go b/cmd/rekor-cli/app/search.go
index 4552a1c..2b1e89d 100644
--- a/cmd/rekor-cli/app/search.go
+++ b/cmd/rekor-cli/app/search.go
@@ -201,7 +201,9 @@ var searchCmd = &cobra.Command{
 			return nil, fmt.Errorf("no matching entries found")
 		}
 
-		fmt.Fprintln(os.Stderr, "Found matching entries (listed by UUID):")
+		if viper.GetString("format") != "json" {
+			fmt.Fprintln(os.Stderr, "Found matching entries (listed by UUID):")
+		}
 
 		return &searchCmdOutput{
 			UUIDs: resp.GetPayload(),
diff --git a/cmd/rekor-cli/app/state/state.go b/cmd/rekor-cli/app/state/state.go
index 21c0088..1985b81 100644
--- a/cmd/rekor-cli/app/state/state.go
+++ b/cmd/rekor-cli/app/state/state.go
@@ -47,10 +47,7 @@ func Dump(key string, sth *util.SignedCheckpoint) error {
 	if err != nil {
 		return err
 	}
-	if err := os.WriteFile(statePath, b, 0600); err != nil {
-		return err
-	}
-	return nil
+	return os.WriteFile(statePath, b, 0600)
 }
 
 func loadStateFile() persistedState {
diff --git a/cmd/rekor-cli/app/upload.go b/cmd/rekor-cli/app/upload.go
index aedc6da..040ab4e 100644
--- a/cmd/rekor-cli/app/upload.go
+++ b/cmd/rekor-cli/app/upload.go
@@ -61,10 +61,7 @@ var uploadCmd = &cobra.Command{
 		if err := viper.BindPFlags(cmd.Flags()); err != nil {
 			return err
 		}
-		if err := validateArtifactPFlags(false, false); err != nil {
-			return err
-		}
-		return nil
+		return validateArtifactPFlags(false, false)
 	},
 	Long: `This command takes the public key, signature and URL of the release artifact and uploads it to the rekor server.`,
 	Run: format.WrapCmd(func(args []string) (interface{}, error) {
diff --git a/cmd/rekor-cli/app/verify.go b/cmd/rekor-cli/app/verify.go
index 1d4d86f..cffd81b 100644
--- a/cmd/rekor-cli/app/verify.go
+++ b/cmd/rekor-cli/app/verify.go
@@ -85,10 +85,7 @@ var verifyCmd = &cobra.Command{
 		if err := viper.BindPFlags(cmd.Flags()); err != nil {
 			return fmt.Errorf("error initializing cmd line args: %s", err)
 		}
-		if err := validateArtifactPFlags(true, true); err != nil {
-			return err
-		}
-		return nil
+		return validateArtifactPFlags(true, true)
 	},
 	Run: format.WrapCmd(func(args []string) (interface{}, error) {
 		ctx := context.Background()
diff --git a/cmd/rekor-server/app/root.go b/cmd/rekor-server/app/root.go
index df31336..c91013f 100644
--- a/cmd/rekor-server/app/root.go
+++ b/cmd/rekor-server/app/root.go
@@ -80,6 +80,9 @@ func init() {
 	rootCmd.PersistentFlags().Uint("trillian_log_server.tlog_id", 0, "Trillian tree id")
 	rootCmd.PersistentFlags().String("trillian_log_server.sharding_config", "", "path to config file for inactive shards, in JSON or YAML")
 
+	rootCmd.PersistentFlags().Bool("enable_stable_checkpoint", true, "publish stable checkpoints to Redis. When disabled, gossiping may not be possible if the log checkpoint updates too frequently")
+	rootCmd.PersistentFlags().Uint("publish_frequency", 5, "how often to publish a new checkpoint, in minutes")
+
 	hostname, err := os.Hostname()
 	if err != nil {
 		hostname = "localhost"
diff --git a/cmd/rekor-server/app/watch.go b/cmd/rekor-server/app/watch.go
deleted file mode 100644
index 563a29e..0000000
--- a/cmd/rekor-server/app/watch.go
+++ /dev/null
@@ -1,178 +0,0 @@
-//
-// 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 app
-
-import (
-	"context"
-	"crypto"
-	"crypto/x509"
-	"encoding/json"
-	"encoding/pem"
-	"errors"
-	"flag"
-	"fmt"
-	"os"
-	"time"
-
-	_ "gocloud.dev/blob/fileblob" // fileblob
-	_ "gocloud.dev/blob/gcsblob"
-
-	"github.com/spf13/cobra"
-	"github.com/spf13/viper"
-	"gocloud.dev/blob"
-
-	"github.com/sigstore/rekor/pkg/client"
-	genclient "github.com/sigstore/rekor/pkg/generated/client"
-	"github.com/sigstore/rekor/pkg/log"
-	"github.com/sigstore/rekor/pkg/util"
-	"github.com/sigstore/sigstore/pkg/signature"
-)
-
-const rekorSthBucketEnv = "REKOR_STH_BUCKET"
-
-// watchCmd represents the serve command
-var watchCmd = &cobra.Command{
-	Use:   "watch",
-	Short: "Start a process to watch and record STH's from Rekor",
-	Long:  `Start a process to watch and record STH's from Rekor`,
-	PreRun: func(cmd *cobra.Command, args []string) {
-		// these are bound here so that they are not overwritten by other commands
-		if err := viper.BindPFlags(cmd.Flags()); err != nil {
-			log.Logger.Fatal("Error initializing cmd line args: ", err)
-		}
-	},
-	RunE: func(cmd *cobra.Command, args []string) error {
-
-		// Setup the logger to dev/prod
-		log.ConfigureLogger(viper.GetString("log_type"))
-
-		// workaround for https://github.com/sigstore/rekor/issues/68
-		// from https://github.com/golang/glog/commit/fca8c8854093a154ff1eb580aae10276ad6b1b5f
-		_ = flag.CommandLine.Parse([]string{})
-
-		host := viper.GetString("rekor_server.address")
-		port := viper.GetUint("port")
-		interval := viper.GetDuration("interval")
-		url := fmt.Sprintf("http://%s:%d", host, port)
-		c, err := client.GetRekorClient(url)
-		if err != nil {
-			return err
-		}
-
-		keyResp, err := c.Pubkey.GetPublicKey(nil)
-		if err != nil {
-			return err
-		}
-		publicKey := keyResp.Payload
-		block, _ := pem.Decode([]byte(publicKey))
-		if block == nil {
-			return errors.New("failed to decode public key of server")
-		}
-
-		pub, err := x509.ParsePKIXPublicKey(block.Bytes)
-		if err != nil {
-			return err
-		}
-
-		ctx := context.Background()
-		bucketURL := os.Getenv(rekorSthBucketEnv)
-		if bucketURL == "" {
-			log.CliLogger.Fatalf("%s env var must be set", rekorSthBucketEnv)
-		}
-		bucket, err := blob.OpenBucket(ctx, bucketURL)
-		if err != nil {
-			return err
-		}
-		defer bucket.Close()
-		tick := time.NewTicker(interval)
-		var last *SignedAndUnsignedLogRoot
-
-		for {
-			<-tick.C
-			log.Logger.Info("performing check")
-			lr, err := doCheck(c, pub)
-			if err != nil {
-				log.Logger.Warnf("error verifiying tree: %s", err)
-				continue
-			}
-			log.Logger.Infof("Found and verified state at %d", lr.VerifiedLogRoot.Size)
-			if last != nil && last.VerifiedLogRoot.Size == lr.VerifiedLogRoot.Size {
-				log.Logger.Infof("Last tree size is the same as the current one: %d %d",
-					last.VerifiedLogRoot.Size, lr.VerifiedLogRoot.Size)
-				// If it's the same, it shouldn't have changed but we'll still upload anyway
-				// in case that failed.
-			}
-
-			if err := uploadToBlobStorage(ctx, bucket, lr); err != nil {
-				log.Logger.Warnf("error uploading result: %s", err)
-				continue
-			}
-			last = lr
-		}
-	},
-}
-
-func init() {
-	watchCmd.Flags().Duration("interval", 1*time.Minute, "Polling interval")
-	rootCmd.AddCommand(watchCmd)
-}
-
-func doCheck(c *genclient.Rekor, pub crypto.PublicKey) (*SignedAndUnsignedLogRoot, error) {
-	li, err := c.Tlog.GetLogInfo(nil)
-	if err != nil {
-		return nil, fmt.Errorf("getting log info: %w", err)
-	}
-	sth := util.SignedCheckpoint{}
-	if err := sth.UnmarshalText([]byte(*li.Payload.SignedTreeHead)); err != nil {
-		return nil, fmt.Errorf("unmarshalling tree head: %w", err)
-	}
-
-	verifier, err := signature.LoadVerifier(pub, crypto.SHA256)
-	if err != nil {
-		return nil, err
-	}
-
-	if !sth.Verify(verifier) {
-		return nil, fmt.Errorf("signed tree head failed verification: %w", err)
-	}
-
-	return &SignedAndUnsignedLogRoot{
-		VerifiedLogRoot: &sth,
-	}, nil
-}
-
-func uploadToBlobStorage(ctx context.Context, bucket *blob.Bucket, lr *SignedAndUnsignedLogRoot) error {
-	b, err := json.Marshal(lr)
-	if err != nil {
-		return err
-	}
-
-	objName := fmt.Sprintf("sth-%d.json", lr.VerifiedLogRoot.Size)
-	w, err := bucket.NewWriter(ctx, objName, nil)
-	if err != nil {
-		return err
-	}
-	defer w.Close()
-	if _, err := w.Write(b); err != nil {
-		return err
-	}
-	return nil
-}
-
-// For JSON marshalling
-type SignedAndUnsignedLogRoot struct {
-	VerifiedLogRoot *util.SignedCheckpoint
-}
diff --git a/config/watcher.yaml b/config/watcher.yaml
deleted file mode 100644
index 659ad09..0000000
--- a/config/watcher.yaml
+++ /dev/null
@@ -1,62 +0,0 @@
-#
-# 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.
-
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  namespace: rekor-system
-  name: rekor-watcher
-  labels:
-    app: rekor-watcher
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: rekor-watcher
-  template:
-    metadata:
-      labels:
-        app: rekor-watcher
-      annotations:
-        prometheus.io/scrape: "true"
-        prometheus.io/path: /metrics
-        prometheus.io/port: "2112"
-    spec:
-      containers:
-      - name: rekor-watcher
-        image: ko://github.com/sigstore/rekor/cmd/rekor-server
-        command: ["/ko-app/rekor-server"]
-        ports:
-        - containerPort: 3000
-        - containerPort: 2112 # metrics
-        args: [
-          "watch",
-          "--rekor_server.address=rekor-server",
-          "--port=80",
-          "--log_type=prod",
-        ]
-        env:
-        - name: REKOR_STH_BUCKET
-          value: "gs://rekor-sth"
-        resources:
-          requests:
-            memory: "1G"
-            cpu: ".5"
-        securityContext:
-          readOnlyRootFilesystem: true
-          runAsNonRoot: true
-          capabilities:
-            drop:
-            - all
diff --git a/docker-compose.yml b/docker-compose.yml
index 69d6b0b..fb4c991 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -92,6 +92,7 @@ services:
       "--rekor_server.signer=memory",
       "--enable_attestation_storage",
       "--attestation_storage_bucket=file:///var/run/attestations",
+      "--enable_stable_checkpoint",
       # Uncomment this for production logging
       # "--log_type=prod",
       ]
diff --git a/go.mod b/go.mod
index 0981cb6..aadc344 100644
--- a/go.mod
+++ b/go.mod
@@ -15,7 +15,7 @@ require (
 	github.com/go-openapi/strfmt v0.21.7
 	github.com/go-openapi/swag v0.22.3
 	github.com/go-openapi/validate v0.22.1
-	github.com/go-playground/validator/v10 v10.13.0
+	github.com/go-playground/validator/v10 v10.14.0
 	github.com/google/go-cmp v0.5.9
 	github.com/google/rpmpack v0.0.0-20210518075352-dc539ef4f2ea
 	github.com/google/trillian v1.5.2
@@ -34,7 +34,7 @@ require (
 	github.com/spf13/viper v1.15.0
 	github.com/theupdateframework/go-tuf v0.5.2
 	github.com/transparency-dev/merkle v0.0.2
-	github.com/veraison/go-cose v1.0.0
+	github.com/veraison/go-cose v1.1.0
 	github.com/zalando/go-keyring v0.1.1 // indirect
 	go.uber.org/goleak v1.2.1
 	go.uber.org/zap v1.24.0
@@ -47,12 +47,13 @@ require (
 	google.golang.org/grpc v1.55.0
 	google.golang.org/protobuf v1.30.0
 	gopkg.in/ini.v1 v1.67.0
-	sigs.k8s.io/release-utils v0.7.3
+	sigs.k8s.io/release-utils v0.7.4
 )
 
 require (
 	github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230329111138-12e09aba5ebd
 	github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7
+	github.com/go-redis/redismock/v9 v9.0.3
 	github.com/golang/mock v1.6.0
 	github.com/hashicorp/go-cleanhttp v0.5.2
 	github.com/hashicorp/go-retryablehttp v0.7.2
@@ -64,7 +65,9 @@ require (
 	cloud.google.com/go/compute/metadata v0.2.3 // indirect
 	filippo.io/edwards25519 v1.0.0 // indirect
 	github.com/cyphar/filepath-securejoin v0.2.3 // indirect
+	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+	github.com/gabriel-vasile/mimetype v1.4.2 // indirect
 	github.com/go-logr/logr v1.2.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/google/s2a-go v0.1.3 // indirect
@@ -106,7 +109,7 @@ require (
 	github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
-	github.com/leodido/go-urn v1.2.3 // indirect
+	github.com/leodido/go-urn v1.2.4 // indirect
 	github.com/letsencrypt/boulder v0.0.0-20221109233200-85aa52084eaf // indirect
 	github.com/magiconair/properties v1.8.7 // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
diff --git a/go.sum b/go.sum
index b5d0f6a..77fdd29 100644
--- a/go.sum
+++ b/go.sum
@@ -923,6 +923,8 @@ github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbS
 github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
 github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88=
 github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
+github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
+github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
 github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
 github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
 github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
@@ -1022,8 +1024,10 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
 github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
 github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
-github.com/go-playground/validator/v10 v10.13.0 h1:cFRQdfaSMCOSfGCCLB20MHvuoHb/s5G8L5pu2ppK5AQ=
-github.com/go-playground/validator/v10 v10.13.0/go.mod h1:dwu7+CG8/CtBiJFZDz4e+5Upb6OLw04gtBYw0mcG/z4=
+github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
+github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
+github.com/go-redis/redismock/v9 v9.0.3 h1:mtHQi2l51lCmXIbTRTqb1EiHYe9tL5Yk5oorlSJJqR0=
+github.com/go-redis/redismock/v9 v9.0.3/go.mod h1:F6tJRfnU8R/NZ0E+Gjvoluk14MqMC5ueSZX6vVQypc0=
 github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8=
 github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
 github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
@@ -1471,8 +1475,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
 github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
-github.com/leodido/go-urn v1.2.3 h1:6BE2vPT0lqoz3fmOesHZiaiFh7889ssCo2GMvLCfiuA=
-github.com/leodido/go-urn v1.2.3/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
+github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
+github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
 github.com/letsencrypt/boulder v0.0.0-20221109233200-85aa52084eaf h1:ndns1qx/5dL43g16EQkPV/i8+b3l5bYQwLeoSBe7tS8=
 github.com/letsencrypt/boulder v0.0.0-20221109233200-85aa52084eaf/go.mod h1:aGkAgvWY/IUcVFfuly53REpfv5edu25oij+qHRFaraA=
 github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@@ -1596,6 +1600,7 @@ github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OS
 github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
 github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
 github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
 github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
@@ -1615,6 +1620,7 @@ github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1ls
 github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
 github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
 github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
+github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
 github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
 github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU=
 github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
@@ -1637,6 +1643,7 @@ github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeR
 github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc=
 github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM=
 github.com/onsi/gomega v1.23.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg=
+github.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y=
 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
 github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
 github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
@@ -1921,8 +1928,8 @@ github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/
 github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
 github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
-github.com/veraison/go-cose v1.0.0 h1:Jxirc0rl3gG7wUFgW+82tBQNeK8T8e2Bk1Vd298ob4A=
-github.com/veraison/go-cose v1.0.0/go.mod h1:7ziE85vSq4ScFTg6wyoMXjucIGOf4JkFEZi/an96Ct4=
+github.com/veraison/go-cose v1.1.0 h1:AalPS4VGiKavpAzIlBjrn7bhqXiXi4jbMYY/2+UC+4o=
+github.com/veraison/go-cose v1.1.0/go.mod h1:7ziE85vSq4ScFTg6wyoMXjucIGOf4JkFEZi/an96Ct4=
 github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
 github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
 github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
@@ -2925,6 +2932,7 @@ gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI
 gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
 gopkg.in/telebot.v3 v3.0.0/go.mod h1:7rExV8/0mDDNu9epSrDm/8j22KLaActH1Tbee6YjzWg=
 gopkg.in/telebot.v3 v3.1.2/go.mod h1:GJKwwWqp9nSkIVN51eRKU78aB5f5OnQuWdwiIZfPbko=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
 gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
@@ -3026,8 +3034,8 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22/go.mod h1:LEScyz
 sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs=
 sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
 sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
-sigs.k8s.io/release-utils v0.7.3 h1:6pS8x6c5RmdUgR9qcg1LO6hjUzuE4Yo9TGZ3DemrZdM=
-sigs.k8s.io/release-utils v0.7.3/go.mod h1:n0mVez/1PZYZaZUTJmxewxH3RJ/Lf7JUDh7TG1CASOE=
+sigs.k8s.io/release-utils v0.7.4 h1:17LmJrydpUloTCtaoWj95uKlcrUp4h2A9Sa+ZL+lV9w=
+sigs.k8s.io/release-utils v0.7.4/go.mod h1:JEt2QPHItd5Pg2UKLAU8PEaSlF4bUjCZimpxFDgymVU=
 sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
 sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
 sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
diff --git a/openapi.yaml b/openapi.yaml
index d90503f..5b5e960 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -67,6 +67,12 @@ paths:
       operationId: getLogInfo
       tags:
         - tlog
+      parameters:
+        - in: query
+          name: stable
+          type: boolean
+          default: false
+          description: Whether to return a stable checkpoint for the active shard
       responses:
         200:
           description: A JSON object with the root hash and tree size as properties
diff --git a/pkg/api/api.go b/pkg/api/api.go
index bfec95c..12925b6 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -35,6 +35,7 @@ import (
 	"github.com/sigstore/rekor/pkg/signer"
 	"github.com/sigstore/rekor/pkg/storage"
 	"github.com/sigstore/rekor/pkg/trillianclient"
+	"github.com/sigstore/rekor/pkg/witness"
 	"github.com/sigstore/sigstore/pkg/cryptoutils"
 	"github.com/sigstore/sigstore/pkg/signature"
 	"github.com/sigstore/sigstore/pkg/signature/options"
@@ -60,6 +61,8 @@ type API struct {
 	pubkey     string // PEM encoded public key
 	pubkeyHash string // SHA256 hash of DER-encoded public key
 	signer     signature.Signer
+	// stops checkpoint publishing
+	checkpointPublishCancel context.CancelFunc
 }
 
 func NewAPI(treeID uint) (*API, error) {
@@ -134,7 +137,8 @@ func ConfigureAPI(treeID uint) {
 	if err != nil {
 		log.Logger.Panic(err)
 	}
-	if viper.GetBool("enable_retrieve_api") || slices.Contains(viper.GetStringSlice("enabled_api_endpoints"), "searchIndex") {
+	if viper.GetBool("enable_retrieve_api") || viper.GetBool("enable_stable_checkpoint") ||
+		slices.Contains(viper.GetStringSlice("enabled_api_endpoints"), "searchIndex") {
 		redisClient = redis.NewClient(&redis.Options{
 			Addr:    fmt.Sprintf("%v:%v", viper.GetString("redis_server.address"), viper.GetUint64("redis_server.port")),
 			Network: "tcp",
@@ -148,4 +152,18 @@ func ConfigureAPI(treeID uint) {
 			log.Logger.Panic(err)
 		}
 	}
+
+	if viper.GetBool("enable_stable_checkpoint") {
+		checkpointPublisher := witness.NewCheckpointPublisher(context.Background(), api.logClient, api.logRanges.ActiveTreeID(),
+			viper.GetString("rekor_server.hostname"), api.signer, redisClient, viper.GetUint("publish_frequency"), CheckpointPublishCount)
+
+		// create context to cancel goroutine on server shutdown
+		ctx, cancel := context.WithCancel(context.Background())
+		api.checkpointPublishCancel = cancel
+		checkpointPublisher.StartPublisher(ctx)
+	}
+}
+
+func StopAPI() {
+	api.checkpointPublishCancel()
 }
diff --git a/pkg/api/entries.go b/pkg/api/entries.go
index 27f9b8c..1f5bc26 100644
--- a/pkg/api/entries.go
+++ b/pkg/api/entries.go
@@ -68,7 +68,7 @@ func signEntry(ctx context.Context, signer signature.Signer, entry models.LogEnt
 }
 
 // logEntryFromLeaf creates a signed LogEntry struct from trillian structs
-func logEntryFromLeaf(ctx context.Context, signer signature.Signer, tc trillianclient.TrillianClient, leaf *trillian.LogLeaf,
+func logEntryFromLeaf(ctx context.Context, signer signature.Signer, _ trillianclient.TrillianClient, leaf *trillian.LogLeaf,
 	signedLogRoot *trillian.SignedLogRoot, proof *trillian.Proof, tid int64, ranges sharding.LogRanges) (models.LogEntry, error) {
 
 	log.ContextLogger(ctx).Debugf("log entry from leaf %d", leaf.GetLeafIndex())
@@ -554,7 +554,7 @@ func retrieveUUIDFromTree(ctx context.Context, uuid string, tid int64) (models.L
 
 // handlers for APIs that may be disabled in a given instance
 
-func CreateLogEntryNotImplementedHandler(params entries.CreateLogEntryParams) middleware.Responder {
+func CreateLogEntryNotImplementedHandler(_ entries.CreateLogEntryParams) middleware.Responder {
 	err := &models.Error{
 		Code:    http.StatusNotImplemented,
 		Message: "Create Entry API not enabled in this Rekor instance",
@@ -563,7 +563,7 @@ func CreateLogEntryNotImplementedHandler(params entries.CreateLogEntryParams) mi
 	return entries.NewCreateLogEntryDefault(http.StatusNotImplemented).WithPayload(err)
 }
 
-func GetLogEntryByIndexNotImplementedHandler(params entries.GetLogEntryByIndexParams) middleware.Responder {
+func GetLogEntryByIndexNotImplementedHandler(_ entries.GetLogEntryByIndexParams) middleware.Responder {
 	err := &models.Error{
 		Code:    http.StatusNotImplemented,
 		Message: "Get Log Entry by Index API not enabled in this Rekor instance",
@@ -572,7 +572,7 @@ func GetLogEntryByIndexNotImplementedHandler(params entries.GetLogEntryByIndexPa
 	return entries.NewGetLogEntryByIndexDefault(http.StatusNotImplemented).WithPayload(err)
 }
 
-func GetLogEntryByUUIDNotImplementedHandler(params entries.GetLogEntryByUUIDParams) middleware.Responder {
+func GetLogEntryByUUIDNotImplementedHandler(_ entries.GetLogEntryByUUIDParams) middleware.Responder {
 	err := &models.Error{
 		Code:    http.StatusNotImplemented,
 		Message: "Get Log Entry by UUID API not enabled in this Rekor instance",
@@ -581,7 +581,7 @@ func GetLogEntryByUUIDNotImplementedHandler(params entries.GetLogEntryByUUIDPara
 	return entries.NewGetLogEntryByUUIDDefault(http.StatusNotImplemented).WithPayload(err)
 }
 
-func SearchLogQueryNotImplementedHandler(params entries.SearchLogQueryParams) middleware.Responder {
+func SearchLogQueryNotImplementedHandler(_ entries.SearchLogQueryParams) middleware.Responder {
 	err := &models.Error{
 		Code:    http.StatusNotImplemented,
 		Message: "Search Log Query API not enabled in this Rekor instance",
diff --git a/pkg/api/index.go b/pkg/api/index.go
index 7286425..69f695a 100644
--- a/pkg/api/index.go
+++ b/pkg/api/index.go
@@ -89,7 +89,7 @@ func SearchIndexHandler(params index.SearchIndexParams) middleware.Responder {
 	return index.NewSearchIndexOK().WithPayload(result.Values())
 }
 
-func SearchIndexNotImplementedHandler(params index.SearchIndexParams) middleware.Responder {
+func SearchIndexNotImplementedHandler(_ index.SearchIndexParams) middleware.Responder {
 	err := models.Error{
 		Code:    http.StatusNotImplemented,
 		Message: "Search Index API not enabled in this Rekor instance",
diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go
index 36d0d32..8f0efb9 100644
--- a/pkg/api/metrics.go
+++ b/pkg/api/metrics.go
@@ -53,6 +53,11 @@ var (
 		Help: "Api QPS by path, method, and response code",
 	}, []string{"path", "method", "code"})
 
+	CheckpointPublishCount = promauto.NewCounterVec(prometheus.CounterOpts{
+		Name: "rekor_checkpoint_publish",
+		Help: "Checkpoint publishing by shard and code",
+	}, []string{"shard", "code"})
+
 	_ = promauto.NewGaugeFunc(
 		prometheus.GaugeOpts{
 			Namespace: "rekor",
diff --git a/pkg/api/public_key.go b/pkg/api/public_key.go
index e7e6b0f..b4ff916 100644
--- a/pkg/api/public_key.go
+++ b/pkg/api/public_key.go
@@ -36,7 +36,7 @@ func GetPublicKeyHandler(params pubkey.GetPublicKeyParams) middleware.Responder
 
 // handlers for APIs that may be disabled in a given instance
 
-func GetPublicKeyNotImplementedHandler(params pubkey.GetPublicKeyParams) middleware.Responder {
+func GetPublicKeyNotImplementedHandler(_ pubkey.GetPublicKeyParams) middleware.Responder {
 	err := &models.Error{
 		Code:    http.StatusNotImplemented,
 		Message: "Get Public Key API not enabled in this Rekor instance",
diff --git a/pkg/api/tlog.go b/pkg/api/tlog.go
index 33da70b..66d3ae9 100644
--- a/pkg/api/tlog.go
+++ b/pkg/api/tlog.go
@@ -53,6 +53,38 @@ func GetLogInfoHandler(params tlog.GetLogInfoParams) middleware.Responder {
 		inactiveShards = append(inactiveShards, is)
 	}
 
+	if swag.BoolValue(params.Stable) && redisClient != nil {
+		// key is treeID/latest
+		key := fmt.Sprintf("%d/latest", api.logRanges.ActiveTreeID())
+		redisResult, err := redisClient.Get(params.HTTPRequest.Context(), key).Result()
+		if err != nil {
+			return handleRekorAPIError(params, http.StatusInternalServerError,
+				fmt.Errorf("error getting checkpoint from redis: %w", err), "error getting checkpoint from redis")
+		}
+		// should not occur, a checkpoint should always be present
+		if redisResult == "" {
+			return handleRekorAPIError(params, http.StatusInternalServerError,
+				fmt.Errorf("no checkpoint found in redis: %w", err), "no checkpoint found in redis")
+		}
+		decoded, err := hex.DecodeString(redisResult)
+		if err != nil {
+			return handleRekorAPIError(params, http.StatusInternalServerError,
+				fmt.Errorf("error decoding checkpoint from redis: %w", err), "error decoding checkpoint from redis")
+		}
+		checkpoint := util.SignedCheckpoint{}
+		if err := checkpoint.UnmarshalText(decoded); err != nil {
+			return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("invalid checkpoint: %w", err), "invalid checkpoint")
+		}
+		logInfo := models.LogInfo{
+			RootHash:       stringPointer(hex.EncodeToString(checkpoint.Hash)),
+			TreeSize:       swag.Int64(int64(checkpoint.Size)),
+			SignedTreeHead: stringPointer(string(decoded)),
+			TreeID:         stringPointer(fmt.Sprintf("%d", api.logID)),
+			InactiveShards: inactiveShards,
+		}
+		return tlog.NewGetLogInfoOK().WithPayload(&logInfo)
+	}
+
 	resp := tc.GetLatest(0)
 	if resp.Status != codes.OK {
 		return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("grpc error: %w", resp.Err), trillianCommunicationError)
@@ -168,7 +200,7 @@ func inactiveShardLogInfo(ctx context.Context, tid int64) (*models.InactiveShard
 
 // handlers for APIs that may be disabled in a given instance
 
-func GetLogInfoNotImplementedHandler(params tlog.GetLogInfoParams) middleware.Responder {
+func GetLogInfoNotImplementedHandler(_ tlog.GetLogInfoParams) middleware.Responder {
 	err := &models.Error{
 		Code:    http.StatusNotImplemented,
 		Message: "Get Log Info API not enabled in this Rekor instance",
@@ -177,7 +209,7 @@ func GetLogInfoNotImplementedHandler(params tlog.GetLogInfoParams) middleware.Re
 	return tlog.NewGetLogInfoDefault(http.StatusNotImplemented).WithPayload(err)
 }
 
-func GetLogProofNotImplementedHandler(params tlog.GetLogProofParams) middleware.Responder {
+func GetLogProofNotImplementedHandler(_ tlog.GetLogProofParams) middleware.Responder {
 	err := &models.Error{
 		Code:    http.StatusNotImplemented,
 		Message: "Get Log Proof API not enabled in this Rekor instance",
diff --git a/pkg/fuzz/fuzz_utils.go b/pkg/fuzz/fuzz_utils.go
index 15f274c..feb38f0 100644
--- a/pkg/fuzz/fuzz_utils.go
+++ b/pkg/fuzz/fuzz_utils.go
@@ -139,7 +139,7 @@ func setArtifactFields(ff *fuzz.ConsumeFuzzer, props *types.ArtifactProperties)
 }
 
 // creates a file on disk and returns the url of it.
-func createAbsFile(ff *fuzz.ConsumeFuzzer, fileName string, fileContents []byte) (*url.URL, error) {
+func createAbsFile(_ *fuzz.ConsumeFuzzer, fileName string, fileContents []byte) (*url.URL, error) {
 	file, err := os.Create(fileName)
 	if err != nil {
 		return nil, err
diff --git a/pkg/generated/client/tlog/get_log_info_parameters.go b/pkg/generated/client/tlog/get_log_info_parameters.go
index e0ae2cd..b2e3294 100644
--- a/pkg/generated/client/tlog/get_log_info_parameters.go
+++ b/pkg/generated/client/tlog/get_log_info_parameters.go
@@ -30,6 +30,7 @@ import (
 	"github.com/go-openapi/runtime"
 	cr "github.com/go-openapi/runtime/client"
 	"github.com/go-openapi/strfmt"
+	"github.com/go-openapi/swag"
 )
 
 // NewGetLogInfoParams creates a new GetLogInfoParams object,
@@ -76,6 +77,13 @@ GetLogInfoParams contains all the parameters to send to the API endpoint
 	Typically these are written to a http.Request.
 */
 type GetLogInfoParams struct {
+
+	/* Stable.
+
+	   Whether to return a stable checkpoint for the active shard
+	*/
+	Stable *bool
+
 	timeout    time.Duration
 	Context    context.Context
 	HTTPClient *http.Client
@@ -93,7 +101,18 @@ func (o *GetLogInfoParams) WithDefaults() *GetLogInfoParams {
 //
 // All values with no default are reset to their zero value.
 func (o *GetLogInfoParams) SetDefaults() {
-	// no default values defined for this parameter
+	var (
+		stableDefault = bool(false)
+	)
+
+	val := GetLogInfoParams{
+		Stable: &stableDefault,
+	}
+
+	val.timeout = o.timeout
+	val.Context = o.Context
+	val.HTTPClient = o.HTTPClient
+	*o = val
 }
 
 // WithTimeout adds the timeout to the get log info params
@@ -129,6 +148,17 @@ func (o *GetLogInfoParams) SetHTTPClient(client *http.Client) {
 	o.HTTPClient = client
 }
 
+// WithStable adds the stable to the get log info params
+func (o *GetLogInfoParams) WithStable(stable *bool) *GetLogInfoParams {
+	o.SetStable(stable)
+	return o
+}
+
+// SetStable adds the stable to the get log info params
+func (o *GetLogInfoParams) SetStable(stable *bool) {
+	o.Stable = stable
+}
+
 // WriteToRequest writes these params to a swagger request
 func (o *GetLogInfoParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {
 
@@ -137,6 +167,23 @@ func (o *GetLogInfoParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Re
 	}
 	var res []error
 
+	if o.Stable != nil {
+
+		// query param stable
+		var qrStable bool
+
+		if o.Stable != nil {
+			qrStable = *o.Stable
+		}
+		qStable := swag.FormatBool(qrStable)
+		if qStable != "" {
+
+			if err := r.SetQueryParam("stable", qStable); err != nil {
+				return err
+			}
+		}
+	}
+
 	if len(res) > 0 {
 		return errors.CompositeValidationError(res...)
 	}
diff --git a/pkg/generated/models/intoto_v002_schema.go b/pkg/generated/models/intoto_v002_schema.go
index 3297e5a..b630f15 100644
--- a/pkg/generated/models/intoto_v002_schema.go
+++ b/pkg/generated/models/intoto_v002_schema.go
@@ -450,7 +450,6 @@ type IntotoV002SchemaContentEnvelopeSignaturesItems0 struct {
 	Keyid string `json:"keyid,omitempty"`
 
 	// public key that corresponds to this signature
-	// Read Only: true
 	// Format: byte
 	PublicKey strfmt.Base64 `json:"publicKey,omitempty"`
 
@@ -464,26 +463,8 @@ func (m *IntotoV002SchemaContentEnvelopeSignaturesItems0) Validate(formats strfm
 	return nil
 }
 
-// ContextValidate validate this intoto v002 schema content envelope signatures items0 based on the context it is used
+// ContextValidate validates this intoto v002 schema content envelope signatures items0 based on context it is used
 func (m *IntotoV002SchemaContentEnvelopeSignaturesItems0) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
-	var res []error
-
-	if err := m.contextValidatePublicKey(ctx, formats); err != nil {
-		res = append(res, err)
-	}
-
-	if len(res) > 0 {
-		return errors.CompositeValidationError(res...)
-	}
-	return nil
-}
-
-func (m *IntotoV002SchemaContentEnvelopeSignaturesItems0) contextValidatePublicKey(ctx context.Context, formats strfmt.Registry) error {
-
-	if err := validate.ReadOnly(ctx, "publicKey", "body", strfmt.Base64(m.PublicKey)); err != nil {
-		return err
-	}
-
 	return nil
 }
 
diff --git a/pkg/generated/models/tuf_v001_schema.go b/pkg/generated/models/tuf_v001_schema.go
index f8bf4b0..db5d8a3 100644
--- a/pkg/generated/models/tuf_v001_schema.go
+++ b/pkg/generated/models/tuf_v001_schema.go
@@ -195,11 +195,30 @@ func (m *TUFV001Schema) UnmarshalBinary(b []byte) error {
 type TUFV001SchemaMetadata struct {
 
 	// Specifies the metadata inline within the document
-	Content interface{} `json:"content,omitempty"`
+	// Required: true
+	Content interface{} `json:"content"`
 }
 
 // Validate validates this TUF v001 schema metadata
 func (m *TUFV001SchemaMetadata) Validate(formats strfmt.Registry) error {
+	var res []error
+
+	if err := m.validateContent(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+func (m *TUFV001SchemaMetadata) validateContent(formats strfmt.Registry) error {
+
+	if m.Content == nil {
+		return errors.Required("metadata"+"."+"content", "body", nil)
+	}
+
 	return nil
 }
 
diff --git a/pkg/generated/restapi/configure_rekor_server.go b/pkg/generated/restapi/configure_rekor_server.go
index 2041f07..8aab189 100644
--- a/pkg/generated/restapi/configure_rekor_server.go
+++ b/pkg/generated/restapi/configure_rekor_server.go
@@ -65,7 +65,7 @@ type apiToRecord struct {
 	path   *string // Path to record in metrics, if any.
 }
 
-func configureFlags(api *operations.RekorServerAPI) {
+func configureFlags(_ *operations.RekorServerAPI) {
 	// api.CommandLineOptionsGroups = []swag.CommandLineOptionsGroup{ ... }
 }
 
@@ -156,13 +156,15 @@ func configureAPI(api *operations.RekorServerAPI) http.Handler {
 	api.RegisterFormat("signedCheckpoint", &util.SignedNote{}, util.SignedCheckpointValidator)
 
 	api.PreServerShutdown = func() {}
-	api.ServerShutdown = func() {}
+	api.ServerShutdown = func() {
+		pkgapi.StopAPI()
+	}
 
 	return setupGlobalMiddleware(api.Serve(setupMiddlewares))
 }
 
 // The TLS configuration before HTTPS server starts.
-func configureTLS(tlsConfig *tls.Config) {
+func configureTLS(_ *tls.Config) {
 	// Make all necessary changes to the TLS configuration here.
 }
 
@@ -170,7 +172,7 @@ func configureTLS(tlsConfig *tls.Config) {
 // If you need to modify a config, store server instance to stop it individually later, this is the place.
 // This function can be called multiple times, depending on the number of serving schemes.
 // scheme value will be set accordingly: "http", "https" or "unix"
-func configureServer(s *http.Server, scheme, addr string) {
+func configureServer(_ *http.Server, _, _ string) {
 }
 
 // The middleware configuration is for the handler executors. These do not apply to the swagger.json document.
@@ -209,7 +211,7 @@ type zapLogEntry struct {
 	r *http.Request
 }
 
-func (z *zapLogEntry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra interface{}) {
+func (z *zapLogEntry) Write(status, bytes int, _ http.Header, elapsed time.Duration, extra interface{}) {
 	var fields []interface{}
 
 	// follows https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry as a convention
diff --git a/pkg/generated/restapi/embedded_spec.go b/pkg/generated/restapi/embedded_spec.go
index 9466b1f..2f7e5db 100644
--- a/pkg/generated/restapi/embedded_spec.go
+++ b/pkg/generated/restapi/embedded_spec.go
@@ -99,6 +99,15 @@ func init() {
         ],
         "summary": "Get information about the current state of the transparency log",
         "operationId": "getLogInfo",
+        "parameters": [
+          {
+            "type": "boolean",
+            "default": false,
+            "description": "Whether to return a stable checkpoint for the active shard",
+            "name": "stable",
+            "in": "query"
+          }
+        ],
         "responses": {
           "200": {
             "description": "A JSON object with the root hash and tree size as properties",
@@ -995,6 +1004,15 @@ func init() {
         ],
         "summary": "Get information about the current state of the transparency log",
         "operationId": "getLogInfo",
+        "parameters": [
+          {
+            "type": "boolean",
+            "default": false,
+            "description": "Whether to return a stable checkpoint for the active shard",
+            "name": "stable",
+            "in": "query"
+          }
+        ],
         "responses": {
           "200": {
             "description": "A JSON object with the root hash and tree size as properties",
@@ -2042,8 +2060,7 @@ func init() {
         "publicKey": {
           "description": "public key that corresponds to this signature",
           "type": "string",
-          "format": "byte",
-          "readOnly": true
+          "format": "byte"
         },
         "sig": {
           "description": "signature of the payload",
@@ -2665,7 +2682,7 @@ func init() {
       "description": "TUF metadata",
       "type": "object",
       "required": [
-        "metadata"
+        "content"
       ],
       "properties": {
         "content": {
@@ -3852,7 +3869,7 @@ func init() {
           "description": "TUF metadata",
           "type": "object",
           "required": [
-            "metadata"
+            "content"
           ],
           "properties": {
             "content": {
diff --git a/pkg/generated/restapi/operations/tlog/get_log_info_parameters.go b/pkg/generated/restapi/operations/tlog/get_log_info_parameters.go
index f6b7e15..72fc764 100644
--- a/pkg/generated/restapi/operations/tlog/get_log_info_parameters.go
+++ b/pkg/generated/restapi/operations/tlog/get_log_info_parameters.go
@@ -25,15 +25,25 @@ import (
 	"net/http"
 
 	"github.com/go-openapi/errors"
+	"github.com/go-openapi/runtime"
 	"github.com/go-openapi/runtime/middleware"
+	"github.com/go-openapi/strfmt"
+	"github.com/go-openapi/swag"
 )
 
 // NewGetLogInfoParams creates a new GetLogInfoParams object
-//
-// There are no default values defined in the spec.
+// with the default values initialized.
 func NewGetLogInfoParams() GetLogInfoParams {
 
-	return GetLogInfoParams{}
+	var (
+		// initialize parameters with default values
+
+		stableDefault = bool(false)
+	)
+
+	return GetLogInfoParams{
+		Stable: &stableDefault,
+	}
 }
 
 // GetLogInfoParams contains all the bound params for the get log info operation
@@ -44,6 +54,12 @@ type GetLogInfoParams struct {
 
 	// HTTP Request Object
 	HTTPRequest *http.Request `json:"-"`
+
+	/*Whether to return a stable checkpoint for the active shard
+	  In: query
+	  Default: false
+	*/
+	Stable *bool
 }
 
 // BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
@@ -55,8 +71,38 @@ func (o *GetLogInfoParams) BindRequest(r *http.Request, route *middleware.Matche
 
 	o.HTTPRequest = r
 
+	qs := runtime.Values(r.URL.Query())
+
+	qStable, qhkStable, _ := qs.GetOK("stable")
+	if err := o.bindStable(qStable, qhkStable, route.Formats); err != nil {
+		res = append(res, err)
+	}
 	if len(res) > 0 {
 		return errors.CompositeValidationError(res...)
 	}
 	return nil
 }
+
+// bindStable binds and validates parameter Stable from query.
+func (o *GetLogInfoParams) bindStable(rawData []string, hasKey bool, formats strfmt.Registry) error {
+	var raw string
+	if len(rawData) > 0 {
+		raw = rawData[len(rawData)-1]
+	}
+
+	// Required: false
+	// AllowEmptyValue: false
+
+	if raw == "" { // empty values pass all other validations
+		// Default values have been previously initialized by NewGetLogInfoParams()
+		return nil
+	}
+
+	value, err := swag.ConvertBool(raw)
+	if err != nil {
+		return errors.InvalidType("stable", "query", "bool", raw)
+	}
+	o.Stable = &value
+
+	return nil
+}
diff --git a/pkg/generated/restapi/operations/tlog/get_log_info_urlbuilder.go b/pkg/generated/restapi/operations/tlog/get_log_info_urlbuilder.go
index e344fac..19587e4 100644
--- a/pkg/generated/restapi/operations/tlog/get_log_info_urlbuilder.go
+++ b/pkg/generated/restapi/operations/tlog/get_log_info_urlbuilder.go
@@ -25,11 +25,17 @@ import (
 	"errors"
 	"net/url"
 	golangswaggerpaths "path"
+
+	"github.com/go-openapi/swag"
 )
 
 // GetLogInfoURL generates an URL for the get log info operation
 type GetLogInfoURL struct {
+	Stable *bool
+
 	_basePath string
+	// avoid unkeyed usage
+	_ struct{}
 }
 
 // WithBasePath sets the base path for this url builder, only required when it's different from the
@@ -56,6 +62,18 @@ func (o *GetLogInfoURL) Build() (*url.URL, error) {
 	_basePath := o._basePath
 	_result.Path = golangswaggerpaths.Join(_basePath, _path)
 
+	qs := make(url.Values)
+
+	var stableQ string
+	if o.Stable != nil {
+		stableQ = swag.FormatBool(*o.Stable)
+	}
+	if stableQ != "" {
+		qs.Set("stable", stableQ)
+	}
+
+	_result.RawQuery = qs.Encode()
+
 	return &_result, nil
 }
 
diff --git a/pkg/pki/minisign/minisign.go b/pkg/pki/minisign/minisign.go
index 928dfdb..0bcb02f 100644
--- a/pkg/pki/minisign/minisign.go
+++ b/pkg/pki/minisign/minisign.go
@@ -114,7 +114,7 @@ func (s Signature) Verify(r io.Reader, k interface{}, opts ...sigsig.VerifyOptio
 		r = bytes.NewReader(h.Sum(nil))
 	}
 
-	return verifier.VerifySignature(bytes.NewReader(s.signature.Signature[:]), r)
+	return verifier.VerifySignature(bytes.NewReader(s.signature.Signature[:]), r, opts...)
 }
 
 // PublicKey Public Key that follows the minisign standard; supports signify and minisign public keys
diff --git a/pkg/pki/pgp/pgp.go b/pkg/pki/pgp/pgp.go
index fce63ff..aee7fa1 100644
--- a/pkg/pki/pgp/pgp.go
+++ b/pkg/pki/pgp/pgp.go
@@ -134,7 +134,7 @@ func (s Signature) CanonicalValue() ([]byte, error) {
 }
 
 // Verify implements the pki.Signature interface
-func (s Signature) Verify(r io.Reader, k interface{}, opts ...sigsig.VerifyOption) error {
+func (s Signature) Verify(r io.Reader, k interface{}, _ ...sigsig.VerifyOption) error {
 	if len(s.signature) == 0 {
 		return fmt.Errorf("PGP signature has not been initialized")
 	}
diff --git a/pkg/pki/pkcs7/pkcs7.go b/pkg/pki/pkcs7/pkcs7.go
index 6fd5757..8694844 100644
--- a/pkg/pki/pkcs7/pkcs7.go
+++ b/pkg/pki/pkcs7/pkcs7.go
@@ -106,7 +106,7 @@ func (s Signature) CanonicalValue() ([]byte, error) {
 }
 
 // Verify implements the pki.Signature interface
-func (s Signature) Verify(r io.Reader, k interface{}, opts ...sigsig.VerifyOption) error {
+func (s Signature) Verify(r io.Reader, _ interface{}, _ ...sigsig.VerifyOption) error {
 	if len(*s.raw) == 0 {
 		return fmt.Errorf("PKCS7 signature has not been initialized")
 	}
diff --git a/pkg/pki/ssh/ssh.go b/pkg/pki/ssh/ssh.go
index a55a0ef..30f84ba 100644
--- a/pkg/pki/ssh/ssh.go
+++ b/pkg/pki/ssh/ssh.go
@@ -51,7 +51,7 @@ func (s Signature) CanonicalValue() ([]byte, error) {
 }
 
 // Verify implements the pki.Signature interface
-func (s Signature) Verify(r io.Reader, k interface{}, opts ...sigsig.VerifyOption) error {
+func (s Signature) Verify(r io.Reader, k interface{}, _ ...sigsig.VerifyOption) error {
 	if s.signature == nil {
 		return fmt.Errorf("ssh signature has not been initialized")
 	}
diff --git a/pkg/pki/tuf/tuf.go b/pkg/pki/tuf/tuf.go
index 0a0cf49..7da68e2 100644
--- a/pkg/pki/tuf/tuf.go
+++ b/pkg/pki/tuf/tuf.go
@@ -78,7 +78,7 @@ func (s Signature) CanonicalValue() ([]byte, error) {
 }
 
 // Verify implements the pki.Signature interface
-func (s Signature) Verify(_ io.Reader, k interface{}, opts ...sigsig.VerifyOption) error {
+func (s Signature) Verify(_ io.Reader, k interface{}, _ ...sigsig.VerifyOption) error {
 	key, ok := k.(*PublicKey)
 	if !ok {
 		return fmt.Errorf("invalid public key type for: %v", k)
diff --git a/pkg/types/alpine/v0.0.1/entry.go b/pkg/types/alpine/v0.0.1/entry.go
index 827dd76..98d31da 100644
--- a/pkg/types/alpine/v0.0.1/entry.go
+++ b/pkg/types/alpine/v0.0.1/entry.go
@@ -357,3 +357,19 @@ func (v V001Entry) Verifier() (pki.PublicKey, error) {
 	}
 	return x509.NewPublicKey(bytes.NewReader(*v.AlpineModel.PublicKey.Content))
 }
+
+func (v V001Entry) Insertable() (bool, error) {
+	if v.AlpineModel.Package == nil {
+		return false, fmt.Errorf("missing package entry")
+	}
+	if len(v.AlpineModel.Package.Content) == 0 {
+		return false, fmt.Errorf("missing package content")
+	}
+	if v.AlpineModel.PublicKey == nil {
+		return false, fmt.Errorf("missing public key")
+	}
+	if v.AlpineModel.PublicKey.Content == nil || len(*v.AlpineModel.PublicKey.Content) == 0 {
+		return false, fmt.Errorf("missing public key content")
+	}
+	return true, nil
+}
diff --git a/pkg/types/cose/v0.0.1/entry.go b/pkg/types/cose/v0.0.1/entry.go
index 5311026..b7c12a9 100644
--- a/pkg/types/cose/v0.0.1/entry.go
+++ b/pkg/types/cose/v0.0.1/entry.go
@@ -188,7 +188,7 @@ func (v *V001Entry) Unmarshal(pe models.ProposedEntry) error {
 	return v.validate()
 }
 
-func (v *V001Entry) Canonicalize(ctx context.Context) ([]byte, error) {
+func (v *V001Entry) Canonicalize(_ context.Context) ([]byte, error) {
 	if v.keyObj == nil {
 		return nil, errors.New("cannot canonicalze empty key")
 	}
@@ -354,3 +354,26 @@ func (v V001Entry) Verifier() (pki.PublicKey, error) {
 	}
 	return x509.NewPublicKey(bytes.NewReader(*v.CoseObj.PublicKey))
 }
+
+func (v V001Entry) Insertable() (bool, error) {
+	if len(v.CoseObj.Message) == 0 {
+		return false, errors.New("missing COSE Sign1 message")
+	}
+	if v.CoseObj.PublicKey == nil || len(*v.CoseObj.PublicKey) == 0 {
+		return false, errors.New("missing public key")
+	}
+	if v.CoseObj.Data == nil {
+		return false, errors.New("missing COSE data property")
+	}
+	if len(v.envelopeHash) == 0 {
+		return false, errors.New("envelope hash has not been computed")
+	}
+	if v.keyObj == nil {
+		return false, errors.New("public key has not been parsed")
+	}
+	if v.sign1Msg == nil {
+		return false, errors.New("signature has not been validated")
+	}
+
+	return true, nil
+}
diff --git a/pkg/types/entries.go b/pkg/types/entries.go
index 760f0d8..9309b4c 100644
--- a/pkg/types/entries.go
+++ b/pkg/types/entries.go
@@ -38,6 +38,7 @@ type EntryImpl interface {
 	Unmarshal(e models.ProposedEntry) error           // unmarshal the abstract entry into the specific struct for this versioned type
 	CreateFromArtifactProperties(context.Context, ArtifactProperties) (models.ProposedEntry, error)
 	Verifier() (pki.PublicKey, error)
+	Insertable() (bool, error) // denotes whether the entry that was unmarshalled has the writeOnly fields required to validate and insert into the log
 }
 
 // EntryWithAttestationImpl specifies the behavior of a versioned type that also stores attestations
@@ -82,6 +83,12 @@ func CreateVersionedEntry(pe models.ProposedEntry) (EntryImpl, error) {
 		if !tf.(func() TypeImpl)().IsSupportedVersion(ei.APIVersion()) {
 			return nil, fmt.Errorf("entry kind '%v' does not support inserting entries of version '%v'", kind, ei.APIVersion())
 		}
+	} else {
+		return nil, fmt.Errorf("unknown kind '%v' specified", kind)
+	}
+
+	if ok, err := ei.Insertable(); !ok {
+		return nil, fmt.Errorf("entry not insertable into log: %w", err)
 	}
 
 	return ei, nil
diff --git a/pkg/types/hashedrekord/v0.0.1/entry.go b/pkg/types/hashedrekord/v0.0.1/entry.go
index a386d8a..82f54fe 100644
--- a/pkg/types/hashedrekord/v0.0.1/entry.go
+++ b/pkg/types/hashedrekord/v0.0.1/entry.go
@@ -104,7 +104,7 @@ func (v *V001Entry) Unmarshal(pe models.ProposedEntry) error {
 	return err
 }
 
-func (v *V001Entry) Canonicalize(ctx context.Context) ([]byte, error) {
+func (v *V001Entry) Canonicalize(_ context.Context) ([]byte, error) {
 	sigObj, keyObj, err := v.validate()
 	if err != nil {
 		return nil, types.ValidationError(err)
@@ -189,7 +189,7 @@ func (v *V001Entry) validate() (pki.Signature, pki.PublicKey, error) {
 	return sigObj, keyObj, nil
 }
 
-func (v V001Entry) CreateFromArtifactProperties(ctx context.Context, props types.ArtifactProperties) (models.ProposedEntry, error) {
+func (v V001Entry) CreateFromArtifactProperties(_ context.Context, props types.ArtifactProperties) (models.ProposedEntry, error) {
 	returnVal := models.Hashedrekord{}
 	re := V001Entry{}
 
@@ -252,3 +252,31 @@ func (v V001Entry) Verifier() (pki.PublicKey, error) {
 	}
 	return x509.NewPublicKey(bytes.NewReader(v.HashedRekordObj.Signature.PublicKey.Content))
 }
+
+func (v V001Entry) Insertable() (bool, error) {
+	if v.HashedRekordObj.Signature == nil {
+		return false, errors.New("missing signature property")
+	}
+	if len(v.HashedRekordObj.Signature.Content) == 0 {
+		return false, errors.New("missing signature content")
+	}
+	if v.HashedRekordObj.Signature.PublicKey == nil {
+		return false, errors.New("missing publicKey property")
+	}
+	if len(v.HashedRekordObj.Signature.PublicKey.Content) == 0 {
+		return false, errors.New("missing publicKey content")
+	}
+	if v.HashedRekordObj.Data == nil {
+		return false, errors.New("missing data property")
+	}
+	if v.HashedRekordObj.Data.Hash == nil {
+		return false, errors.New("missing hash property")
+	}
+	if v.HashedRekordObj.Data.Hash.Algorithm == nil {
+		return false, errors.New("missing hash algorithm")
+	}
+	if v.HashedRekordObj.Data.Hash.Value == nil {
+		return false, errors.New("missing hash value")
+	}
+	return true, nil
+}
diff --git a/pkg/types/helm/v0.0.1/entry.go b/pkg/types/helm/v0.0.1/entry.go
index e76e752..dfb05c2 100644
--- a/pkg/types/helm/v0.0.1/entry.go
+++ b/pkg/types/helm/v0.0.1/entry.go
@@ -355,3 +355,23 @@ func (v V001Entry) Verifier() (pki.PublicKey, error) {
 	}
 	return pgp.NewPublicKey(bytes.NewReader(*v.HelmObj.PublicKey.Content))
 }
+
+func (v V001Entry) Insertable() (bool, error) {
+	if v.HelmObj.PublicKey == nil {
+		return false, errors.New("missing public key property")
+	}
+	if v.HelmObj.PublicKey.Content == nil || len(*v.HelmObj.PublicKey.Content) == 0 {
+		return false, errors.New("missing public key content")
+	}
+
+	if v.HelmObj.Chart == nil {
+		return false, errors.New("missing chart property")
+	}
+	if v.HelmObj.Chart.Provenance == nil {
+		return false, errors.New("missing provenance property")
+	}
+	if len(v.HelmObj.Chart.Provenance.Content) == 0 {
+		return false, errors.New("missing provenance content")
+	}
+	return true, nil
+}
diff --git a/pkg/types/intoto/v0.0.1/entry.go b/pkg/types/intoto/v0.0.1/entry.go
index 8b84dec..66af807 100644
--- a/pkg/types/intoto/v0.0.1/entry.go
+++ b/pkg/types/intoto/v0.0.1/entry.go
@@ -183,7 +183,7 @@ func (v *V001Entry) Unmarshal(pe models.ProposedEntry) error {
 	return v.validate()
 }
 
-func (v *V001Entry) Canonicalize(ctx context.Context) ([]byte, error) {
+func (v *V001Entry) Canonicalize(_ context.Context) ([]byte, error) {
 	if v.keyObj == nil {
 		return nil, errors.New("cannot canonicalize empty key")
 	}
@@ -356,3 +356,24 @@ func (v V001Entry) Verifier() (pki.PublicKey, error) {
 	}
 	return x509.NewPublicKey(bytes.NewReader(*v.IntotoObj.PublicKey))
 }
+
+func (v V001Entry) Insertable() (bool, error) {
+	if v.IntotoObj.Content == nil {
+		return false, errors.New("missing content property")
+	}
+	if len(v.IntotoObj.Content.Envelope) == 0 {
+		return false, errors.New("missing envelope content")
+	}
+
+	if v.IntotoObj.PublicKey == nil || len(*v.IntotoObj.PublicKey) == 0 {
+		return false, errors.New("missing publicKey content")
+	}
+
+	if v.keyObj == nil {
+		return false, errors.New("failed to parse public key")
+	}
+	if v.env.Payload == "" || v.env.PayloadType == "" || len(v.env.Signatures) == 0 {
+		return false, errors.New("invalid DSSE envelope")
+	}
+	return true, nil
+}
diff --git a/pkg/types/intoto/v0.0.2/entry.go b/pkg/types/intoto/v0.0.2/entry.go
index 083e937..513812d 100644
--- a/pkg/types/intoto/v0.0.2/entry.go
+++ b/pkg/types/intoto/v0.0.2/entry.go
@@ -211,7 +211,7 @@ func (v *V002Entry) Unmarshal(pe models.ProposedEntry) error {
 	return nil
 }
 
-func (v *V002Entry) Canonicalize(ctx context.Context) ([]byte, error) {
+func (v *V002Entry) Canonicalize(_ context.Context) ([]byte, error) {
 	if err := v.IntotoObj.Validate(strfmt.Default); err != nil {
 		return nil, err
 	}
@@ -460,3 +460,37 @@ func (v V002Entry) Verifier() (pki.PublicKey, error) {
 
 	return x509.NewPublicKey(bytes.NewReader(v.IntotoObj.Content.Envelope.Signatures[0].PublicKey))
 }
+
+func (v V002Entry) Insertable() (bool, error) {
+	if v.IntotoObj.Content == nil {
+		return false, errors.New("missing content property")
+	}
+	if v.IntotoObj.Content.Envelope == nil {
+		return false, errors.New("missing envelope property")
+	}
+	if len(v.IntotoObj.Content.Envelope.Payload) == 0 {
+		return false, errors.New("missing envelope content")
+	}
+
+	if v.IntotoObj.Content.Envelope.PayloadType == nil || len(*v.IntotoObj.Content.Envelope.PayloadType) == 0 {
+		return false, errors.New("missing payloadType content")
+	}
+
+	if len(v.IntotoObj.Content.Envelope.Signatures) == 0 {
+		return false, errors.New("missing signatures content")
+	}
+	for _, sig := range v.IntotoObj.Content.Envelope.Signatures {
+		if len(sig.Sig) == 0 {
+			return false, errors.New("missing signature content")
+		}
+		if len(sig.PublicKey) == 0 {
+			return false, errors.New("missing publicKey content")
+		}
+	}
+
+	if v.env.Payload == "" || v.env.PayloadType == "" || len(v.env.Signatures) == 0 {
+		return false, errors.New("invalid DSSE envelope")
+	}
+
+	return true, nil
+}
diff --git a/pkg/types/intoto/v0.0.2/intoto_v0_0_2_schema.json b/pkg/types/intoto/v0.0.2/intoto_v0_0_2_schema.json
index f614ccf..9153b0a 100644
--- a/pkg/types/intoto/v0.0.2/intoto_v0_0_2_schema.json
+++ b/pkg/types/intoto/v0.0.2/intoto_v0_0_2_schema.json
@@ -42,8 +42,7 @@
                                     "publicKey": {
                                         "description": "public key that corresponds to this signature",
                                         "type": "string",
-                                        "format": "byte",
-                                        "readOnly": true
+                                        "format": "byte"
                                     }
                                 }
                             }
diff --git a/pkg/types/jar/v0.0.1/entry.go b/pkg/types/jar/v0.0.1/entry.go
index ab85ccd..60faa25 100644
--- a/pkg/types/jar/v0.0.1/entry.go
+++ b/pkg/types/jar/v0.0.1/entry.go
@@ -110,7 +110,7 @@ func (v *V001Entry) Unmarshal(pe models.ProposedEntry) error {
 	return v.validate()
 }
 
-func (v *V001Entry) fetchExternalEntities(ctx context.Context) (*pkcs7.PublicKey, *pkcs7.Signature, error) {
+func (v *V001Entry) fetchExternalEntities(_ context.Context) (*pkcs7.PublicKey, *pkcs7.Signature, error) {
 	if err := v.validate(); err != nil {
 		return nil, nil, types.ValidationError(err)
 	}
@@ -344,3 +344,14 @@ func (v V001Entry) Verifier() (pki.PublicKey, error) {
 	}
 	return x509.NewPublicKey(bytes.NewReader(*v.JARModel.Signature.PublicKey.Content))
 }
+
+func (v V001Entry) Insertable() (bool, error) {
+	if v.JARModel.Archive == nil {
+		return false, errors.New("missing archive property")
+	}
+	if len(v.JARModel.Archive.Content) == 0 {
+		return false, errors.New("missing archive content")
+	}
+
+	return true, nil
+}
diff --git a/pkg/types/rekord/v0.0.1/entry.go b/pkg/types/rekord/v0.0.1/entry.go
index b2853c8..dd124e1 100644
--- a/pkg/types/rekord/v0.0.1/entry.go
+++ b/pkg/types/rekord/v0.0.1/entry.go
@@ -443,3 +443,30 @@ func (v V001Entry) Verifier() (pki.PublicKey, error) {
 		return nil, fmt.Errorf("unexpected format of public key: %s", f)
 	}
 }
+
+func (v V001Entry) Insertable() (bool, error) {
+	if v.RekordObj.Signature == nil {
+		return false, errors.New("missing signature property")
+	}
+	if v.RekordObj.Signature.Content == nil || len(*v.RekordObj.Signature.Content) == 0 {
+		return false, errors.New("missing signature content")
+	}
+	if v.RekordObj.Signature.PublicKey == nil {
+		return false, errors.New("missing publicKey property")
+	}
+	if v.RekordObj.Signature.PublicKey.Content == nil || len(*v.RekordObj.Signature.PublicKey.Content) == 0 {
+		return false, errors.New("missing publicKey content")
+	}
+	if v.RekordObj.Signature.Format == nil || len(*v.RekordObj.Signature.Format) == 0 {
+		return false, errors.New("missing signature format")
+	}
+
+	if v.RekordObj.Data == nil {
+		return false, errors.New("missing data property")
+	}
+	if len(v.RekordObj.Data.Content) == 0 {
+		return false, errors.New("missing data content")
+	}
+
+	return true, nil
+}
diff --git a/pkg/types/rfc3161/v0.0.1/entry.go b/pkg/types/rfc3161/v0.0.1/entry.go
index eed8bfa..35a21e0 100644
--- a/pkg/types/rfc3161/v0.0.1/entry.go
+++ b/pkg/types/rfc3161/v0.0.1/entry.go
@@ -118,7 +118,7 @@ func (v *V001Entry) Unmarshal(pe models.ProposedEntry) error {
 	return nil
 }
 
-func (v *V001Entry) Canonicalize(ctx context.Context) ([]byte, error) {
+func (v *V001Entry) Canonicalize(_ context.Context) ([]byte, error) {
 	if v.tsrContent == nil {
 		return nil, errors.New("tsr content must be set before canonicalizing")
 	}
@@ -210,3 +210,19 @@ func (v V001Entry) CreateFromArtifactProperties(_ context.Context, props types.A
 func (v V001Entry) Verifier() (pki.PublicKey, error) {
 	return nil, errors.New("Verifier() does not support rfc3161 entry type")
 }
+
+func (v V001Entry) Insertable() (bool, error) {
+	if v.Rfc3161Obj.Tsr == nil {
+		return false, errors.New("missing tsr property")
+	}
+
+	if v.Rfc3161Obj.Tsr.Content == nil || len(*v.Rfc3161Obj.Tsr.Content) == 0 {
+		return false, errors.New("missing tsr content")
+	}
+
+	if v.tsrContent == nil || len(*v.tsrContent) == 0 {
+		return false, errors.New("timestamp response has not been parsed")
+	}
+
+	return true, nil
+}
diff --git a/pkg/types/rpm/v0.0.1/entry.go b/pkg/types/rpm/v0.0.1/entry.go
index 79e5e3a..a10b60c 100644
--- a/pkg/types/rpm/v0.0.1/entry.go
+++ b/pkg/types/rpm/v0.0.1/entry.go
@@ -377,3 +377,20 @@ func (v V001Entry) Verifier() (pki.PublicKey, error) {
 	}
 	return pgp.NewPublicKey(bytes.NewReader(*v.RPMModel.PublicKey.Content))
 }
+
+func (v V001Entry) Insertable() (bool, error) {
+	if v.RPMModel.PublicKey == nil {
+		return false, errors.New("missing publicKey property")
+	}
+	if v.RPMModel.PublicKey.Content == nil || len(*v.RPMModel.PublicKey.Content) == 0 {
+		return false, errors.New("missing publicKey content")
+	}
+
+	if v.RPMModel.Package == nil {
+		return false, errors.New("missing package property")
+	}
+	if len(v.RPMModel.Package.Content) == 0 {
+		return false, errors.New("missing package content")
+	}
+	return true, nil
+}
diff --git a/pkg/types/tuf/v0.0.1/entry.go b/pkg/types/tuf/v0.0.1/entry.go
index 7e4e5f9..ec471c9 100644
--- a/pkg/types/tuf/v0.0.1/entry.go
+++ b/pkg/types/tuf/v0.0.1/entry.go
@@ -379,3 +379,20 @@ func (v V001Entry) Verifier() (pki.PublicKey, error) {
 	}
 	return ptuf.NewPublicKey(bytes.NewReader(keyBytes))
 }
+
+func (v V001Entry) Insertable() (bool, error) {
+	if v.TufObj.Metadata == nil {
+		return false, errors.New("missing metadata property")
+	}
+	if v.TufObj.Metadata.Content == nil {
+		return false, errors.New("missing metadata content")
+	}
+
+	if v.TufObj.Root == nil {
+		return false, errors.New("missing root property")
+	}
+	if v.TufObj.Root.Content == nil {
+		return false, errors.New("missing root content")
+	}
+	return true, nil
+}
diff --git a/pkg/types/tuf/v0.0.1/tuf_v0_0_1_schema.json b/pkg/types/tuf/v0.0.1/tuf_v0_0_1_schema.json
index f1a6cd3..5d2dc21 100644
--- a/pkg/types/tuf/v0.0.1/tuf_v0_0_1_schema.json
+++ b/pkg/types/tuf/v0.0.1/tuf_v0_0_1_schema.json
@@ -20,7 +20,7 @@
                     "additionalProperties": true
                 }
             },
-            "required": [ "metadata" ]
+            "required": [ "content" ]
         },
         "root" : {
             "description": "root metadata containing about the public keys used to sign the manifest",
diff --git a/pkg/witness/mockclient/generate.go b/pkg/witness/mockclient/generate.go
new file mode 100644
index 0000000..a3be36d
--- /dev/null
+++ b/pkg/witness/mockclient/generate.go
@@ -0,0 +1,18 @@
+// Copyright 2023 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 mockclient provides a mockable version of the Trillian log client API.
+package mockclient
+
+//go:generate mockgen -package mockclient -destination mock_log_client.go github.com/google/trillian TrillianLogClient
diff --git a/pkg/witness/mockclient/mock_log_client.go b/pkg/witness/mockclient/mock_log_client.go
new file mode 100644
index 0000000..33859f9
--- /dev/null
+++ b/pkg/witness/mockclient/mock_log_client.go
@@ -0,0 +1,217 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/google/trillian (interfaces: TrillianLogClient)
+
+// Package mockclient is a generated GoMock package.
+package mockclient
+
+import (
+	context "context"
+	reflect "reflect"
+
+	gomock "github.com/golang/mock/gomock"
+	trillian "github.com/google/trillian"
+	grpc "google.golang.org/grpc"
+)
+
+// MockTrillianLogClient is a mock of TrillianLogClient interface.
+type MockTrillianLogClient struct {
+	ctrl     *gomock.Controller
+	recorder *MockTrillianLogClientMockRecorder
+}
+
+// MockTrillianLogClientMockRecorder is the mock recorder for MockTrillianLogClient.
+type MockTrillianLogClientMockRecorder struct {
+	mock *MockTrillianLogClient
+}
+
+// NewMockTrillianLogClient creates a new mock instance.
+func NewMockTrillianLogClient(ctrl *gomock.Controller) *MockTrillianLogClient {
+	mock := &MockTrillianLogClient{ctrl: ctrl}
+	mock.recorder = &MockTrillianLogClientMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockTrillianLogClient) EXPECT() *MockTrillianLogClientMockRecorder {
+	return m.recorder
+}
+
+// AddSequencedLeaves mocks base method.
+func (m *MockTrillianLogClient) AddSequencedLeaves(arg0 context.Context, arg1 *trillian.AddSequencedLeavesRequest, arg2 ...grpc.CallOption) (*trillian.AddSequencedLeavesResponse, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "AddSequencedLeaves", varargs...)
+	ret0, _ := ret[0].(*trillian.AddSequencedLeavesResponse)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// AddSequencedLeaves indicates an expected call of AddSequencedLeaves.
+func (mr *MockTrillianLogClientMockRecorder) AddSequencedLeaves(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddSequencedLeaves", reflect.TypeOf((*MockTrillianLogClient)(nil).AddSequencedLeaves), varargs...)
+}
+
+// GetConsistencyProof mocks base method.
+func (m *MockTrillianLogClient) GetConsistencyProof(arg0 context.Context, arg1 *trillian.GetConsistencyProofRequest, arg2 ...grpc.CallOption) (*trillian.GetConsistencyProofResponse, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "GetConsistencyProof", varargs...)
+	ret0, _ := ret[0].(*trillian.GetConsistencyProofResponse)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetConsistencyProof indicates an expected call of GetConsistencyProof.
+func (mr *MockTrillianLogClientMockRecorder) GetConsistencyProof(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConsistencyProof", reflect.TypeOf((*MockTrillianLogClient)(nil).GetConsistencyProof), varargs...)
+}
+
+// GetEntryAndProof mocks base method.
+func (m *MockTrillianLogClient) GetEntryAndProof(arg0 context.Context, arg1 *trillian.GetEntryAndProofRequest, arg2 ...grpc.CallOption) (*trillian.GetEntryAndProofResponse, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "GetEntryAndProof", varargs...)
+	ret0, _ := ret[0].(*trillian.GetEntryAndProofResponse)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetEntryAndProof indicates an expected call of GetEntryAndProof.
+func (mr *MockTrillianLogClientMockRecorder) GetEntryAndProof(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEntryAndProof", reflect.TypeOf((*MockTrillianLogClient)(nil).GetEntryAndProof), varargs...)
+}
+
+// GetInclusionProof mocks base method.
+func (m *MockTrillianLogClient) GetInclusionProof(arg0 context.Context, arg1 *trillian.GetInclusionProofRequest, arg2 ...grpc.CallOption) (*trillian.GetInclusionProofResponse, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "GetInclusionProof", varargs...)
+	ret0, _ := ret[0].(*trillian.GetInclusionProofResponse)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetInclusionProof indicates an expected call of GetInclusionProof.
+func (mr *MockTrillianLogClientMockRecorder) GetInclusionProof(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInclusionProof", reflect.TypeOf((*MockTrillianLogClient)(nil).GetInclusionProof), varargs...)
+}
+
+// GetInclusionProofByHash mocks base method.
+func (m *MockTrillianLogClient) GetInclusionProofByHash(arg0 context.Context, arg1 *trillian.GetInclusionProofByHashRequest, arg2 ...grpc.CallOption) (*trillian.GetInclusionProofByHashResponse, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "GetInclusionProofByHash", varargs...)
+	ret0, _ := ret[0].(*trillian.GetInclusionProofByHashResponse)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetInclusionProofByHash indicates an expected call of GetInclusionProofByHash.
+func (mr *MockTrillianLogClientMockRecorder) GetInclusionProofByHash(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInclusionProofByHash", reflect.TypeOf((*MockTrillianLogClient)(nil).GetInclusionProofByHash), varargs...)
+}
+
+// GetLatestSignedLogRoot mocks base method.
+func (m *MockTrillianLogClient) GetLatestSignedLogRoot(arg0 context.Context, arg1 *trillian.GetLatestSignedLogRootRequest, arg2 ...grpc.CallOption) (*trillian.GetLatestSignedLogRootResponse, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "GetLatestSignedLogRoot", varargs...)
+	ret0, _ := ret[0].(*trillian.GetLatestSignedLogRootResponse)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetLatestSignedLogRoot indicates an expected call of GetLatestSignedLogRoot.
+func (mr *MockTrillianLogClientMockRecorder) GetLatestSignedLogRoot(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLatestSignedLogRoot", reflect.TypeOf((*MockTrillianLogClient)(nil).GetLatestSignedLogRoot), varargs...)
+}
+
+// GetLeavesByRange mocks base method.
+func (m *MockTrillianLogClient) GetLeavesByRange(arg0 context.Context, arg1 *trillian.GetLeavesByRangeRequest, arg2 ...grpc.CallOption) (*trillian.GetLeavesByRangeResponse, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "GetLeavesByRange", varargs...)
+	ret0, _ := ret[0].(*trillian.GetLeavesByRangeResponse)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetLeavesByRange indicates an expected call of GetLeavesByRange.
+func (mr *MockTrillianLogClientMockRecorder) GetLeavesByRange(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLeavesByRange", reflect.TypeOf((*MockTrillianLogClient)(nil).GetLeavesByRange), varargs...)
+}
+
+// InitLog mocks base method.
+func (m *MockTrillianLogClient) InitLog(arg0 context.Context, arg1 *trillian.InitLogRequest, arg2 ...grpc.CallOption) (*trillian.InitLogResponse, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "InitLog", varargs...)
+	ret0, _ := ret[0].(*trillian.InitLogResponse)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// InitLog indicates an expected call of InitLog.
+func (mr *MockTrillianLogClientMockRecorder) InitLog(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InitLog", reflect.TypeOf((*MockTrillianLogClient)(nil).InitLog), varargs...)
+}
+
+// QueueLeaf mocks base method.
+func (m *MockTrillianLogClient) QueueLeaf(arg0 context.Context, arg1 *trillian.QueueLeafRequest, arg2 ...grpc.CallOption) (*trillian.QueueLeafResponse, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "QueueLeaf", varargs...)
+	ret0, _ := ret[0].(*trillian.QueueLeafResponse)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// QueueLeaf indicates an expected call of QueueLeaf.
+func (mr *MockTrillianLogClientMockRecorder) QueueLeaf(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueueLeaf", reflect.TypeOf((*MockTrillianLogClient)(nil).QueueLeaf), varargs...)
+}
diff --git a/pkg/witness/publish_checkpoint.go b/pkg/witness/publish_checkpoint.go
new file mode 100644
index 0000000..8f946ff
--- /dev/null
+++ b/pkg/witness/publish_checkpoint.go
@@ -0,0 +1,199 @@
+// Copyright 2023 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 witness
+
+import (
+	"context"
+	"encoding/hex"
+	"fmt"
+	"strconv"
+	"time"
+
+	"github.com/google/trillian"
+	"github.com/google/trillian/types"
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/redis/go-redis/v9"
+	"github.com/sigstore/rekor/pkg/log"
+	"github.com/sigstore/rekor/pkg/trillianclient"
+	"github.com/sigstore/rekor/pkg/util"
+	"github.com/sigstore/sigstore/pkg/signature"
+	"google.golang.org/grpc/codes"
+)
+
+// CheckpointPublisher is a long-running job to periodically publish signed checkpoints to etc.d
+type CheckpointPublisher struct {
+	ctx context.Context
+	// logClient is the client for Trillian
+	logClient trillian.TrillianLogClient
+	// treeID is used to construct the origin and configure the Trillian client
+	treeID int64
+	// hostname is used to construct the origin ("hostname - treeID")
+	hostname string
+	// signer signs the checkpoint
+	signer signature.Signer
+	// publishFreq is how often a new checkpoint is published to Rekor, in minutes
+	checkpointFreq uint
+	// redisClient to upload signed checkpoints
+	redisClient *redis.Client
+	// reqCounter tracks successes and failures for publishing
+	reqCounter *prometheus.CounterVec
+}
+
+// Constant values used with metrics
+const (
+	Success = iota
+	SuccessObtainLock
+	GetCheckpoint
+	UnmarshalCheckpoint
+	SignCheckpoint
+	RedisFailure
+	RedisLatestFailure
+)
+
+// NewCheckpointPublisher creates a CheckpointPublisher to write stable checkpoints to Redis
+func NewCheckpointPublisher(ctx context.Context,
+	logClient trillian.TrillianLogClient,
+	treeID int64,
+	hostname string,
+	signer signature.Signer,
+	redisClient *redis.Client,
+	checkpointFreq uint,
+	reqCounter *prometheus.CounterVec) CheckpointPublisher {
+	return CheckpointPublisher{ctx: ctx, logClient: logClient, treeID: treeID, hostname: hostname,
+		signer: signer, checkpointFreq: checkpointFreq, redisClient: redisClient, reqCounter: reqCounter}
+}
+
+// StartPublisher creates a long-running task that publishes the latest checkpoint every X minutes
+// Writing to Redis is best effort. Failure will be detected either through metrics or by witnesses
+// or Verifiers monitoring for fresh checkpoints. Failure can occur after a lock is obtained but
+// before publishing the latest checkpoint. If this occurs due to a sporadic failure, this simply
+// means that a witness will not see a fresh checkpoint for an additional period.
+func (c *CheckpointPublisher) StartPublisher(ctx context.Context) {
+	tc := trillianclient.NewTrillianClient(context.Background(), c.logClient, c.treeID)
+	sTreeID := strconv.FormatInt(c.treeID, 10)
+
+	// publish on startup to ensure a checkpoint is available the first time Rekor starts up
+	c.publish(&tc, sTreeID)
+
+	ticker := time.NewTicker(time.Duration(c.checkpointFreq) * time.Minute)
+	go func() {
+		for {
+			select {
+			case <-ctx.Done():
+				return
+			case <-ticker.C:
+				c.publish(&tc, sTreeID)
+			}
+		}
+	}()
+}
+
+// publish publishes the latest checkpoint to Redis once
+func (c *CheckpointPublisher) publish(tc *trillianclient.TrillianClient, sTreeID string) {
+	// get latest checkpoint
+	resp := tc.GetLatest(0)
+	if resp.Status != codes.OK {
+		c.reqCounter.With(
+			map[string]string{
+				"shard": sTreeID,
+				"code":  strconv.Itoa(GetCheckpoint),
+			}).Inc()
+		log.Logger.Errorf("error getting latest checkpoint to publish: %v", resp.Status)
+		return
+	}
+
+	// unmarshal checkpoint
+	root := &types.LogRootV1{}
+	if err := root.UnmarshalBinary(resp.GetLatestResult.SignedLogRoot.LogRoot); err != nil {
+		c.reqCounter.With(
+			map[string]string{
+				"shard": sTreeID,
+				"code":  strconv.Itoa(UnmarshalCheckpoint),
+			}).Inc()
+		log.Logger.Errorf("error unmarshalling latest checkpoint to publish: %v", err)
+		return
+	}
+
+	// sign checkpoint with Rekor private key
+	checkpoint, err := util.CreateAndSignCheckpoint(context.Background(), c.hostname, c.treeID, root, c.signer)
+	if err != nil {
+		c.reqCounter.With(
+			map[string]string{
+				"shard": sTreeID,
+				"code":  strconv.Itoa(SignCheckpoint),
+			}).Inc()
+		log.Logger.Errorf("error signing checkpoint to publish: %v", err)
+		return
+	}
+
+	// encode checkpoint as hex to write to redis
+	hexCP := hex.EncodeToString(checkpoint)
+
+	// write checkpoint to Redis if key does not yet exist
+	// this prevents multiple instances of Rekor from writing different checkpoints in the same time window
+	ts := time.Now().Truncate(time.Duration(c.checkpointFreq) * time.Minute).UnixNano()
+	// key is treeID/timestamp, where timestamp is rounded down to the nearest X minutes
+	key := fmt.Sprintf("%d/%d", c.treeID, ts)
+	ctx, cancel := context.WithTimeout(c.ctx, 10*time.Second)
+	defer cancel()
+
+	// return value ignored, which is whether or not the entry was set
+	// no error is thrown if the key already exists
+	successNX, err := c.redisClient.SetNX(ctx, key, hexCP, 0).Result()
+	if err != nil {
+		c.reqCounter.With(
+			map[string]string{
+				"shard": sTreeID,
+				"code":  strconv.Itoa(RedisFailure),
+			}).Inc()
+		log.Logger.Errorf("error with client publishing checkpoint: %v", err)
+		return
+	}
+	// if the key was not set, then the key already exists for this time period
+	if !successNX {
+		return
+	}
+
+	// successful obtaining of lock for time period
+	c.reqCounter.With(
+		map[string]string{
+			"shard": sTreeID,
+			"code":  strconv.Itoa(SuccessObtainLock),
+		}).Inc()
+
+	// on successfully obtaining the "lock" for the time window, update latest checkpoint
+	latestKey := fmt.Sprintf("%d/latest", c.treeID)
+	latestCtx, latestCancel := context.WithTimeout(c.ctx, 10*time.Second)
+	defer latestCancel()
+
+	// return value ignored, which is whether or not the entry was set
+	// no error is thrown if the key already exists
+	if _, err = c.redisClient.Set(latestCtx, latestKey, hexCP, 0).Result(); err != nil {
+		c.reqCounter.With(
+			map[string]string{
+				"shard": sTreeID,
+				"code":  strconv.Itoa(RedisLatestFailure),
+			}).Inc()
+		log.Logger.Errorf("error with client publishing latest checkpoint: %v", err)
+		return
+	}
+
+	// successful publish
+	c.reqCounter.With(
+		map[string]string{
+			"shard": sTreeID,
+			"code":  strconv.Itoa(Success),
+		}).Inc()
+}


Test patch:
diff --git a/cmd/rekor-cli/main_test.go b/cmd/rekor-cli/main_test.go
index 6a11ccb..fd89027 100644
--- a/cmd/rekor-cli/main_test.go
+++ b/cmd/rekor-cli/main_test.go
@@ -21,6 +21,6 @@ import (
 	"github.com/sigstore/rekor/cmd/rekor-cli/app"
 )
 
-func TestCover(t *testing.T) {
+func TestCover(_ *testing.T) {
 	app.Execute()
 }
diff --git a/cmd/rekor-server/main_test.go b/cmd/rekor-server/main_test.go
index 8fa9c4e..5c151b8 100644
--- a/cmd/rekor-server/main_test.go
+++ b/cmd/rekor-server/main_test.go
@@ -21,6 +21,6 @@ import (
 	"github.com/sigstore/rekor/cmd/rekor-server/app"
 )
 
-func TestCover(t *testing.T) {
+func TestCover(_ *testing.T) {
 	app.Execute()
 }
diff --git a/pkg/pki/minisign/minisign_test.go b/pkg/pki/minisign/minisign_test.go
index 4487b47..c91db02 100644
--- a/pkg/pki/minisign/minisign_test.go
+++ b/pkg/pki/minisign/minisign_test.go
@@ -122,7 +122,7 @@ func TestReadSignature(t *testing.T) {
 type BadReader struct {
 }
 
-func (br BadReader) Read(p []byte) (n int, err error) {
+func (br BadReader) Read(_ []byte) (n int, err error) {
 	return 0, errors.New("test error")
 }
 
diff --git a/pkg/pki/pgp/pgp_test.go b/pkg/pki/pgp/pgp_test.go
index 60ed66c..20362f7 100644
--- a/pkg/pki/pgp/pgp_test.go
+++ b/pkg/pki/pgp/pgp_test.go
@@ -94,7 +94,7 @@ func TestReadSignature(t *testing.T) {
 type BadReader struct {
 }
 
-func (br BadReader) Read(p []byte) (n int, err error) {
+func (br BadReader) Read(_ []byte) (n int, err error) {
 	return 0, errors.New("test error")
 }
 
diff --git a/pkg/types/alpine/alpine_test.go b/pkg/types/alpine/alpine_test.go
index 3d0e10e..a961968 100644
--- a/pkg/types/alpine/alpine_test.go
+++ b/pkg/types/alpine/alpine_test.go
@@ -39,7 +39,7 @@ func (u UnmarshalFailsTester) NewEntry() types.EntryImpl {
 	return &UnmarshalFailsTester{}
 }
 
-func (u UnmarshalFailsTester) Unmarshal(pe models.ProposedEntry) error {
+func (u UnmarshalFailsTester) Unmarshal(_ models.ProposedEntry) error {
 	return errors.New("error")
 }
 
@@ -47,6 +47,10 @@ func (u UnmarshalFailsTester) Verifier() (pki.PublicKey, error) {
 	return nil, nil
 }
 
+func (u UnmarshalFailsTester) Insertable() (bool, error) {
+	return false, nil
+}
+
 func TestAlpineType(t *testing.T) {
 	// empty to start
 	if VersionMap.Count() != 0 {
diff --git a/pkg/types/alpine/v0.0.1/entry_test.go b/pkg/types/alpine/v0.0.1/entry_test.go
index abd6a25..f6c6cc8 100644
--- a/pkg/types/alpine/v0.0.1/entry_test.go
+++ b/pkg/types/alpine/v0.0.1/entry_test.go
@@ -151,6 +151,12 @@ func TestCrossFieldValidation(t *testing.T) {
 			t.Errorf("unexpected result in '%v': %v", tc.caseDesc, err)
 		}
 
+		if tc.expectUnmarshalSuccess {
+			if ok, err := v.Insertable(); !ok || err != nil {
+				t.Errorf("unexpected result in calling Insertable on valid proposed entry: %v", err)
+			}
+		}
+
 		b, err := v.Canonicalize(context.TODO())
 		if (err == nil) != tc.expectCanonicalizeSuccess {
 			t.Errorf("unexpected result from Canonicalize for '%v': %v", tc.caseDesc, err)
@@ -164,9 +170,13 @@ func TestCrossFieldValidation(t *testing.T) {
 			if err != nil {
 				t.Errorf("unexpected err from Unmarshalling canonicalized entry for '%v': %v", tc.caseDesc, err)
 			}
-			if _, err := types.UnmarshalEntry(pe); err != nil {
+			ei, err := types.UnmarshalEntry(pe)
+			if err != nil {
 				t.Errorf("unexpected err from type-specific unmarshalling for '%v': %v", tc.caseDesc, err)
 			}
+			if ok, err := ei.Insertable(); ok || err == nil {
+				t.Errorf("unexpected success calling Insertable on entry created from canonicalized content")
+			}
 		}
 
 		verifier, err := v.Verifier()
@@ -187,3 +197,114 @@ func TestCrossFieldValidation(t *testing.T) {
 		}
 	}
 }
+
+func TestInsertable(t *testing.T) {
+	type TestCase struct {
+		caseDesc      string
+		entry         V001Entry
+		expectSuccess bool
+	}
+
+	pub := strfmt.Base64([]byte("pub"))
+
+	testCases := []TestCase{
+		{
+			caseDesc: "valid entry",
+			entry: V001Entry{
+				AlpineModel: models.AlpineV001Schema{
+					Package: &models.AlpineV001SchemaPackage{
+						Content: strfmt.Base64("package"),
+					},
+					PublicKey: &models.AlpineV001SchemaPublicKey{
+						Content: &pub,
+					},
+				},
+			},
+			expectSuccess: true,
+		},
+		{
+			caseDesc: "missing key content",
+			entry: V001Entry{
+				AlpineModel: models.AlpineV001Schema{
+					Package: &models.AlpineV001SchemaPackage{
+						Content: strfmt.Base64("package"),
+					},
+					PublicKey: &models.AlpineV001SchemaPublicKey{
+						//Content: &pub,
+					},
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing public key",
+			entry: V001Entry{
+				AlpineModel: models.AlpineV001Schema{
+					Package: &models.AlpineV001SchemaPackage{
+						Content: strfmt.Base64("package"),
+					},
+					/*
+						PublicKey: &models.AlpineV001SchemaPublicKey{
+							Content: &pub,
+						},
+					*/
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing package content",
+			entry: V001Entry{
+				AlpineModel: models.AlpineV001Schema{
+					Package: &models.AlpineV001SchemaPackage{
+						//Content: strfmt.Base64("package"),
+					},
+					PublicKey: &models.AlpineV001SchemaPublicKey{
+						Content: &pub,
+					},
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing package",
+			entry: V001Entry{
+				AlpineModel: models.AlpineV001Schema{
+					/*
+						Package: &models.AlpineV001SchemaPackage{
+							Content: strfmt.Base64("package"),
+						},
+					*/
+					PublicKey: &models.AlpineV001SchemaPublicKey{
+						Content: &pub,
+					},
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "empty model",
+			entry: V001Entry{
+				AlpineModel: models.AlpineV001Schema{
+					/*
+						Package: &models.AlpineV001SchemaPackage{
+							Content: strfmt.Base64("package"),
+						},
+						PublicKey: &models.AlpineV001SchemaPublicKey{
+							Content: &pub,
+						},
+					*/
+				},
+			},
+			expectSuccess: false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			if ok, err := tc.entry.Insertable(); ok != tc.expectSuccess {
+				t.Errorf("unexpected result calling Insertable: %v", err)
+			}
+		})
+	}
+}
diff --git a/pkg/types/alpine/v0.0.1/fuzz_test.go b/pkg/types/alpine/v0.0.1/fuzz_test.go
index 568e1d7..9b96e75 100644
--- a/pkg/types/alpine/v0.0.1/fuzz_test.go
+++ b/pkg/types/alpine/v0.0.1/fuzz_test.go
@@ -21,8 +21,11 @@ import (
 	"testing"
 
 	fuzz "github.com/AdamKorcz/go-fuzz-headers-1"
+	"github.com/go-openapi/swag"
 
 	fuzzUtils "github.com/sigstore/rekor/pkg/fuzz"
+	"github.com/sigstore/rekor/pkg/generated/models"
+	"github.com/sigstore/rekor/pkg/types"
 	"github.com/sigstore/rekor/pkg/types/alpine"
 )
 
@@ -47,10 +50,48 @@ func FuzzAlpineCreateProposedEntry(f *testing.F) {
 		if err != nil {
 			t.Skip()
 		}
-		c, err := it.UnmarshalEntry(entry)
+
+		ei, err := types.CreateVersionedEntry(entry)
 		if err != nil {
 			t.Skip()
 		}
-		_, _ = c.IndexKeys()
+
+		if ok, err := ei.Insertable(); !ok || err != nil {
+			t.Errorf("entry created via CreateProposedEntry should be insertable: %v", err)
+		}
+
+		if _, err := types.CanonicalizeEntry(context.Background(), ei); err != nil {
+			t.Errorf("valid insertable entry should be able to be canonicalized: %v", err)
+		}
+
+		_, _ = ei.IndexKeys()
+	})
+}
+
+func FuzzAlpineUnmarshalAndCanonicalize(f *testing.F) {
+	f.Fuzz(func(t *testing.T, entryData []byte) {
+		initter.Do(fuzzUtils.SetFuzzLogger)
+
+		ff := fuzz.NewConsumer(entryData)
+
+		targetV001 := &models.AlpineV001Schema{}
+
+		if err := ff.GenerateStruct(targetV001); err != nil {
+			t.Skip()
+		}
+
+		targetEntry := &models.Alpine{
+			APIVersion: swag.String(APIVERSION),
+			Spec:       targetV001,
+		}
+
+		ei, err := types.UnmarshalEntry(targetEntry)
+		if err != nil {
+			t.Skip()
+		}
+
+		if _, err := types.CanonicalizeEntry(context.Background(), ei); err != nil {
+			t.Errorf("error canonicalizing unmarshalled entry: %v", err)
+		}
 	})
 }
diff --git a/pkg/types/cose/cose_test.go b/pkg/types/cose/cose_test.go
index 2a0cfb1..080253a 100644
--- a/pkg/types/cose/cose_test.go
+++ b/pkg/types/cose/cose_test.go
@@ -40,7 +40,7 @@ func (u UnmarshalFailsTester) NewEntry() types.EntryImpl {
 	return &UnmarshalFailsTester{}
 }
 
-func (u UnmarshalFailsTester) Unmarshal(pe models.ProposedEntry) error {
+func (u UnmarshalFailsTester) Unmarshal(_ models.ProposedEntry) error {
 	return errors.New("error")
 }
 
@@ -48,6 +48,10 @@ func (u UnmarshalFailsTester) Verifier() (pki.PublicKey, error) {
 	return nil, nil
 }
 
+func (u UnmarshalFailsTester) Insertable() (bool, error) {
+	return false, nil
+}
+
 func TestCOSEType(t *testing.T) {
 	// empty to start
 	if VersionMap.Count() != 0 {
diff --git a/pkg/types/cose/v0.0.1/entry_test.go b/pkg/types/cose/v0.0.1/entry_test.go
index 582e423..34af5cf 100644
--- a/pkg/types/cose/v0.0.1/entry_test.go
+++ b/pkg/types/cose/v0.0.1/entry_test.go
@@ -17,6 +17,7 @@ package cose
 
 import (
 	"bytes"
+	"context"
 	"crypto/ecdsa"
 	"crypto/elliptic"
 	"crypto/rand"
@@ -30,6 +31,7 @@ import (
 	"reflect"
 	"testing"
 
+	"github.com/go-openapi/runtime"
 	"github.com/go-openapi/strfmt"
 	"github.com/sigstore/rekor/pkg/generated/models"
 	sigx509 "github.com/sigstore/rekor/pkg/pki/x509"
@@ -270,12 +272,39 @@ func TestV001Entry_Unmarshal(t *testing.T) {
 				t.Errorf("V001Entry.Unmarshal() error = %v, wantErr %v", err, tt.wantErr)
 			}
 
+			if !tt.wantErr {
+				if ok, err := v.Insertable(); !ok || err != nil {
+					t.Errorf("unexpected error calling Insertable on valid proposed entry: %v", err)
+				}
+			}
+
 			verifier, err := v.Verifier()
 			if !tt.wantVerifierErr {
 				if err != nil {
 					s, _ := verifier.CanonicalValue()
 					t.Errorf("%v: unexpected error for %v, got %v", tt.name, string(s), err)
 				}
+
+				if !tt.wantErr {
+					b, err := v.Canonicalize(context.Background())
+					if err != nil {
+						t.Errorf("unexpected error canonicalizing %v", tt.name)
+					}
+					if len(b) != 0 {
+						pe, err := models.UnmarshalProposedEntry(bytes.NewReader(b), runtime.JSONConsumer())
+						if err != nil {
+							t.Errorf("unexpected err from Unmarshalling canonicalized entry for '%v': %v", tt.name, err)
+						}
+						ei, err := types.UnmarshalEntry(pe)
+						if err != nil {
+							t.Errorf("unexpected err from type-specific unmarshalling for '%v': %v", tt.name, err)
+						}
+						if ok, err := ei.Insertable(); ok || err == nil {
+							t.Errorf("entry created from canonicalized entry should not also be insertable")
+						}
+					}
+				}
+
 				pubV, _ := verifier.CanonicalValue()
 				if !reflect.DeepEqual(pubV, pub) && !reflect.DeepEqual(pubV, pemBytes) {
 					t.Errorf("verifier and public keys do not match: %v, %v", string(pubV), string(pub))
@@ -690,3 +719,202 @@ func mustContain(t *testing.T, want string, l []string) {
 	}
 	t.Fatalf("list %v does not contain %s", l, want)
 }
+
+func TestInsertable(t *testing.T) {
+	type TestCase struct {
+		caseDesc      string
+		entry         V001Entry
+		expectSuccess bool
+	}
+
+	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",
+	})
+	keyObj, err := sigx509.NewPublicKey(bytes.NewReader(pub))
+	if err != nil {
+		t.Fatal(err)
+	}
+	pubKey := strfmt.Base64(pub)
+
+	testCases := []TestCase{
+		{
+			caseDesc: "valid entry",
+			entry: V001Entry{
+				CoseObj: models.CoseV001Schema{
+					Data: &models.CoseV001SchemaData{
+						Aad: strfmt.Base64([]byte("aad")),
+					},
+					Message:   strfmt.Base64([]byte("message")),
+					PublicKey: &pubKey,
+				},
+				keyObj:       keyObj,
+				sign1Msg:     &gocose.Sign1Message{},
+				envelopeHash: []byte("hash"),
+			},
+			expectSuccess: true,
+		},
+		{
+			caseDesc: "no aad",
+			entry: V001Entry{
+				CoseObj: models.CoseV001Schema{
+					Data:      &models.CoseV001SchemaData{},
+					Message:   strfmt.Base64([]byte("message")),
+					PublicKey: &pubKey,
+				},
+				keyObj:       keyObj,
+				sign1Msg:     &gocose.Sign1Message{},
+				envelopeHash: []byte("hash"),
+			},
+			expectSuccess: true,
+		},
+		{
+			caseDesc: "missing hash",
+			entry: V001Entry{
+				CoseObj: models.CoseV001Schema{
+					Data:      &models.CoseV001SchemaData{},
+					Message:   strfmt.Base64([]byte("message")),
+					PublicKey: &pubKey,
+				},
+				keyObj:   keyObj,
+				sign1Msg: &gocose.Sign1Message{},
+				//envelopeHash: []byte("hash"),
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "unparsed message",
+			entry: V001Entry{
+				CoseObj: models.CoseV001Schema{
+					Data: &models.CoseV001SchemaData{
+						Aad: strfmt.Base64([]byte("aad")),
+					},
+					Message:   strfmt.Base64([]byte("message")),
+					PublicKey: &pubKey,
+				},
+				keyObj: keyObj,
+				//sign1Msg:     &gocose.Sign1Message{},
+				envelopeHash: []byte("hash"),
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "unparsed public key",
+			entry: V001Entry{
+				CoseObj: models.CoseV001Schema{
+					Data: &models.CoseV001SchemaData{
+						Aad: strfmt.Base64([]byte("aad")),
+					},
+					Message:   strfmt.Base64([]byte("message")),
+					PublicKey: &pubKey,
+				},
+				//keyObj:       keyObj,
+				sign1Msg:     &gocose.Sign1Message{},
+				envelopeHash: []byte("hash"),
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing public key",
+			entry: V001Entry{
+				CoseObj: models.CoseV001Schema{
+					Data: &models.CoseV001SchemaData{
+						Aad: strfmt.Base64([]byte("aad")),
+					},
+					Message: strfmt.Base64([]byte("message")),
+					//PublicKey: &pubKey,
+				},
+				keyObj:       keyObj,
+				sign1Msg:     &gocose.Sign1Message{},
+				envelopeHash: []byte("hash"),
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing unparsed message",
+			entry: V001Entry{
+				CoseObj: models.CoseV001Schema{
+					Data: &models.CoseV001SchemaData{
+						Aad: strfmt.Base64([]byte("aad")),
+					},
+					//Message:   strfmt.Base64([]byte("message")),
+					PublicKey: &pubKey,
+				},
+				keyObj:       keyObj,
+				sign1Msg:     &gocose.Sign1Message{},
+				envelopeHash: []byte("hash"),
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing data",
+			entry: V001Entry{
+				CoseObj: models.CoseV001Schema{
+					/*
+						Data: &models.CoseV001SchemaData{
+							Aad: strfmt.Base64([]byte("aad")),
+						},
+					*/
+					Message:   strfmt.Base64([]byte("message")),
+					PublicKey: &pubKey,
+				},
+				keyObj:       keyObj,
+				sign1Msg:     &gocose.Sign1Message{},
+				envelopeHash: []byte("hash"),
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing cose obj",
+			entry: V001Entry{
+				/*
+					CoseObj: models.CoseV001Schema{
+						Data: &models.CoseV001SchemaData{
+							Aad: strfmt.Base64([]byte("aad")),
+						},
+						Message:   strfmt.Base64([]byte("message")),
+						PublicKey: &pubKey,
+					},
+				*/
+				keyObj:       keyObj,
+				sign1Msg:     &gocose.Sign1Message{},
+				envelopeHash: []byte("hash"),
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "empty obj",
+			entry:    V001Entry{
+				/*
+					CoseObj: models.CoseV001Schema{
+						Data: &models.CoseV001SchemaData{
+							Aad: strfmt.Base64([]byte("aad")),
+						},
+						Message:   strfmt.Base64([]byte("message")),
+						PublicKey: &pubKey,
+					},
+					keyObj:       keyObj,
+					sign1Msg:     &gocose.Sign1Message{},
+					envelopeHash: []byte("hash"),
+				*/
+			},
+			expectSuccess: false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			if ok, err := tc.entry.Insertable(); ok != tc.expectSuccess {
+				t.Errorf("unexpected result calling Insertable: %v", err)
+			}
+		})
+	}
+}
diff --git a/pkg/types/cose/v0.0.1/fuzz_test.go b/pkg/types/cose/v0.0.1/fuzz_test.go
index d7520d8..5545c48 100644
--- a/pkg/types/cose/v0.0.1/fuzz_test.go
+++ b/pkg/types/cose/v0.0.1/fuzz_test.go
@@ -21,8 +21,11 @@ import (
 	"testing"
 
 	fuzz "github.com/AdamKorcz/go-fuzz-headers-1"
+	"github.com/go-openapi/swag"
 
 	fuzzUtils "github.com/sigstore/rekor/pkg/fuzz"
+	"github.com/sigstore/rekor/pkg/generated/models"
+	"github.com/sigstore/rekor/pkg/types"
 	"github.com/sigstore/rekor/pkg/types/cose"
 )
 
@@ -47,9 +50,48 @@ func FuzzCoseCreateProposedEntry(f *testing.F) {
 		if err != nil {
 			t.Skip()
 		}
-		_, err = it.UnmarshalEntry(entry)
+
+		ei, err := types.CreateVersionedEntry(entry)
 		if err != nil {
 			t.Skip()
 		}
+
+		if ok, err := ei.Insertable(); !ok || err != nil {
+			t.Errorf("entry created via CreateProposedEntry should be insertable: %v", err)
+		}
+
+		if _, err := types.CanonicalizeEntry(context.Background(), ei); err != nil {
+			t.Errorf("valid insertable entry should be able to be canonicalized: %v", err)
+		}
+
+		_, _ = ei.IndexKeys()
+	})
+}
+
+func FuzzCoseUnmarshalAndCanonicalize(f *testing.F) {
+	f.Fuzz(func(t *testing.T, entryData []byte) {
+		initter.Do(fuzzUtils.SetFuzzLogger)
+
+		ff := fuzz.NewConsumer(entryData)
+
+		targetV001 := &models.CoseV001Schema{}
+
+		if err := ff.GenerateStruct(targetV001); err != nil {
+			t.Skip()
+		}
+
+		targetEntry := &models.Cose{
+			APIVersion: swag.String(APIVERSION),
+			Spec:       targetV001,
+		}
+
+		ei, err := types.UnmarshalEntry(targetEntry)
+		if err != nil {
+			t.Skip()
+		}
+
+		if _, err := types.CanonicalizeEntry(context.Background(), ei); err != nil {
+			t.Errorf("error canonicalizing unmarshalled entry: %v", err)
+		}
 	})
 }
diff --git a/pkg/types/hashedrekord/hashedrekord_test.go b/pkg/types/hashedrekord/hashedrekord_test.go
index 213e13b..e033954 100644
--- a/pkg/types/hashedrekord/hashedrekord_test.go
+++ b/pkg/types/hashedrekord/hashedrekord_test.go
@@ -39,7 +39,7 @@ func (u UnmarshalFailsTester) NewEntry() types.EntryImpl {
 	return &UnmarshalFailsTester{}
 }
 
-func (u UnmarshalFailsTester) Unmarshal(pe models.ProposedEntry) error {
+func (u UnmarshalFailsTester) Unmarshal(_ models.ProposedEntry) error {
 	return errors.New("error")
 }
 
diff --git a/pkg/types/hashedrekord/v0.0.1/entry_test.go b/pkg/types/hashedrekord/v0.0.1/entry_test.go
index e78ae21..ba9a528 100644
--- a/pkg/types/hashedrekord/v0.0.1/entry_test.go
+++ b/pkg/types/hashedrekord/v0.0.1/entry_test.go
@@ -284,6 +284,13 @@ func TestCrossFieldValidation(t *testing.T) {
 			t.Errorf("unexpected result in '%v': %v", tc.caseDesc, err)
 		}
 
+		if tc.expectUnmarshalSuccess {
+			ok, err := v.Insertable()
+			if !ok || err != nil {
+				t.Errorf("unexpected failure in testing insertable on valid entry: %v", err)
+			}
+		}
+
 		b, err := v.Canonicalize(context.TODO())
 		if (err == nil) != tc.expectCanonicalizeSuccess {
 			t.Errorf("unexpected result from Canonicalize for '%v': %v", tc.caseDesc, err)
@@ -297,9 +304,15 @@ func TestCrossFieldValidation(t *testing.T) {
 			if err != nil {
 				t.Errorf("unexpected err from Unmarshalling canonicalized entry for '%v': %v", tc.caseDesc, err)
 			}
-			if _, err := types.UnmarshalEntry(pe); err != nil {
+			ei, err := types.UnmarshalEntry(pe)
+			if err != nil {
 				t.Errorf("unexpected err from type-specific unmarshalling for '%v': %v", tc.caseDesc, err)
 			}
+			// hashedrekord is one of two types (rfc3161, hashedrekord) in that what is persisted is also insertable
+			ok, err := ei.Insertable()
+			if !ok || err != nil {
+				t.Errorf("unexpected failure in testing insertable on entry created from canonicalized content: %v", err)
+			}
 		}
 
 		verifier, err := v.Verifier()
@@ -443,3 +456,203 @@ func testKeyAndCert(t *testing.T) ([]byte, []byte, *ecdsa.PrivateKey) {
 
 	return pub, certPem, priv
 }
+
+func TestInsertable(t *testing.T) {
+	type TestCase struct {
+		caseDesc      string
+		entry         V001Entry
+		expectSuccess bool
+	}
+
+	testCases := []TestCase{
+		{
+			caseDesc: "valid entry",
+			entry: V001Entry{
+				HashedRekordObj: models.HashedrekordV001Schema{
+					Data: &models.HashedrekordV001SchemaData{
+						Hash: &models.HashedrekordV001SchemaDataHash{
+							Algorithm: swag.String(models.HashedrekordV001SchemaDataHashAlgorithmSha256),
+							Value:     swag.String("deadbeef"),
+						},
+					},
+					Signature: &models.HashedrekordV001SchemaSignature{
+						Content: strfmt.Base64("sig"),
+						PublicKey: &models.HashedrekordV001SchemaSignaturePublicKey{
+							Content: strfmt.Base64("key"),
+						},
+					},
+				},
+			},
+			expectSuccess: true,
+		},
+		{
+			caseDesc: "missing key",
+			entry: V001Entry{
+				HashedRekordObj: models.HashedrekordV001Schema{
+					Data: &models.HashedrekordV001SchemaData{
+						Hash: &models.HashedrekordV001SchemaDataHash{
+							Algorithm: swag.String(models.HashedrekordV001SchemaDataHashAlgorithmSha256),
+							Value:     swag.String("deadbeef"),
+						},
+					},
+					Signature: &models.HashedrekordV001SchemaSignature{
+						Content: strfmt.Base64("sig"),
+					},
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing key content",
+			entry: V001Entry{
+				HashedRekordObj: models.HashedrekordV001Schema{
+					Data: &models.HashedrekordV001SchemaData{
+						Hash: &models.HashedrekordV001SchemaDataHash{
+							Algorithm: swag.String(models.HashedrekordV001SchemaDataHashAlgorithmSha256),
+							Value:     swag.String("deadbeef"),
+						},
+					},
+					Signature: &models.HashedrekordV001SchemaSignature{
+						Content:   strfmt.Base64("sig"),
+						PublicKey: &models.HashedrekordV001SchemaSignaturePublicKey{},
+					},
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing key content",
+			entry: V001Entry{
+				HashedRekordObj: models.HashedrekordV001Schema{
+					Data: &models.HashedrekordV001SchemaData{
+						Hash: &models.HashedrekordV001SchemaDataHash{
+							Algorithm: swag.String(models.HashedrekordV001SchemaDataHashAlgorithmSha256),
+							Value:     swag.String("deadbeef"),
+						},
+					},
+					Signature: &models.HashedrekordV001SchemaSignature{
+						Content:   strfmt.Base64("sig"),
+						PublicKey: nil,
+					},
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing sig content",
+			entry: V001Entry{
+				HashedRekordObj: models.HashedrekordV001Schema{
+					Data: &models.HashedrekordV001SchemaData{
+						Hash: &models.HashedrekordV001SchemaDataHash{
+							Algorithm: swag.String(models.HashedrekordV001SchemaDataHashAlgorithmSha256),
+							Value:     swag.String("deadbeef"),
+						},
+					},
+					Signature: &models.HashedrekordV001SchemaSignature{
+						Content: nil,
+						PublicKey: &models.HashedrekordV001SchemaSignaturePublicKey{
+							Content: strfmt.Base64("key"),
+						},
+					},
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing hash value",
+			entry: V001Entry{
+				HashedRekordObj: models.HashedrekordV001Schema{
+					Data: &models.HashedrekordV001SchemaData{
+						Hash: &models.HashedrekordV001SchemaDataHash{
+							Algorithm: swag.String(models.HashedrekordV001SchemaDataHashAlgorithmSha256),
+						},
+					},
+					Signature: &models.HashedrekordV001SchemaSignature{
+						Content: strfmt.Base64("sig"),
+						PublicKey: &models.HashedrekordV001SchemaSignaturePublicKey{
+							Content: strfmt.Base64("key"),
+						},
+					},
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing hash algorithm",
+			entry: V001Entry{
+				HashedRekordObj: models.HashedrekordV001Schema{
+					Data: &models.HashedrekordV001SchemaData{
+						Hash: &models.HashedrekordV001SchemaDataHash{
+							Value: swag.String("deadbeef"),
+						},
+					},
+					Signature: &models.HashedrekordV001SchemaSignature{
+						Content: strfmt.Base64("sig"),
+						PublicKey: &models.HashedrekordV001SchemaSignaturePublicKey{
+							Content: strfmt.Base64("key"),
+						},
+					},
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing hash object",
+			entry: V001Entry{
+				HashedRekordObj: models.HashedrekordV001Schema{
+					Data: &models.HashedrekordV001SchemaData{},
+					Signature: &models.HashedrekordV001SchemaSignature{
+						Content: strfmt.Base64("sig"),
+						PublicKey: &models.HashedrekordV001SchemaSignaturePublicKey{
+							Content: strfmt.Base64("key"),
+						},
+					},
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing data object",
+			entry: V001Entry{
+				HashedRekordObj: models.HashedrekordV001Schema{
+					Signature: &models.HashedrekordV001SchemaSignature{
+						Content: strfmt.Base64("sig"),
+						PublicKey: &models.HashedrekordV001SchemaSignaturePublicKey{
+							Content: strfmt.Base64("key"),
+						},
+					},
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing sig object",
+			entry: V001Entry{
+				HashedRekordObj: models.HashedrekordV001Schema{
+					Data: &models.HashedrekordV001SchemaData{
+						Hash: &models.HashedrekordV001SchemaDataHash{
+							Algorithm: swag.String(models.HashedrekordV001SchemaDataHashAlgorithmSha256),
+							Value:     swag.String("deadbeef"),
+						},
+					},
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "empty object",
+			entry: V001Entry{
+				HashedRekordObj: models.HashedrekordV001Schema{},
+			},
+			expectSuccess: false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			if ok, err := tc.entry.Insertable(); ok != tc.expectSuccess {
+				t.Errorf("unexpected result calling Insertable: %v", err)
+			}
+		})
+	}
+}
diff --git a/pkg/types/hashedrekord/v0.0.1/fuzz_test.go b/pkg/types/hashedrekord/v0.0.1/fuzz_test.go
index 7adb281..4a3d546 100644
--- a/pkg/types/hashedrekord/v0.0.1/fuzz_test.go
+++ b/pkg/types/hashedrekord/v0.0.1/fuzz_test.go
@@ -21,8 +21,11 @@ import (
 	"testing"
 
 	fuzz "github.com/AdamKorcz/go-fuzz-headers-1"
+	"github.com/go-openapi/swag"
 
 	fuzzUtils "github.com/sigstore/rekor/pkg/fuzz"
+	"github.com/sigstore/rekor/pkg/generated/models"
+	"github.com/sigstore/rekor/pkg/types"
 	"github.com/sigstore/rekor/pkg/types/hashedrekord"
 )
 
@@ -47,10 +50,48 @@ func FuzzHashedRekordCreateProposedEntry(f *testing.F) {
 		if err != nil {
 			t.Skip()
 		}
-		c, err := it.UnmarshalEntry(entry)
+
+		ei, err := types.CreateVersionedEntry(entry)
 		if err != nil {
 			t.Skip()
 		}
-		_, _ = c.IndexKeys()
+
+		if ok, err := ei.Insertable(); !ok || err != nil {
+			t.Errorf("entry created via CreateProposedEntry should be insertable: %v", err)
+		}
+
+		if _, err := types.CanonicalizeEntry(context.Background(), ei); err != nil {
+			t.Errorf("valid insertable entry should be able to be canonicalized: %v", err)
+		}
+
+		_, _ = ei.IndexKeys()
+	})
+}
+
+func FuzzHashedRekordUnmarshalAndCanonicalize(f *testing.F) {
+	f.Fuzz(func(t *testing.T, entryData []byte) {
+		initter.Do(fuzzUtils.SetFuzzLogger)
+
+		ff := fuzz.NewConsumer(entryData)
+
+		targetV001 := &models.HashedrekordV001Schema{}
+
+		if err := ff.GenerateStruct(targetV001); err != nil {
+			t.Skip()
+		}
+
+		targetEntry := &models.Hashedrekord{
+			APIVersion: swag.String(APIVERSION),
+			Spec:       targetV001,
+		}
+
+		ei, err := types.UnmarshalEntry(targetEntry)
+		if err != nil {
+			t.Skip()
+		}
+
+		if _, err := types.CanonicalizeEntry(context.Background(), ei); err != nil {
+			t.Errorf("error canonicalizing unmarshalled entry: %v", err)
+		}
 	})
 }
diff --git a/pkg/types/helm/helm_test.go b/pkg/types/helm/helm_test.go
index a8d2fda..db9d7a7 100644
--- a/pkg/types/helm/helm_test.go
+++ b/pkg/types/helm/helm_test.go
@@ -39,7 +39,7 @@ func (u UnmarshalFailsTester) NewEntry() types.EntryImpl {
 	return &UnmarshalFailsTester{}
 }
 
-func (u UnmarshalFailsTester) Unmarshal(pe models.ProposedEntry) error {
+func (u UnmarshalFailsTester) Unmarshal(_ models.ProposedEntry) error {
 	return errors.New("error")
 }
 
diff --git a/pkg/types/helm/v0.0.1/entry_test.go b/pkg/types/helm/v0.0.1/entry_test.go
index eabde47..36a4cf3 100644
--- a/pkg/types/helm/v0.0.1/entry_test.go
+++ b/pkg/types/helm/v0.0.1/entry_test.go
@@ -182,6 +182,13 @@ func TestCrossFieldValidation(t *testing.T) {
 				t.Errorf("unexpected result in '%v': %v", tc.caseDesc, err)
 			}
 
+			if tc.expectUnmarshalSuccess {
+				ok, err := v.Insertable()
+				if !ok || err != nil {
+					t.Errorf("unexpected error calling Insertable on valid proposed entry: %v", err)
+				}
+			}
+
 			b, err := v.Canonicalize(context.TODO())
 			if (err == nil) != tc.expectCanonicalizeSuccess {
 				t.Errorf("unexpected result from Canonicalize for '%v': %v", tc.caseDesc, err)
@@ -195,9 +202,13 @@ func TestCrossFieldValidation(t *testing.T) {
 				if err != nil {
 					t.Errorf("unexpected err from Unmarshalling canonicalized entry for '%v': %v", tc.caseDesc, err)
 				}
-				if _, err := types.UnmarshalEntry(pe); err != nil {
+				ei, err := types.UnmarshalEntry(pe)
+				if err != nil {
 					t.Errorf("unexpected err from type-specific unmarshalling for '%v': %v", tc.caseDesc, err)
 				}
+				if ok, err := ei.Insertable(); ok || err == nil {
+					t.Errorf("unexpected success calling Insertable on entry created from canonicalized content")
+				}
 			}
 
 			verifier, err := v.Verifier()
@@ -220,3 +231,131 @@ func TestCrossFieldValidation(t *testing.T) {
 		})
 	}
 }
+
+func TestInsertable(t *testing.T) {
+	type TestCase struct {
+		caseDesc      string
+		entry         V001Entry
+		expectSuccess bool
+	}
+
+	pubKey := strfmt.Base64([]byte("pubKey"))
+
+	testCases := []TestCase{
+		{
+			caseDesc: "valid entry",
+			entry: V001Entry{
+				HelmObj: models.HelmV001Schema{
+					Chart: &models.HelmV001SchemaChart{
+						Provenance: &models.HelmV001SchemaChartProvenance{
+							Content: strfmt.Base64([]byte("content")),
+						},
+					},
+					PublicKey: &models.HelmV001SchemaPublicKey{
+						Content: &pubKey,
+					},
+				},
+			},
+			expectSuccess: true,
+		},
+		{
+			caseDesc: "missing key content",
+			entry: V001Entry{
+				HelmObj: models.HelmV001Schema{
+					Chart: &models.HelmV001SchemaChart{
+						Provenance: &models.HelmV001SchemaChartProvenance{
+							Content: strfmt.Base64([]byte("content")),
+						},
+					},
+					PublicKey: &models.HelmV001SchemaPublicKey{
+						//Content: &pubKey,
+					},
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing key",
+			entry: V001Entry{
+				HelmObj: models.HelmV001Schema{
+					Chart: &models.HelmV001SchemaChart{
+						Provenance: &models.HelmV001SchemaChartProvenance{
+							Content: strfmt.Base64([]byte("content")),
+						},
+					},
+					/*
+						PublicKey: &models.HelmV001SchemaPublicKey{
+							Content: &pubKey,
+						},
+					*/
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing provenance content",
+			entry: V001Entry{
+				HelmObj: models.HelmV001Schema{
+					Chart: &models.HelmV001SchemaChart{
+						Provenance: &models.HelmV001SchemaChartProvenance{
+							//Content: strfmt.Base64([]byte("content")),
+						},
+					},
+					PublicKey: &models.HelmV001SchemaPublicKey{
+						Content: &pubKey,
+					},
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing provenance obj",
+			entry: V001Entry{
+				HelmObj: models.HelmV001Schema{
+					Chart: &models.HelmV001SchemaChart{
+						/*
+							Provenance: &models.HelmV001SchemaChartProvenance{
+								Content: strfmt.Base64([]byte("content")),
+							},
+						*/
+					},
+					PublicKey: &models.HelmV001SchemaPublicKey{
+						Content: &pubKey,
+					},
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing chart obj",
+			entry: V001Entry{
+				HelmObj: models.HelmV001Schema{
+					/*
+						Chart: &models.HelmV001SchemaChart{
+							Provenance: &models.HelmV001SchemaChartProvenance{
+								Content: strfmt.Base64([]byte("content")),
+							},
+						},
+					*/
+					PublicKey: &models.HelmV001SchemaPublicKey{
+						Content: &pubKey,
+					},
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc:      "empty obj",
+			entry:         V001Entry{},
+			expectSuccess: false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			if ok, err := tc.entry.Insertable(); ok != tc.expectSuccess {
+				t.Errorf("unexpected result calling Insertable: %v", err)
+			}
+		})
+	}
+}
diff --git a/pkg/types/helm/v0.0.1/fuzz_test.go b/pkg/types/helm/v0.0.1/fuzz_test.go
index 5e091ee..717f27a 100644
--- a/pkg/types/helm/v0.0.1/fuzz_test.go
+++ b/pkg/types/helm/v0.0.1/fuzz_test.go
@@ -22,8 +22,11 @@ import (
 	"testing"
 
 	fuzz "github.com/AdamKorcz/go-fuzz-headers-1"
+	"github.com/go-openapi/swag"
 
 	fuzzUtils "github.com/sigstore/rekor/pkg/fuzz"
+	"github.com/sigstore/rekor/pkg/generated/models"
+	"github.com/sigstore/rekor/pkg/types"
 	"github.com/sigstore/rekor/pkg/types/helm"
 )
 
@@ -48,11 +51,48 @@ func FuzzHelmCreateProposedEntry(f *testing.F) {
 		if err != nil {
 			t.Skip()
 		}
-		c, err := it.UnmarshalEntry(entry)
+		ei, err := types.CreateVersionedEntry(entry)
 		if err != nil {
 			t.Skip()
 		}
-		_, _ = c.IndexKeys()
+
+		if ok, err := ei.Insertable(); !ok || err != nil {
+			t.Errorf("entry created via CreateProposedEntry should be insertable: %v", err)
+		}
+
+		if _, err := types.CanonicalizeEntry(context.Background(), ei); err != nil {
+			t.Errorf("valid insertable entry should be able to be canonicalized: %v", err)
+		}
+
+		_, _ = ei.IndexKeys()
+	})
+}
+
+func FuzzHelmUnmarshalAndCanonicalize(f *testing.F) {
+	f.Fuzz(func(t *testing.T, entryData []byte) {
+		initter.Do(fuzzUtils.SetFuzzLogger)
+
+		ff := fuzz.NewConsumer(entryData)
+
+		targetV001 := &models.HelmV001Schema{}
+
+		if err := ff.GenerateStruct(targetV001); err != nil {
+			t.Skip()
+		}
+
+		targetEntry := &models.Helm{
+			APIVersion: swag.String(APIVERSION),
+			Spec:       targetV001,
+		}
+
+		ei, err := types.UnmarshalEntry(targetEntry)
+		if err != nil {
+			t.Skip()
+		}
+
+		if _, err := types.CanonicalizeEntry(context.Background(), ei); err != nil {
+			t.Errorf("error canonicalizing unmarshalled entry: %v", err)
+		}
 	})
 }
 
diff --git a/pkg/types/intoto/intoto_test.go b/pkg/types/intoto/intoto_test.go
index 7feeb2a..af4c90f 100644
--- a/pkg/types/intoto/intoto_test.go
+++ b/pkg/types/intoto/intoto_test.go
@@ -39,7 +39,7 @@ func (u UnmarshalFailsTester) NewEntry() types.EntryImpl {
 	return &UnmarshalFailsTester{}
 }
 
-func (u UnmarshalFailsTester) Unmarshal(pe models.ProposedEntry) error {
+func (u UnmarshalFailsTester) Unmarshal(_ models.ProposedEntry) error {
 	return errors.New("error")
 }
 
diff --git a/pkg/types/intoto/v0.0.1/entry_test.go b/pkg/types/intoto/v0.0.1/entry_test.go
index d63c033..d48b2b4 100644
--- a/pkg/types/intoto/v0.0.1/entry_test.go
+++ b/pkg/types/intoto/v0.0.1/entry_test.go
@@ -46,6 +46,7 @@ import (
 	slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
 	"github.com/secure-systems-lab/go-securesystemslib/dsse"
 	"github.com/sigstore/rekor/pkg/generated/models"
+	pkix509 "github.com/sigstore/rekor/pkg/pki/x509"
 	"github.com/sigstore/rekor/pkg/types"
 	"github.com/sigstore/sigstore/pkg/signature"
 	dsse_signer "github.com/sigstore/sigstore/pkg/signature/dsse"
@@ -277,6 +278,13 @@ func TestV001Entry_Unmarshal(t *testing.T) {
 				if err := v.Unmarshal(it); err != nil {
 					return err
 				}
+
+				if !tt.wantErr {
+					if ok, err := v.Insertable(); !ok || err != nil {
+						t.Errorf("unexpected error calling Insertable on valid proposed entry: %v", err)
+					}
+				}
+
 				if v.IntotoObj.Content.Hash == nil || v.IntotoObj.Content.Hash.Algorithm != tt.it.Content.Hash.Algorithm || v.IntotoObj.Content.Hash.Value != tt.it.Content.Hash.Value {
 					return errors.New("missing envelope hash in validated object")
 				}
@@ -310,6 +318,11 @@ func TestV001Entry_Unmarshal(t *testing.T) {
 				if err != nil {
 					t.Errorf("unexpected err from type-specific unmarshalling for '%v': %v", tt.name, err)
 				}
+
+				if ok, err := canonicalEntry.Insertable(); ok || err == nil {
+					t.Errorf("unexpected success calling Insertable on entry created from canonicalized content")
+				}
+
 				canonicalV001 := canonicalEntry.(*V001Entry)
 				fmt.Printf("%v", canonicalV001.IntotoObj.Content)
 				if *canonicalV001.IntotoObj.Content.Hash.Value != *tt.it.Content.Hash.Value {
@@ -438,3 +451,136 @@ func TestV001Entry_IndexKeys(t *testing.T) {
 		})
 	}
 }
+
+func TestInsertable(t *testing.T) {
+	type TestCase struct {
+		caseDesc      string
+		entry         V001Entry
+		expectSuccess bool
+	}
+
+	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",
+	})
+	keyObj, err := pkix509.NewPublicKey(bytes.NewReader(pub))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	envStr := envelope(t, key, "payload", "payloadType")
+	env := dsse.Envelope{}
+
+	if err := json.Unmarshal([]byte(envStr), &env); err != nil {
+		t.Fatal(err)
+	}
+
+	testCases := []TestCase{
+		{
+			caseDesc: "valid entry",
+			entry: V001Entry{
+				IntotoObj: models.IntotoV001Schema{
+					Content: &models.IntotoV001SchemaContent{
+						Envelope: "envelope",
+					},
+					PublicKey: p(pub),
+				},
+				keyObj: keyObj,
+				env:    env,
+			},
+			expectSuccess: true,
+		},
+		{
+			caseDesc: "missing parsed keyObj",
+			entry: V001Entry{
+				IntotoObj: models.IntotoV001Schema{
+					Content: &models.IntotoV001SchemaContent{
+						Envelope: "envelope",
+					},
+					PublicKey: p(pub),
+				},
+				env: env,
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing parsed DSSE envelope",
+			entry: V001Entry{
+				IntotoObj: models.IntotoV001Schema{
+					Content: &models.IntotoV001SchemaContent{
+						Envelope: "envelope",
+					},
+					PublicKey: p(pub),
+				},
+				keyObj: keyObj,
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing content",
+			entry: V001Entry{
+				IntotoObj: models.IntotoV001Schema{
+					PublicKey: p(pub),
+				},
+				keyObj: keyObj,
+				env:    env,
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing envelope string",
+			entry: V001Entry{
+				IntotoObj: models.IntotoV001Schema{
+					Content:   &models.IntotoV001SchemaContent{},
+					PublicKey: p(pub),
+				},
+				keyObj: keyObj,
+				env:    env,
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing unparsed public key",
+			entry: V001Entry{
+				IntotoObj: models.IntotoV001Schema{
+					Content: &models.IntotoV001SchemaContent{
+						Envelope: "envelope",
+					},
+				},
+				keyObj: keyObj,
+				env:    env,
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "empty parsed DSSE envelope",
+			entry: V001Entry{
+				IntotoObj: models.IntotoV001Schema{
+					Content: &models.IntotoV001SchemaContent{
+						Envelope: "envelope",
+					},
+					PublicKey: p(pub),
+				},
+				keyObj: keyObj,
+				env:    dsse.Envelope{},
+			},
+			expectSuccess: false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			if ok, err := tc.entry.Insertable(); ok != tc.expectSuccess {
+				t.Errorf("unexpected result calling Insertable: %v", err)
+			}
+		})
+	}
+}
diff --git a/pkg/types/intoto/v0.0.1/fuzz_test.go b/pkg/types/intoto/v0.0.1/fuzz_test.go
index eecbfe0..a023a4d 100644
--- a/pkg/types/intoto/v0.0.1/fuzz_test.go
+++ b/pkg/types/intoto/v0.0.1/fuzz_test.go
@@ -21,8 +21,11 @@ import (
 	"testing"
 
 	fuzz "github.com/AdamKorcz/go-fuzz-headers-1"
+	"github.com/go-openapi/swag"
 
 	fuzzUtils "github.com/sigstore/rekor/pkg/fuzz"
+	"github.com/sigstore/rekor/pkg/generated/models"
+	"github.com/sigstore/rekor/pkg/types"
 	"github.com/sigstore/rekor/pkg/types/intoto"
 )
 
@@ -47,10 +50,47 @@ func FuzzIntotoCreateProposedEntry(f *testing.F) {
 		if err != nil {
 			t.Skip()
 		}
-		c, err := it.UnmarshalEntry(entry)
+		ei, err := types.CreateVersionedEntry(entry)
 		if err != nil {
 			t.Skip()
 		}
-		_, _ = c.IndexKeys()
+
+		if ok, err := ei.Insertable(); !ok || err != nil {
+			t.Errorf("entry created via CreateProposedEntry should be insertable: %v", err)
+		}
+
+		if _, err := types.CanonicalizeEntry(context.Background(), ei); err != nil {
+			t.Errorf("valid insertable entry should be able to be canonicalized: %v", err)
+		}
+
+		_, _ = ei.IndexKeys()
+	})
+}
+
+func FuzzIntotoUnmarshalAndCanonicalize(f *testing.F) {
+	f.Fuzz(func(t *testing.T, entryData []byte) {
+		initter.Do(fuzzUtils.SetFuzzLogger)
+
+		ff := fuzz.NewConsumer(entryData)
+
+		targetV001 := &models.IntotoV001Schema{}
+
+		if err := ff.GenerateStruct(targetV001); err != nil {
+			t.Skip()
+		}
+
+		targetEntry := &models.Intoto{
+			APIVersion: swag.String(APIVERSION),
+			Spec:       targetV001,
+		}
+
+		ei, err := types.UnmarshalEntry(targetEntry)
+		if err != nil {
+			t.Skip()
+		}
+
+		if _, err := types.CanonicalizeEntry(context.Background(), ei); err != nil {
+			t.Errorf("error canonicalizing unmarshalled entry: %v", err)
+		}
 	})
 }
diff --git a/pkg/types/intoto/v0.0.2/entry_test.go b/pkg/types/intoto/v0.0.2/entry_test.go
index 556339e..9098d3a 100644
--- a/pkg/types/intoto/v0.0.2/entry_test.go
+++ b/pkg/types/intoto/v0.0.2/entry_test.go
@@ -286,6 +286,13 @@ func TestV002Entry_Unmarshal(t *testing.T) {
 				if err := v.Unmarshal(it); err != nil {
 					return err
 				}
+
+				if !tt.wantErr {
+					if ok, err := v.Insertable(); !ok || err != nil {
+						t.Errorf("unexpected error calling insertable on valid proposed entry: %v", err)
+					}
+				}
+
 				want := []string{}
 				for _, sig := range v.IntotoObj.Content.Envelope.Signatures {
 					keyHash := sha256.Sum256(sig.PublicKey)
@@ -325,6 +332,9 @@ func TestV002Entry_Unmarshal(t *testing.T) {
 				if err != nil {
 					t.Errorf("unexpected err from type-specific unmarshalling for '%v': %v", tt.name, err)
 				}
+				if ok, err := canonicalEntry.Insertable(); ok || err == nil {
+					t.Errorf("unexpected success calling Insertable on entry created from canonicalized content")
+				}
 				canonicalV002 := canonicalEntry.(*V002Entry)
 				fmt.Printf("%v", canonicalV002.IntotoObj.Content)
 				if *canonicalV002.IntotoObj.Content.Hash.Value != *tt.it.Content.Hash.Value {
@@ -489,3 +499,225 @@ func TestV002Entry_IndexKeys(t *testing.T) {
 		})
 	}
 }
+
+func TestInsertable(t *testing.T) {
+	type TestCase struct {
+		caseDesc      string
+		entry         V002Entry
+		expectSuccess bool
+	}
+
+	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	env := envelope(t, key, []byte("payload"))
+
+	testCases := []TestCase{
+		{
+			caseDesc: "valid entry",
+			entry: V002Entry{
+				IntotoObj: models.IntotoV002Schema{
+					Content: &models.IntotoV002SchemaContent{
+						Envelope: &models.IntotoV002SchemaContentEnvelope{
+							Payload:     strfmt.Base64("payload"),
+							PayloadType: swag.String("payloadType"),
+							Signatures: []*models.IntotoV002SchemaContentEnvelopeSignaturesItems0{
+								{
+									PublicKey: strfmt.Base64([]byte("key")),
+									Sig:       strfmt.Base64([]byte("sig")),
+								},
+							},
+						},
+					},
+				},
+				env: *env,
+			},
+			expectSuccess: true,
+		},
+		{
+			caseDesc: "valid entry but hasn't been parsed",
+			entry: V002Entry{
+				IntotoObj: models.IntotoV002Schema{
+					Content: &models.IntotoV002SchemaContent{
+						Envelope: &models.IntotoV002SchemaContentEnvelope{
+							Payload:     strfmt.Base64("payload"),
+							PayloadType: swag.String("payloadType"),
+							Signatures: []*models.IntotoV002SchemaContentEnvelopeSignaturesItems0{
+								{
+									PublicKey: strfmt.Base64([]byte("key")),
+									Sig:       strfmt.Base64([]byte("sig")),
+								},
+							},
+						},
+					},
+				},
+				env: dsse.Envelope{},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing sig",
+			entry: V002Entry{
+				IntotoObj: models.IntotoV002Schema{
+					Content: &models.IntotoV002SchemaContent{
+						Envelope: &models.IntotoV002SchemaContentEnvelope{
+							Payload:     strfmt.Base64("payload"),
+							PayloadType: swag.String("payloadType"),
+							Signatures: []*models.IntotoV002SchemaContentEnvelopeSignaturesItems0{
+								{
+									PublicKey: strfmt.Base64([]byte("key")),
+									//Sig:       strfmt.Base64([]byte("sig")),
+								},
+							},
+						},
+					},
+				},
+				env: *env,
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing key",
+			entry: V002Entry{
+				IntotoObj: models.IntotoV002Schema{
+					Content: &models.IntotoV002SchemaContent{
+						Envelope: &models.IntotoV002SchemaContentEnvelope{
+							Payload:     strfmt.Base64("payload"),
+							PayloadType: swag.String("payloadType"),
+							Signatures: []*models.IntotoV002SchemaContentEnvelopeSignaturesItems0{
+								{
+									//PublicKey: strfmt.Base64([]byte("key")),
+									Sig: strfmt.Base64([]byte("sig")),
+								},
+							},
+						},
+					},
+				},
+				env: *env,
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "empty signatures",
+			entry: V002Entry{
+				IntotoObj: models.IntotoV002Schema{
+					Content: &models.IntotoV002SchemaContent{
+						Envelope: &models.IntotoV002SchemaContentEnvelope{
+							Payload:     strfmt.Base64("payload"),
+							PayloadType: swag.String("payloadType"),
+							Signatures:  []*models.IntotoV002SchemaContentEnvelopeSignaturesItems0{},
+							/*
+								Signatures: []*models.IntotoV002SchemaContentEnvelopeSignaturesItems0{
+									{
+										PublicKey: strfmt.Base64([]byte("key")),
+										Sig:       strfmt.Base64([]byte("sig")),
+									},
+								},
+							*/
+						},
+					},
+				},
+				env: *env,
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing payloadType",
+			entry: V002Entry{
+				IntotoObj: models.IntotoV002Schema{
+					Content: &models.IntotoV002SchemaContent{
+						Envelope: &models.IntotoV002SchemaContentEnvelope{
+							Payload: strfmt.Base64("payload"),
+							//PayloadType: swag.String("payloadType"),
+							Signatures: []*models.IntotoV002SchemaContentEnvelopeSignaturesItems0{
+								{
+									PublicKey: strfmt.Base64([]byte("key")),
+									Sig:       strfmt.Base64([]byte("sig")),
+								},
+							},
+						},
+					},
+				},
+				env: *env,
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing payload",
+			entry: V002Entry{
+				IntotoObj: models.IntotoV002Schema{
+					Content: &models.IntotoV002SchemaContent{
+						Envelope: &models.IntotoV002SchemaContentEnvelope{
+							//Payload:     strfmt.Base64("payload"),
+							PayloadType: swag.String("payloadType"),
+							Signatures: []*models.IntotoV002SchemaContentEnvelopeSignaturesItems0{
+								{
+									PublicKey: strfmt.Base64([]byte("key")),
+									Sig:       strfmt.Base64([]byte("sig")),
+								},
+							},
+						},
+					},
+				},
+				env: *env,
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing envelope",
+			entry: V002Entry{
+				IntotoObj: models.IntotoV002Schema{
+					Content: &models.IntotoV002SchemaContent{
+						/*
+							Envelope: &models.IntotoV002SchemaContentEnvelope{
+								Payload:     strfmt.Base64("payload"),
+								PayloadType: swag.String("payloadType"),
+								Signatures: []*models.IntotoV002SchemaContentEnvelopeSignaturesItems0{
+									{
+										PublicKey: strfmt.Base64([]byte("key")),
+										Sig:       strfmt.Base64([]byte("sig")),
+									},
+								},
+							},
+						*/
+					},
+				},
+				env: *env,
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing content",
+			entry: V002Entry{
+				IntotoObj: models.IntotoV002Schema{
+					/*
+						Content: &models.IntotoV002SchemaContent{
+							Envelope: &models.IntotoV002SchemaContentEnvelope{
+								Payload:     strfmt.Base64("payload"),
+								PayloadType: swag.String("payloadType"),
+								Signatures: []*models.IntotoV002SchemaContentEnvelopeSignaturesItems0{
+									{
+										PublicKey: strfmt.Base64([]byte("key")),
+										Sig:       strfmt.Base64([]byte("sig")),
+									},
+								},
+							},
+						},
+					*/
+				},
+				env: *env,
+			},
+			expectSuccess: false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			if ok, err := tc.entry.Insertable(); ok != tc.expectSuccess {
+				t.Errorf("unexpected result calling Insertable: %v", err)
+			}
+		})
+	}
+}
diff --git a/pkg/types/intoto/v0.0.2/fuzz_test.go b/pkg/types/intoto/v0.0.2/fuzz_test.go
index 7ef866a..ab9b232 100644
--- a/pkg/types/intoto/v0.0.2/fuzz_test.go
+++ b/pkg/types/intoto/v0.0.2/fuzz_test.go
@@ -21,8 +21,11 @@ import (
 	"testing"
 
 	fuzz "github.com/AdamKorcz/go-fuzz-headers-1"
+	"github.com/go-openapi/swag"
 
 	fuzzUtils "github.com/sigstore/rekor/pkg/fuzz"
+	"github.com/sigstore/rekor/pkg/generated/models"
+	"github.com/sigstore/rekor/pkg/types"
 	"github.com/sigstore/rekor/pkg/types/intoto"
 )
 
@@ -47,10 +50,47 @@ func FuzzIntotoCreateProposedEntry(f *testing.F) {
 		if err != nil {
 			t.Skip()
 		}
-		c, err := it.UnmarshalEntry(entry)
+		ei, err := types.CreateVersionedEntry(entry)
 		if err != nil {
 			t.Skip()
 		}
-		_, _ = c.IndexKeys()
+
+		if ok, err := ei.Insertable(); !ok || err != nil {
+			t.Errorf("entry created via CreateProposedEntry should be insertable: %v", err)
+		}
+
+		if _, err := types.CanonicalizeEntry(context.Background(), ei); err != nil {
+			t.Errorf("valid insertable entry should be able to be canonicalized: %v", err)
+		}
+
+		_, _ = ei.IndexKeys()
+	})
+}
+
+func FuzzIntotoUnmarshalAndCanonicalize(f *testing.F) {
+	f.Fuzz(func(t *testing.T, entryData []byte) {
+		initter.Do(fuzzUtils.SetFuzzLogger)
+
+		ff := fuzz.NewConsumer(entryData)
+
+		targetV002 := &models.IntotoV002Schema{}
+
+		if err := ff.GenerateStruct(targetV002); err != nil {
+			t.Skip()
+		}
+
+		targetEntry := &models.Intoto{
+			APIVersion: swag.String(APIVERSION),
+			Spec:       targetV002,
+		}
+
+		ei, err := types.UnmarshalEntry(targetEntry)
+		if err != nil {
+			t.Skip()
+		}
+
+		if _, err := types.CanonicalizeEntry(context.Background(), ei); err != nil {
+			t.Errorf("error canonicalizing unmarshalled entry: %v", err)
+		}
 	})
 }
diff --git a/pkg/types/jar/jar_test.go b/pkg/types/jar/jar_test.go
index b41f6e2..4e43b74 100644
--- a/pkg/types/jar/jar_test.go
+++ b/pkg/types/jar/jar_test.go
@@ -38,7 +38,7 @@ func (u UnmarshalFailsTester) NewEntry() types.EntryImpl {
 	return &UnmarshalFailsTester{}
 }
 
-func (u UnmarshalFailsTester) Unmarshal(pe models.ProposedEntry) error {
+func (u UnmarshalFailsTester) Unmarshal(_ models.ProposedEntry) error {
 	return errors.New("error")
 }
 
diff --git a/pkg/types/jar/v0.0.1/entry_test.go b/pkg/types/jar/v0.0.1/entry_test.go
index a4b0933..bb5b54e 100644
--- a/pkg/types/jar/v0.0.1/entry_test.go
+++ b/pkg/types/jar/v0.0.1/entry_test.go
@@ -116,6 +116,10 @@ Hr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==
 			continue
 		}
 
+		if ok, err := v.Insertable(); !ok || err != nil {
+			t.Errorf("unexpected error calling Insertable on valid proposed entry: %v", err)
+		}
+
 		b, err := v.Canonicalize(context.TODO())
 		if (err == nil) != tc.expectCanonicalizeSuccess {
 			t.Errorf("unexpected result from Canonicalize for '%v': %v", tc.caseDesc, err)
@@ -129,9 +133,13 @@ Hr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==
 			if err != nil {
 				t.Errorf("unexpected err from Unmarshalling canonicalized entry for '%v': %v", tc.caseDesc, err)
 			}
-			if _, err := types.UnmarshalEntry(pe); err != nil {
+			ei, err := types.UnmarshalEntry(pe)
+			if err != nil {
 				t.Errorf("unexpected err from type-specific unmarshalling for '%v': %v", tc.caseDesc, err)
 			}
+			if ok, err := ei.Insertable(); ok || err == nil {
+				t.Errorf("unexpected err from calling Insertable on entry created from canonicalized content")
+			}
 		}
 
 		verifier, err := v.Verifier()
@@ -184,3 +192,56 @@ func TestJarMetadataSize(t *testing.T) {
 		t.Fatalf("unexpected error %v", err)
 	}
 }
+
+func TestInsertable(t *testing.T) {
+	type TestCase struct {
+		caseDesc      string
+		entry         V001Entry
+		expectSuccess bool
+	}
+	testCases := []TestCase{
+		{
+			caseDesc: "valid entry",
+			entry: V001Entry{
+				JARModel: models.JarV001Schema{
+					Archive: &models.JarV001SchemaArchive{
+						Content: strfmt.Base64([]byte("content")),
+					},
+				},
+			},
+			expectSuccess: true,
+		},
+		{
+			caseDesc: "missing archive content",
+			entry: V001Entry{
+				JARModel: models.JarV001Schema{
+					Archive: &models.JarV001SchemaArchive{
+						//Content: strfmt.Base64([]byte("content")),
+					},
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing archive obj",
+			entry: V001Entry{
+				JARModel: models.JarV001Schema{
+					/*
+						Archive: &models.JarV001SchemaArchive{
+							Content: strfmt.Base64([]byte("content")),
+						},
+					*/
+				},
+			},
+			expectSuccess: false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			if ok, err := tc.entry.Insertable(); ok != tc.expectSuccess {
+				t.Errorf("unexpected result calling Insertable: %v", err)
+			}
+		})
+	}
+}
diff --git a/pkg/types/jar/v0.0.1/fuzz_test.go b/pkg/types/jar/v0.0.1/fuzz_test.go
index d9b4108..70f420f 100644
--- a/pkg/types/jar/v0.0.1/fuzz_test.go
+++ b/pkg/types/jar/v0.0.1/fuzz_test.go
@@ -23,10 +23,13 @@ import (
 	"testing"
 
 	fuzz "github.com/AdamKorcz/go-fuzz-headers-1"
+	"github.com/go-openapi/swag"
 
 	jarutils "github.com/sassoftware/relic/lib/signjar"
 
 	fuzzUtils "github.com/sigstore/rekor/pkg/fuzz"
+	"github.com/sigstore/rekor/pkg/generated/models"
+	"github.com/sigstore/rekor/pkg/types"
 	"github.com/sigstore/rekor/pkg/types/jar"
 )
 
@@ -51,11 +54,48 @@ func FuzzJarCreateProposedEntry(f *testing.F) {
 		if err != nil {
 			t.Skip()
 		}
-		c, err := it.UnmarshalEntry(entry)
+		ei, err := types.CreateVersionedEntry(entry)
 		if err != nil {
 			t.Skip()
 		}
-		_, _ = c.IndexKeys()
+
+		if ok, err := ei.Insertable(); !ok || err != nil {
+			t.Errorf("entry created via CreateProposedEntry should be insertable: %v", err)
+		}
+
+		if _, err := types.CanonicalizeEntry(context.Background(), ei); err != nil {
+			t.Errorf("valid insertable entry should be able to be canonicalized: %v", err)
+		}
+
+		_, _ = ei.IndexKeys()
+	})
+}
+
+func FuzzJarUnmarshalAndCanonicalize(f *testing.F) {
+	f.Fuzz(func(t *testing.T, entryData []byte) {
+		initter.Do(fuzzUtils.SetFuzzLogger)
+
+		ff := fuzz.NewConsumer(entryData)
+
+		targetV001 := &models.JarV001Schema{}
+
+		if err := ff.GenerateStruct(targetV001); err != nil {
+			t.Skip()
+		}
+
+		targetEntry := &models.Jar{
+			APIVersion: swag.String(APIVERSION),
+			Spec:       targetV001,
+		}
+
+		ei, err := types.UnmarshalEntry(targetEntry)
+		if err != nil {
+			t.Skip()
+		}
+
+		if _, err := types.CanonicalizeEntry(context.Background(), ei); err != nil {
+			t.Errorf("error canonicalizing unmarshalled entry: %v", err)
+		}
 	})
 }
 
diff --git a/pkg/types/rekord/rekord_test.go b/pkg/types/rekord/rekord_test.go
index dace7f1..7d16941 100644
--- a/pkg/types/rekord/rekord_test.go
+++ b/pkg/types/rekord/rekord_test.go
@@ -39,7 +39,7 @@ func (u UnmarshalFailsTester) NewEntry() types.EntryImpl {
 	return &UnmarshalFailsTester{}
 }
 
-func (u UnmarshalFailsTester) Unmarshal(pe models.ProposedEntry) error {
+func (u UnmarshalFailsTester) Unmarshal(_ models.ProposedEntry) error {
 	return errors.New("error")
 }
 
diff --git a/pkg/types/rekord/v0.0.1/entry_test.go b/pkg/types/rekord/v0.0.1/entry_test.go
index 55e1f9b..ecca289 100644
--- a/pkg/types/rekord/v0.0.1/entry_test.go
+++ b/pkg/types/rekord/v0.0.1/entry_test.go
@@ -232,6 +232,10 @@ func TestCrossFieldValidation(t *testing.T) {
 			continue
 		}
 
+		if ok, err := v.Insertable(); !ok || err != nil {
+			t.Errorf("unexpected error calling Insertable on valid proposed entry: %v", err)
+		}
+
 		b, err := v.Canonicalize(context.TODO())
 		if (err == nil) != tc.expectCanonicalizeSuccess {
 			t.Errorf("unexpected result from Canonicalize for '%v': %v", tc.caseDesc, err)
@@ -245,9 +249,13 @@ func TestCrossFieldValidation(t *testing.T) {
 			if err != nil {
 				t.Errorf("unexpected err from Unmarshalling canonicalized entry for '%v': %v", tc.caseDesc, err)
 			}
-			if _, err := types.UnmarshalEntry(pe); err != nil {
+			ei, err := types.UnmarshalEntry(pe)
+			if err != nil {
 				t.Errorf("unexpected err from type-specific unmarshalling for '%v': %v", tc.caseDesc, err)
 			}
+			if ok, err := ei.Insertable(); ok || err == nil {
+				t.Errorf("unexpected success calling Insertable on entry created from canonicalized content")
+			}
 		}
 
 		verifier, err := v.Verifier()
@@ -288,3 +296,194 @@ func TestUnspecifiedPKIFormat(t *testing.T) {
 		t.Errorf("invalid pki format should not create a valid entry")
 	}
 }
+
+func TestInsertable(t *testing.T) {
+	type TestCase struct {
+		caseDesc      string
+		entry         V001Entry
+		expectSuccess bool
+	}
+
+	sig := strfmt.Base64([]byte("sig"))
+	pub := strfmt.Base64([]byte("pub"))
+	testCases := []TestCase{
+		{
+			caseDesc: "valid entry",
+			entry: V001Entry{
+				RekordObj: models.RekordV001Schema{
+					Data: &models.RekordV001SchemaData{
+						Content: strfmt.Base64([]byte("content")),
+					},
+					Signature: &models.RekordV001SchemaSignature{
+						Content: &sig,
+						Format:  swag.String("format"),
+						PublicKey: &models.RekordV001SchemaSignaturePublicKey{
+							Content: &pub,
+						},
+					},
+				},
+			},
+			expectSuccess: true,
+		},
+		{
+			caseDesc: "missing public key content",
+			entry: V001Entry{
+				RekordObj: models.RekordV001Schema{
+					Data: &models.RekordV001SchemaData{
+						Content: strfmt.Base64([]byte("content")),
+					},
+					Signature: &models.RekordV001SchemaSignature{
+						Content:   &sig,
+						Format:    swag.String("format"),
+						PublicKey: &models.RekordV001SchemaSignaturePublicKey{
+							//Content: &pub,
+						},
+					},
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing public key obj",
+			entry: V001Entry{
+				RekordObj: models.RekordV001Schema{
+					Data: &models.RekordV001SchemaData{
+						Content: strfmt.Base64([]byte("content")),
+					},
+					Signature: &models.RekordV001SchemaSignature{
+						Content: &sig,
+						Format:  swag.String("format"),
+						/*
+							PublicKey: &models.RekordV001SchemaSignaturePublicKey{
+								Content: &pub,
+							},
+						*/
+					},
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing format string",
+			entry: V001Entry{
+				RekordObj: models.RekordV001Schema{
+					Data: &models.RekordV001SchemaData{
+						Content: strfmt.Base64([]byte("content")),
+					},
+					Signature: &models.RekordV001SchemaSignature{
+						Content: &sig,
+						//Format:  swag.String("format"),
+						PublicKey: &models.RekordV001SchemaSignaturePublicKey{
+							Content: &pub,
+						},
+					},
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing signature content",
+			entry: V001Entry{
+				RekordObj: models.RekordV001Schema{
+					Data: &models.RekordV001SchemaData{
+						Content: strfmt.Base64([]byte("content")),
+					},
+					Signature: &models.RekordV001SchemaSignature{
+						//Content: &sig,
+						Format: swag.String("format"),
+						PublicKey: &models.RekordV001SchemaSignaturePublicKey{
+							Content: &pub,
+						},
+					},
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing signature obj",
+			entry: V001Entry{
+				RekordObj: models.RekordV001Schema{
+					Data: &models.RekordV001SchemaData{
+						Content: strfmt.Base64([]byte("content")),
+					},
+					/*
+						Signature: &models.RekordV001SchemaSignature{
+							Content: &sig,
+							Format:  swag.String("format"),
+							PublicKey: &models.RekordV001SchemaSignaturePublicKey{
+								Content: &pub,
+							},
+						},
+					*/
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing data content",
+			entry: V001Entry{
+				RekordObj: models.RekordV001Schema{
+					Data: &models.RekordV001SchemaData{
+						//Content: strfmt.Base64([]byte("content")),
+					},
+					Signature: &models.RekordV001SchemaSignature{
+						Content: &sig,
+						Format:  swag.String("format"),
+						PublicKey: &models.RekordV001SchemaSignaturePublicKey{
+							Content: &pub,
+						},
+					},
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing data obj",
+			entry: V001Entry{
+				RekordObj: models.RekordV001Schema{
+					/*
+						Data: &models.RekordV001SchemaData{
+							Content: strfmt.Base64([]byte("content")),
+						},
+					*/
+					Signature: &models.RekordV001SchemaSignature{
+						Content: &sig,
+						Format:  swag.String("format"),
+						PublicKey: &models.RekordV001SchemaSignaturePublicKey{
+							Content: &pub,
+						},
+					},
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "empty obj",
+			entry: V001Entry{
+				RekordObj: models.RekordV001Schema{
+					/*
+						Data: &models.RekordV001SchemaData{
+							Content: strfmt.Base64([]byte("content")),
+						},
+						Signature: &models.RekordV001SchemaSignature{
+							Content: &sig,
+							Format:  swag.String("format"),
+							PublicKey: &models.RekordV001SchemaSignaturePublicKey{
+								Content: &pub,
+							},
+						},
+					*/
+				},
+			},
+			expectSuccess: false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			if ok, err := tc.entry.Insertable(); ok != tc.expectSuccess {
+				t.Errorf("unexpected result calling Insertable: %v", err)
+			}
+		})
+	}
+}
diff --git a/pkg/types/rekord/v0.0.1/fuzz_test.go b/pkg/types/rekord/v0.0.1/fuzz_test.go
index d669f70..a4e9ef3 100644
--- a/pkg/types/rekord/v0.0.1/fuzz_test.go
+++ b/pkg/types/rekord/v0.0.1/fuzz_test.go
@@ -21,8 +21,11 @@ import (
 	"testing"
 
 	fuzz "github.com/AdamKorcz/go-fuzz-headers-1"
+	"github.com/go-openapi/swag"
 
 	fuzzUtils "github.com/sigstore/rekor/pkg/fuzz"
+	"github.com/sigstore/rekor/pkg/generated/models"
+	"github.com/sigstore/rekor/pkg/types"
 	"github.com/sigstore/rekor/pkg/types/rekord"
 )
 
@@ -47,10 +50,47 @@ func FuzzRekordCreateProposedEntry(f *testing.F) {
 		if err != nil {
 			t.Skip()
 		}
-		c, err := it.UnmarshalEntry(entry)
+		ei, err := types.CreateVersionedEntry(entry)
 		if err != nil {
 			t.Skip()
 		}
-		_, _ = c.IndexKeys()
+
+		if ok, err := ei.Insertable(); !ok || err != nil {
+			t.Errorf("entry created via CreateProposedEntry should be insertable: %v", err)
+		}
+
+		if _, err := types.CanonicalizeEntry(context.Background(), ei); err != nil {
+			t.Errorf("valid insertable entry should be able to be canonicalized: %v", err)
+		}
+
+		_, _ = ei.IndexKeys()
+	})
+}
+
+func FuzzRekordUnmarshalAndCanonicalize(f *testing.F) {
+	f.Fuzz(func(t *testing.T, entryData []byte) {
+		initter.Do(fuzzUtils.SetFuzzLogger)
+
+		ff := fuzz.NewConsumer(entryData)
+
+		targetV001 := &models.RekordV001Schema{}
+
+		if err := ff.GenerateStruct(targetV001); err != nil {
+			t.Skip()
+		}
+
+		targetEntry := &models.Rekord{
+			APIVersion: swag.String(APIVERSION),
+			Spec:       targetV001,
+		}
+
+		ei, err := types.UnmarshalEntry(targetEntry)
+		if err != nil {
+			t.Skip()
+		}
+
+		if _, err := types.CanonicalizeEntry(context.Background(), ei); err != nil {
+			t.Errorf("error canonicalizing unmarshalled entry: %v", err)
+		}
 	})
 }
diff --git a/pkg/types/rfc3161/rfc3161_test.go b/pkg/types/rfc3161/rfc3161_test.go
index 36a146d..f9a6b3f 100644
--- a/pkg/types/rfc3161/rfc3161_test.go
+++ b/pkg/types/rfc3161/rfc3161_test.go
@@ -39,7 +39,7 @@ func (u UnmarshalFailsTester) NewEntry() types.EntryImpl {
 	return &UnmarshalFailsTester{}
 }
 
-func (u UnmarshalFailsTester) Unmarshal(pe models.ProposedEntry) error {
+func (u UnmarshalFailsTester) Unmarshal(_ models.ProposedEntry) error {
 	return errors.New("error")
 }
 
diff --git a/pkg/types/rfc3161/v0.0.1/entry_test.go b/pkg/types/rfc3161/v0.0.1/entry_test.go
index 63b7a9b..6c54574 100644
--- a/pkg/types/rfc3161/v0.0.1/entry_test.go
+++ b/pkg/types/rfc3161/v0.0.1/entry_test.go
@@ -170,6 +170,12 @@ func TestCrossFieldValidation(t *testing.T) {
 			}
 		}
 
+		if tc.expectUnmarshalSuccess {
+			if ok, err := v.Insertable(); !ok || err != nil {
+				t.Errorf("unexpected error calling Insertable on valid proposed entry: %v", err)
+			}
+		}
+
 		b, err := v.Canonicalize(context.TODO())
 		if (err == nil) != tc.expectCanonicalizeSuccess {
 			t.Errorf("unexpected result from Canonicalize for '%v': %v", tc.caseDesc, err)
@@ -183,9 +189,14 @@ func TestCrossFieldValidation(t *testing.T) {
 			if err != nil {
 				t.Errorf("unexpected err from Unmarshalling canonicalized entry for '%v': %v", tc.caseDesc, err)
 			}
-			if _, err := types.UnmarshalEntry(pe); err != nil {
+			ei, err := types.UnmarshalEntry(pe)
+			if err != nil {
 				t.Errorf("unexpected err from type-specific unmarshalling for '%v': %v", tc.caseDesc, err)
 			}
+			// rfc3161 is one of two types that Insertable() should return true from canonicalized entries
+			if ok, err := ei.Insertable(); !ok || err != nil {
+				t.Errorf("unexpected error calling insertable on entry created from canonicalized content")
+			}
 		}
 	}
 }
@@ -244,3 +255,93 @@ func p(b []byte) *strfmt.Base64 {
 	b64 := strfmt.Base64(b)
 	return &b64
 }
+
+func TestInsertable(t *testing.T) {
+	type TestCase struct {
+		caseDesc      string
+		entry         V001Entry
+		expectSuccess bool
+	}
+
+	tsr := strfmt.Base64([]byte("tsr"))
+
+	testCases := []TestCase{
+		{
+			caseDesc: "valid entry",
+			entry: V001Entry{
+				Rfc3161Obj: models.Rfc3161V001Schema{
+					Tsr: &models.Rfc3161V001SchemaTsr{
+						Content: &tsr,
+					},
+				},
+				tsrContent: &tsr,
+			},
+			expectSuccess: true,
+		},
+		{
+			caseDesc: "unparsed tsr",
+			entry: V001Entry{
+				Rfc3161Obj: models.Rfc3161V001Schema{
+					Tsr: &models.Rfc3161V001SchemaTsr{
+						Content: &tsr,
+					},
+				},
+				//tsrContent: &tsr,
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing tsr content",
+			entry: V001Entry{
+				Rfc3161Obj: models.Rfc3161V001Schema{
+					Tsr: &models.Rfc3161V001SchemaTsr{
+						//Content: &tsr,
+					},
+				},
+				tsrContent: &tsr,
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing tsr obj",
+			entry: V001Entry{
+				Rfc3161Obj: models.Rfc3161V001Schema{
+					/*
+						Tsr: &models.Rfc3161V001SchemaTsr{
+							Content: &tsr,
+						},
+					*/
+				},
+				tsrContent: &tsr,
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing Rfc3161 obj",
+			entry: V001Entry{
+				/*
+					Rfc3161Obj: models.Rfc3161V001Schema{
+						Tsr: &models.Rfc3161V001SchemaTsr{
+							Content: &tsr,
+						},
+					},
+				*/
+				tsrContent: &tsr,
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc:      "empty obj",
+			entry:         V001Entry{},
+			expectSuccess: false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			if ok, err := tc.entry.Insertable(); ok != tc.expectSuccess {
+				t.Errorf("unexpected result calling Insertable: %v", err)
+			}
+		})
+	}
+}
diff --git a/pkg/types/rfc3161/v0.0.1/fuzz_test.go b/pkg/types/rfc3161/v0.0.1/fuzz_test.go
index 15e0f41..65f5c7a 100644
--- a/pkg/types/rfc3161/v0.0.1/fuzz_test.go
+++ b/pkg/types/rfc3161/v0.0.1/fuzz_test.go
@@ -21,8 +21,11 @@ import (
 	"testing"
 
 	fuzz "github.com/AdamKorcz/go-fuzz-headers-1"
+	"github.com/go-openapi/swag"
 
 	fuzzUtils "github.com/sigstore/rekor/pkg/fuzz"
+	"github.com/sigstore/rekor/pkg/generated/models"
+	"github.com/sigstore/rekor/pkg/types"
 	"github.com/sigstore/rekor/pkg/types/rfc3161"
 )
 
@@ -47,10 +50,47 @@ func FuzzRfc3161CreateProposedEntry(f *testing.F) {
 		if err != nil {
 			t.Skip()
 		}
-		c, err := it.UnmarshalEntry(entry)
+		ei, err := types.CreateVersionedEntry(entry)
 		if err != nil {
 			t.Skip()
 		}
-		_, _ = c.IndexKeys()
+
+		if ok, err := ei.Insertable(); !ok || err != nil {
+			t.Errorf("entry created via CreateProposedEntry should be insertable: %v", err)
+		}
+
+		if _, err := types.CanonicalizeEntry(context.Background(), ei); err != nil {
+			t.Errorf("valid insertable entry should be able to be canonicalized: %v", err)
+		}
+
+		_, _ = ei.IndexKeys()
+	})
+}
+
+func FuzzRfc3161UnmarshalAndCanonicalize(f *testing.F) {
+	f.Fuzz(func(t *testing.T, entryData []byte) {
+		initter.Do(fuzzUtils.SetFuzzLogger)
+
+		ff := fuzz.NewConsumer(entryData)
+
+		targetV001 := &models.Rfc3161V001Schema{}
+
+		if err := ff.GenerateStruct(targetV001); err != nil {
+			t.Skip()
+		}
+
+		targetEntry := &models.Rfc3161{
+			APIVersion: swag.String(APIVERSION),
+			Spec:       targetV001,
+		}
+
+		ei, err := types.UnmarshalEntry(targetEntry)
+		if err != nil {
+			t.Skip()
+		}
+
+		if _, err := types.CanonicalizeEntry(context.Background(), ei); err != nil {
+			t.Errorf("error canonicalizing unmarshalled entry: %v", err)
+		}
 	})
 }
diff --git a/pkg/types/rpm/rpm_test.go b/pkg/types/rpm/rpm_test.go
index 747cc9a..d365238 100644
--- a/pkg/types/rpm/rpm_test.go
+++ b/pkg/types/rpm/rpm_test.go
@@ -39,7 +39,7 @@ func (u UnmarshalFailsTester) NewEntry() types.EntryImpl {
 	return &UnmarshalFailsTester{}
 }
 
-func (u UnmarshalFailsTester) Unmarshal(pe models.ProposedEntry) error {
+func (u UnmarshalFailsTester) Unmarshal(_ models.ProposedEntry) error {
 	return errors.New("error")
 }
 
diff --git a/pkg/types/rpm/v0.0.1/entry_test.go b/pkg/types/rpm/v0.0.1/entry_test.go
index a994f18..941f852 100644
--- a/pkg/types/rpm/v0.0.1/entry_test.go
+++ b/pkg/types/rpm/v0.0.1/entry_test.go
@@ -167,6 +167,12 @@ func TestCrossFieldValidation(t *testing.T) {
 			t.Errorf("unexpected result in '%v': %v", tc.caseDesc, err)
 		}
 
+		if tc.expectUnmarshalSuccess {
+			if ok, err := v.Insertable(); !ok || err != nil {
+				t.Errorf("unexpected error calling Insertable on valid proposed entry: %v", err)
+			}
+		}
+
 		b, err := v.Canonicalize(context.TODO())
 		if (err == nil) != tc.expectCanonicalizeSuccess {
 			t.Errorf("unexpected result from Canonicalize for '%v': %v", tc.caseDesc, err)
@@ -181,9 +187,13 @@ func TestCrossFieldValidation(t *testing.T) {
 			if err != nil {
 				t.Errorf("unexpected err from Unmarshalling canonicalized entry for '%v': %v", tc.caseDesc, err)
 			}
-			if _, err := types.UnmarshalEntry(pe); err != nil {
+			ei, err := types.UnmarshalEntry(pe)
+			if err != nil {
 				t.Errorf("unexpected err from type-specific unmarshalling for '%v': %v", tc.caseDesc, err)
 			}
+			if ok, err := ei.Insertable(); ok || err == nil {
+				t.Errorf("unexpected success calling Insertable on entry created from canonicalized content")
+			}
 		}
 
 		verifier, err := v.Verifier()
@@ -205,3 +215,103 @@ func TestCrossFieldValidation(t *testing.T) {
 		}
 	}
 }
+
+func TestInsertable(t *testing.T) {
+	type TestCase struct {
+		caseDesc      string
+		entry         V001Entry
+		expectSuccess bool
+	}
+
+	pub := strfmt.Base64([]byte("pub"))
+
+	testCases := []TestCase{
+		{
+			caseDesc: "valid entry",
+			entry: V001Entry{
+				RPMModel: models.RpmV001Schema{
+					Package: &models.RpmV001SchemaPackage{
+						Content: strfmt.Base64([]byte("content")),
+					},
+					PublicKey: &models.RpmV001SchemaPublicKey{
+						Content: &pub,
+					},
+				},
+			},
+			expectSuccess: true,
+		},
+		{
+			caseDesc: "missing public key content",
+			entry: V001Entry{
+				RPMModel: models.RpmV001Schema{
+					Package: &models.RpmV001SchemaPackage{
+						Content: strfmt.Base64([]byte("content")),
+					},
+					PublicKey: &models.RpmV001SchemaPublicKey{
+						//Content: &pub,
+					},
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing public key obj",
+			entry: V001Entry{
+				RPMModel: models.RpmV001Schema{
+					Package: &models.RpmV001SchemaPackage{
+						Content: strfmt.Base64([]byte("content")),
+					},
+					/*
+						PublicKey: &models.RpmV001SchemaPublicKey{
+							Content: &pub,
+						},
+					*/
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing package content",
+			entry: V001Entry{
+				RPMModel: models.RpmV001Schema{
+					Package: &models.RpmV001SchemaPackage{
+						//Content: strfmt.Base64([]byte("content")),
+					},
+					PublicKey: &models.RpmV001SchemaPublicKey{
+						Content: &pub,
+					},
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing package obj",
+			entry: V001Entry{
+				RPMModel: models.RpmV001Schema{
+					/*
+						Package: &models.RpmV001SchemaPackage{
+							Content: strfmt.Base64([]byte("content")),
+						},
+					*/
+					PublicKey: &models.RpmV001SchemaPublicKey{
+						Content: &pub,
+					},
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc:      "empty obj",
+			entry:         V001Entry{},
+			expectSuccess: false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			if ok, err := tc.entry.Insertable(); ok != tc.expectSuccess {
+				t.Errorf("unexpected result calling Insertable: %v", err)
+			}
+		})
+	}
+}
diff --git a/pkg/types/rpm/v0.0.1/fuzz_test.go b/pkg/types/rpm/v0.0.1/fuzz_test.go
index 0f0de91..33686b8 100644
--- a/pkg/types/rpm/v0.0.1/fuzz_test.go
+++ b/pkg/types/rpm/v0.0.1/fuzz_test.go
@@ -21,8 +21,11 @@ import (
 	"testing"
 
 	fuzz "github.com/AdamKorcz/go-fuzz-headers-1"
+	"github.com/go-openapi/swag"
 
 	fuzzUtils "github.com/sigstore/rekor/pkg/fuzz"
+	"github.com/sigstore/rekor/pkg/generated/models"
+	"github.com/sigstore/rekor/pkg/types"
 	"github.com/sigstore/rekor/pkg/types/rpm"
 )
 
@@ -47,10 +50,47 @@ func FuzzRpmCreateProposedEntry(f *testing.F) {
 		if err != nil {
 			t.Skip()
 		}
-		c, err := it.UnmarshalEntry(entry)
+		ei, err := types.CreateVersionedEntry(entry)
 		if err != nil {
 			t.Skip()
 		}
-		_, _ = c.IndexKeys()
+
+		if ok, err := ei.Insertable(); !ok || err != nil {
+			t.Errorf("entry created via CreateProposedEntry should be insertable: %v", err)
+		}
+
+		if _, err := types.CanonicalizeEntry(context.Background(), ei); err != nil {
+			t.Errorf("valid insertable entry should be able to be canonicalized: %v", err)
+		}
+
+		_, _ = ei.IndexKeys()
+	})
+}
+
+func FuzzRpmUnmarshalAndCanonicalize(f *testing.F) {
+	f.Fuzz(func(t *testing.T, entryData []byte) {
+		initter.Do(fuzzUtils.SetFuzzLogger)
+
+		ff := fuzz.NewConsumer(entryData)
+
+		targetV001 := &models.RpmV001Schema{}
+
+		if err := ff.GenerateStruct(targetV001); err != nil {
+			t.Skip()
+		}
+
+		targetEntry := &models.Rpm{
+			APIVersion: swag.String(APIVERSION),
+			Spec:       targetV001,
+		}
+
+		ei, err := types.UnmarshalEntry(targetEntry)
+		if err != nil {
+			t.Skip()
+		}
+
+		if _, err := types.CanonicalizeEntry(context.Background(), ei); err != nil {
+			t.Errorf("error canonicalizing unmarshalled entry: %v", err)
+		}
 	})
 }
diff --git a/pkg/types/test_util.go b/pkg/types/test_util.go
index e65f574..03b5b9c 100644
--- a/pkg/types/test_util.go
+++ b/pkg/types/test_util.go
@@ -43,11 +43,11 @@ func (u BaseUnmarshalTester) IndexKeys() ([]string, error) {
 	return []string{}, nil
 }
 
-func (u BaseUnmarshalTester) Canonicalize(ctx context.Context) ([]byte, error) {
+func (u BaseUnmarshalTester) Canonicalize(_ context.Context) ([]byte, error) {
 	return nil, nil
 }
 
-func (u BaseUnmarshalTester) Unmarshal(pe models.ProposedEntry) error {
+func (u BaseUnmarshalTester) Unmarshal(_ models.ProposedEntry) error {
 	return nil
 }
 
@@ -67,20 +67,24 @@ func (u BaseUnmarshalTester) CreateFromArtifactProperties(_ context.Context, _ A
 	return nil, nil
 }
 
+func (u BaseUnmarshalTester) Insertable() (bool, error) {
+	return false, nil
+}
+
 type BaseProposedEntryTester struct{}
 
 func (b BaseProposedEntryTester) Kind() string {
 	return "nil"
 }
 
-func (b BaseProposedEntryTester) SetKind(v string) {
+func (b BaseProposedEntryTester) SetKind(_ string) {
 
 }
 
-func (b BaseProposedEntryTester) Validate(r strfmt.Registry) error {
+func (b BaseProposedEntryTester) Validate(_ strfmt.Registry) error {
 	return nil
 }
 
-func (b BaseProposedEntryTester) ContextValidate(ctx context.Context, r strfmt.Registry) error {
+func (b BaseProposedEntryTester) ContextValidate(_ context.Context, _ strfmt.Registry) error {
 	return nil
 }
diff --git a/pkg/types/tuf/tuf_test.go b/pkg/types/tuf/tuf_test.go
index a08cbb9..16a9fb9 100644
--- a/pkg/types/tuf/tuf_test.go
+++ b/pkg/types/tuf/tuf_test.go
@@ -43,7 +43,7 @@ func (u UnmarshalFailsTester) Verifier() (pki.PublicKey, error) {
 	return nil, nil
 }
 
-func (u UnmarshalFailsTester) Unmarshal(pe models.ProposedEntry) error {
+func (u UnmarshalFailsTester) Unmarshal(_ models.ProposedEntry) error {
 	return errors.New("error")
 }
 
diff --git a/pkg/types/tuf/v0.0.1/entry_test.go b/pkg/types/tuf/v0.0.1/entry_test.go
index 13ab2fe..4193c4e 100644
--- a/pkg/types/tuf/v0.0.1/entry_test.go
+++ b/pkg/types/tuf/v0.0.1/entry_test.go
@@ -177,68 +177,175 @@ func TestCrossFieldValidation(t *testing.T) {
 	}
 
 	for _, tc := range testCases {
-		if err := tc.entry.Validate(); (err == nil) != tc.expectUnmarshalSuccess {
-			t.Errorf("unexpected result in '%v': %v", tc.caseDesc, err)
-		}
-		// No need to continue here if we failed at unmarshal
-		if !tc.expectUnmarshalSuccess {
-			continue
-		}
-
-		v := &V001Entry{}
-		r := models.TUF{
-			APIVersion: swag.String(tc.entry.APIVersion()),
-			Spec:       tc.entry.TufObj,
-		}
-
-		unmarshalAndValidate := func() error {
-			if err := v.Unmarshal(&r); err != nil {
-				return err
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			if err := tc.entry.Validate(); (err == nil) != tc.expectUnmarshalSuccess {
+				t.Errorf("unexpected result in '%v': %v", tc.caseDesc, err)
 			}
-			return v.Validate()
-		}
-		if err := unmarshalAndValidate(); (err == nil) != tc.expectUnmarshalSuccess {
-			t.Errorf("unexpected result in '%v': %v", tc.caseDesc, err)
-		}
-
-		b, err := v.Canonicalize(context.TODO())
-		if (err == nil) != tc.expectCanonicalizeSuccess {
-			t.Errorf("unexpected result from Canonicalize for '%v': %v", tc.caseDesc, err)
-		} else if err != nil {
-			if _, ok := err.(types.ValidationError); !ok {
-				t.Errorf("canonicalize returned an unexpected error that isn't of type types.ValidationError: %v", err)
+			// No need to continue here if we failed at unmarshal
+			if !tc.expectUnmarshalSuccess {
+				return
 			}
-		}
 
-		if b != nil {
-			pe, err := models.UnmarshalProposedEntry(bytes.NewReader(b), runtime.JSONConsumer())
-			if err != nil {
-				t.Errorf("unexpected err from Unmarshalling canonicalized entry for '%v': %v", tc.caseDesc, err)
+			v := &V001Entry{}
+			r := models.TUF{
+				APIVersion: swag.String(tc.entry.APIVersion()),
+				Spec:       tc.entry.TufObj,
 			}
-			if _, err := types.UnmarshalEntry(pe); err != nil {
-				t.Errorf("unexpected err from type-specific unmarshalling for '%v': %v", tc.caseDesc, err)
+
+			unmarshalAndValidate := func() error {
+				if err := v.Unmarshal(&r); err != nil {
+					return err
+				}
+				return v.Validate()
+			}
+			if err := unmarshalAndValidate(); (err == nil) != tc.expectUnmarshalSuccess {
+				t.Errorf("unexpected result in '%v': %v", tc.caseDesc, err)
 			}
-		}
 
-		verifier, err := v.Verifier()
-		if tc.expectVerifierSuccess {
-			if err != nil {
-				t.Errorf("%v: unexpected error, got %v", tc.caseDesc, err)
-			} else {
-				pub, _ := verifier.CanonicalValue()
-				rootBytes := new(bytes.Buffer)
-				if err := json.Compact(rootBytes, keyBytes); err != nil {
-					t.Fatal(err)
+			if tc.expectUnmarshalSuccess {
+				if ok, err := v.Insertable(); !ok || err != nil {
+					t.Errorf("unexpected error calling Insertable on valid proposed entry: %v", err)
+				}
+			}
+
+			b, err := v.Canonicalize(context.TODO())
+			if (err == nil) != tc.expectCanonicalizeSuccess {
+				t.Errorf("unexpected result from Canonicalize for '%v': %v", tc.caseDesc, err)
+			} else if err != nil {
+				if _, ok := err.(types.ValidationError); !ok {
+					t.Errorf("canonicalize returned an unexpected error that isn't of type types.ValidationError: %v", err)
+				}
+			}
+
+			if b != nil {
+				pe, err := models.UnmarshalProposedEntry(bytes.NewReader(b), runtime.JSONConsumer())
+				if err != nil {
+					t.Errorf("unexpected err from Unmarshalling canonicalized entry for '%v': %v", tc.caseDesc, err)
 				}
-				if !reflect.DeepEqual(pub, rootBytes.Bytes()) {
-					t.Errorf("%v: verifier and public keys do not match: %v, %v", tc.caseDesc, string(pub), rootBytes)
+				if _, err := types.UnmarshalEntry(pe); err != nil {
+					t.Errorf("unexpected err from type-specific unmarshalling for '%v': %v", tc.caseDesc, err)
 				}
+				// Insertable on canonicalized content is variable so we skip testing it here
 			}
-		} else {
-			if err == nil {
-				s, _ := verifier.CanonicalValue()
-				t.Errorf("%v: expected error for %v, got %v", tc.caseDesc, string(s), err)
+
+			verifier, err := v.Verifier()
+			if tc.expectVerifierSuccess {
+				if err != nil {
+					t.Errorf("%v: unexpected error, got %v", tc.caseDesc, err)
+				} else {
+					pub, _ := verifier.CanonicalValue()
+					rootBytes := new(bytes.Buffer)
+					if err := json.Compact(rootBytes, keyBytes); err != nil {
+						t.Fatal(err)
+					}
+					if !reflect.DeepEqual(pub, rootBytes.Bytes()) {
+						t.Errorf("%v: verifier and public keys do not match: %v, %v", tc.caseDesc, string(pub), rootBytes)
+					}
+				}
+			} else {
+				if err == nil {
+					s, _ := verifier.CanonicalValue()
+					t.Errorf("%v: expected error for %v, got %v", tc.caseDesc, string(s), err)
+				}
+			}
+		})
+	}
+}
+
+func TestInsertable(t *testing.T) {
+	type TestCase struct {
+		caseDesc      string
+		entry         V001Entry
+		expectSuccess bool
+	}
+
+	testCases := []TestCase{
+		{
+			caseDesc: "valid entry",
+			entry: V001Entry{
+				TufObj: models.TUFV001Schema{
+					Metadata: &models.TUFV001SchemaMetadata{
+						Content: struct{}{},
+					},
+					Root: &models.TUFV001SchemaRoot{
+						Content: struct{}{},
+					},
+				},
+			},
+			expectSuccess: true,
+		},
+		{
+			caseDesc: "missing root content",
+			entry: V001Entry{
+				TufObj: models.TUFV001Schema{
+					Metadata: &models.TUFV001SchemaMetadata{
+						Content: struct{}{},
+					},
+					Root: &models.TUFV001SchemaRoot{
+						//Content: struct{}{},
+					},
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing root obj",
+			entry: V001Entry{
+				TufObj: models.TUFV001Schema{
+					Metadata: &models.TUFV001SchemaMetadata{
+						Content: struct{}{},
+					},
+					/*
+						Root: &models.TUFV001SchemaRoot{
+							Content: struct{}{},
+						},
+					*/
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing metadata content",
+			entry: V001Entry{
+				TufObj: models.TUFV001Schema{
+					Metadata: &models.TUFV001SchemaMetadata{
+						//Content: struct{}{},
+					},
+					Root: &models.TUFV001SchemaRoot{
+						Content: struct{}{},
+					},
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc: "missing metadata content",
+			entry: V001Entry{
+				TufObj: models.TUFV001Schema{
+					/*
+						Metadata: &models.TUFV001SchemaMetadata{
+							Content: struct{}{},
+						},
+					*/
+					Root: &models.TUFV001SchemaRoot{
+						Content: struct{}{},
+					},
+				},
+			},
+			expectSuccess: false,
+		},
+		{
+			caseDesc:      "empty obj",
+			entry:         V001Entry{},
+			expectSuccess: false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			if ok, err := tc.entry.Insertable(); ok != tc.expectSuccess {
+				t.Errorf("unexpected result calling Insertable: %v", err)
 			}
-		}
+		})
 	}
 }
diff --git a/pkg/types/tuf/v0.0.1/fuzz_test.go b/pkg/types/tuf/v0.0.1/fuzz_test.go
index bd52cf3..9d6d79b 100644
--- a/pkg/types/tuf/v0.0.1/fuzz_test.go
+++ b/pkg/types/tuf/v0.0.1/fuzz_test.go
@@ -21,8 +21,11 @@ import (
 	"testing"
 
 	fuzz "github.com/AdamKorcz/go-fuzz-headers-1"
+	"github.com/go-openapi/swag"
 
 	fuzzUtils "github.com/sigstore/rekor/pkg/fuzz"
+	"github.com/sigstore/rekor/pkg/generated/models"
+	"github.com/sigstore/rekor/pkg/types"
 	"github.com/sigstore/rekor/pkg/types/tuf"
 )
 
@@ -47,10 +50,47 @@ func FuzzTufCreateProposedEntry(f *testing.F) {
 		if err != nil {
 			t.Skip()
 		}
-		c, err := it.UnmarshalEntry(entry)
+		ei, err := types.CreateVersionedEntry(entry)
 		if err != nil {
 			t.Skip()
 		}
-		_, _ = c.IndexKeys()
+
+		if ok, err := ei.Insertable(); !ok || err != nil {
+			t.Errorf("entry created via CreateProposedEntry should be insertable: %v", err)
+		}
+
+		if _, err := types.CanonicalizeEntry(context.Background(), ei); err != nil {
+			t.Errorf("valid insertable entry should be able to be canonicalized: %v", err)
+		}
+
+		_, _ = ei.IndexKeys()
+	})
+}
+
+func FuzzTufUnmarshalAndCanonicalize(f *testing.F) {
+	f.Fuzz(func(t *testing.T, entryData []byte) {
+		initter.Do(fuzzUtils.SetFuzzLogger)
+
+		ff := fuzz.NewConsumer(entryData)
+
+		targetV001 := &models.TUFV001Schema{}
+
+		if err := ff.GenerateStruct(targetV001); err != nil {
+			t.Skip()
+		}
+
+		targetEntry := &models.TUF{
+			APIVersion: swag.String(APIVERSION),
+			Spec:       targetV001,
+		}
+
+		ei, err := types.UnmarshalEntry(targetEntry)
+		if err != nil {
+			t.Skip()
+		}
+
+		if _, err := types.CanonicalizeEntry(context.Background(), ei); err != nil {
+			t.Errorf("error canonicalizing unmarshalled entry: %v", err)
+		}
 	})
 }
diff --git a/pkg/types/types_test.go b/pkg/types/types_test.go
index ea7c530..7d9ca66 100644
--- a/pkg/types/types_test.go
+++ b/pkg/types/types_test.go
@@ -38,11 +38,11 @@ func (e InvalidEntry) Kind() string {
 
 func (e InvalidEntry) SetKind(string) {}
 
-func (e InvalidEntry) Validate(formats strfmt.Registry) error {
+func (e InvalidEntry) Validate(_ strfmt.Registry) error {
 	return nil
 }
 
-func (e InvalidEntry) ContextValidate(context context.Context, formats strfmt.Registry) error {
+func (e InvalidEntry) ContextValidate(_ context.Context, _ strfmt.Registry) error {
 	return nil
 }
 
@@ -57,11 +57,11 @@ func (e UnmarshalErrorValidEntry) Kind() string {
 
 func (e UnmarshalErrorValidEntry) SetKind(string) {}
 
-func (e UnmarshalErrorValidEntry) Validate(formats strfmt.Registry) error {
+func (e UnmarshalErrorValidEntry) Validate(_ strfmt.Registry) error {
 	return errors.New("invalid content")
 }
 
-func (e UnmarshalErrorValidEntry) ContextValidate(context context.Context, formats strfmt.Registry) error {
+func (e UnmarshalErrorValidEntry) ContextValidate(_ context.Context, _ strfmt.Registry) error {
 	return nil
 }
 
diff --git a/pkg/verify/verify_test.go b/pkg/verify/verify_test.go
index 7cc7ded..02dd06a 100644
--- a/pkg/verify/verify_test.go
+++ b/pkg/verify/verify_test.go
@@ -38,7 +38,7 @@ type TlogClient struct {
 	LogInfo models.LogInfo
 }
 
-func (m *TlogClient) GetLogProof(params *tlog.GetLogProofParams, opts ...tlog.ClientOption) (*tlog.GetLogProofOK, error) {
+func (m *TlogClient) GetLogProof(_ *tlog.GetLogProofParams, _ ...tlog.ClientOption) (*tlog.GetLogProofOK, error) {
 	return &tlog.GetLogProofOK{
 		Payload: &models.ConsistencyProof{
 			Hashes:   m.Proof,
@@ -46,14 +46,14 @@ func (m *TlogClient) GetLogProof(params *tlog.GetLogProofParams, opts ...tlog.Cl
 		}}, nil
 }
 
-func (m *TlogClient) GetLogInfo(params *tlog.GetLogInfoParams, opts ...tlog.ClientOption) (*tlog.GetLogInfoOK, error) {
+func (m *TlogClient) GetLogInfo(_ *tlog.GetLogInfoParams, _ ...tlog.ClientOption) (*tlog.GetLogInfoOK, error) {
 	return &tlog.GetLogInfoOK{
 		Payload: &m.LogInfo,
 	}, nil
 }
 
 // TODO: Implement mock
-func (m *TlogClient) SetTransport(transport runtime.ClientTransport) {
+func (m *TlogClient) SetTransport(_ runtime.ClientTransport) {
 }
 
 func TestConsistency(t *testing.T) {
diff --git a/pkg/witness/publish_checkpoint_test.go b/pkg/witness/publish_checkpoint_test.go
new file mode 100644
index 0000000..c6453f3
--- /dev/null
+++ b/pkg/witness/publish_checkpoint_test.go
@@ -0,0 +1,324 @@
+// Copyright 2023 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 witness
+
+import (
+	"context"
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"errors"
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/go-redis/redismock/v9"
+	"github.com/golang/mock/gomock"
+	"github.com/google/trillian"
+	"github.com/google/trillian/types"
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/client_golang/prometheus/testutil"
+	"github.com/sigstore/rekor/pkg/witness/mockclient"
+	"github.com/sigstore/sigstore/pkg/signature"
+	"go.uber.org/goleak"
+)
+
+func TestPublishCheckpoint(t *testing.T) {
+	treeID := 1234
+	hostname := "rekor-test"
+	freq := 1
+	counter := prometheus.NewCounterVec(prometheus.CounterOpts{
+		Name: "rekor_checkpoint_publish",
+		Help: "Checkpoint publishing by shard and code",
+	}, []string{"shard", "code"})
+	priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	signer, _ := signature.LoadSigner(priv, crypto.SHA256)
+
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	root := &types.LogRootV1{TreeSize: 10, RootHash: []byte{1}, TimestampNanos: 123, Revision: 0}
+	mRoot, err := root.MarshalBinary()
+	if err != nil {
+		t.Fatalf("error marshalling log root: %v", err)
+	}
+
+	mockTrillianLogClient := mockclient.NewMockTrillianLogClient(ctrl)
+	mockTrillianLogClient.EXPECT().GetLatestSignedLogRoot(gomock.Any(), &trillian.GetLatestSignedLogRootRequest{
+		LogId:         int64(treeID),
+		FirstTreeSize: 0,
+	}).Return(&trillian.GetLatestSignedLogRootResponse{SignedLogRoot: &trillian.SignedLogRoot{LogRoot: mRoot}}, nil)
+
+	redisClient, mock := redismock.NewClientMock()
+	ts := time.Now().Truncate(time.Duration(freq) * time.Minute).UnixNano()
+	mock.Regexp().ExpectSetNX(fmt.Sprintf("%d/%d", treeID, ts), "[0-9a-fA-F]+", 0).SetVal(true)
+	mock.Regexp().ExpectSet(fmt.Sprintf("%d/latest", treeID), "[0-9a-fA-F]+", 0).SetVal("OK")
+
+	publisher := NewCheckpointPublisher(context.Background(), mockTrillianLogClient, int64(treeID), hostname, signer, redisClient, uint(freq), counter)
+
+	ctx, cancel := context.WithCancel(context.Background())
+	publisher.StartPublisher(ctx)
+	defer cancel()
+
+	// wait for initial publish
+	time.Sleep(1 * time.Second)
+
+	if err := mock.ExpectationsWereMet(); err != nil {
+		t.Error(err)
+	}
+
+	if res := testutil.CollectAndCount(counter); res != 2 {
+		t.Fatalf("unexpected number of metrics: %d", res)
+	}
+	if res := testutil.ToFloat64(counter.WithLabelValues(fmt.Sprint(treeID), fmt.Sprint(Success))); res != 1.0 {
+		t.Fatalf("unexpected number of metrics: %2f", res)
+	}
+	if res := testutil.ToFloat64(counter.WithLabelValues(fmt.Sprint(treeID), fmt.Sprint(SuccessObtainLock))); res != 1.0 {
+		t.Fatalf("unexpected number of metrics: %2f", res)
+	}
+}
+
+func TestPublishCheckpointMultiple(t *testing.T) {
+	treeID := 1234
+	hostname := "rekor-test"
+	freq := 1
+	counter := prometheus.NewCounterVec(prometheus.CounterOpts{
+		Name: "rekor_checkpoint_publish",
+		Help: "Checkpoint publishing by shard and code",
+	}, []string{"shard", "code"})
+	priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	signer, _ := signature.LoadSigner(priv, crypto.SHA256)
+
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	root := &types.LogRootV1{TreeSize: 10, RootHash: []byte{1}, TimestampNanos: 123, Revision: 0}
+	mRoot, err := root.MarshalBinary()
+	if err != nil {
+		t.Fatalf("error marshalling log root: %v", err)
+	}
+
+	mockTrillianLogClient := mockclient.NewMockTrillianLogClient(ctrl)
+	mockTrillianLogClient.EXPECT().GetLatestSignedLogRoot(gomock.Any(), &trillian.GetLatestSignedLogRootRequest{
+		LogId:         int64(treeID),
+		FirstTreeSize: 0,
+	}).Return(&trillian.GetLatestSignedLogRootResponse{SignedLogRoot: &trillian.SignedLogRoot{LogRoot: mRoot}}, nil).MaxTimes(2)
+
+	redisClient, mock := redismock.NewClientMock()
+	ts := time.Now().Truncate(time.Duration(freq) * time.Minute).UnixNano()
+	mock.Regexp().ExpectSetNX(fmt.Sprintf("%d/%d", treeID, ts), "[0-9a-fA-F]+", 0).SetVal(true)
+	mock.Regexp().ExpectSet(fmt.Sprintf("%d/latest", treeID), "[0-9a-fA-F]+", 0).SetVal("OK")
+
+	publisher := NewCheckpointPublisher(context.Background(), mockTrillianLogClient, int64(treeID), hostname, signer, redisClient, uint(freq), counter)
+	ctx, cancel := context.WithCancel(context.Background())
+	publisher.StartPublisher(ctx)
+	defer cancel()
+
+	redisClientEx, mockEx := redismock.NewClientMock()
+	mockEx.Regexp().ExpectSetNX(fmt.Sprintf("%d/%d", treeID, ts), "[0-9a-fA-F]+", 0).SetVal(false)
+	publisherEx := NewCheckpointPublisher(context.Background(), mockTrillianLogClient, int64(treeID), hostname, signer, redisClientEx, uint(freq), counter)
+	ctxEx, cancelEx := context.WithCancel(context.Background())
+	publisherEx.StartPublisher(ctxEx)
+	defer cancelEx()
+
+	// wait for initial publish
+	time.Sleep(1 * time.Second)
+
+	if err := mock.ExpectationsWereMet(); err != nil {
+		t.Error(err)
+	}
+	if err := mockEx.ExpectationsWereMet(); err != nil {
+		t.Error(err)
+	}
+
+	// only publishes once
+	if res := testutil.CollectAndCount(counter); res != 2 {
+		t.Fatalf("unexpected number of metrics: %d", res)
+	}
+	if res := testutil.ToFloat64(counter.WithLabelValues(fmt.Sprint(treeID), fmt.Sprint(Success))); res != 1.0 {
+		t.Fatalf("unexpected number of metrics: %2f", res)
+	}
+	if res := testutil.ToFloat64(counter.WithLabelValues(fmt.Sprint(treeID), fmt.Sprint(SuccessObtainLock))); res != 1.0 {
+		t.Fatalf("unexpected number of metrics: %2f", res)
+	}
+}
+
+func TestPublishCheckpointTrillianError(t *testing.T) {
+	treeID := 1234
+	hostname := "rekor-test"
+	freq := 1
+	counter := prometheus.NewCounterVec(prometheus.CounterOpts{
+		Name: "rekor_checkpoint_publish",
+		Help: "Checkpoint publishing by shard and code",
+	}, []string{"shard", "code"})
+	priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	signer, _ := signature.LoadSigner(priv, crypto.SHA256)
+
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	// return error
+	mockTrillianLogClient := mockclient.NewMockTrillianLogClient(ctrl)
+	mockTrillianLogClient.EXPECT().GetLatestSignedLogRoot(gomock.Any(), gomock.Any()).Return(nil, errors.New("error: LatestSLR"))
+
+	redisClient, _ := redismock.NewClientMock()
+
+	publisher := NewCheckpointPublisher(context.Background(), mockTrillianLogClient, int64(treeID), hostname, signer, redisClient, uint(freq), counter)
+	ctx, cancel := context.WithCancel(context.Background())
+	publisher.StartPublisher(ctx)
+	defer cancel()
+
+	// wait for initial publish
+	time.Sleep(1 * time.Second)
+
+	if res := testutil.CollectAndCount(counter); res != 1 {
+		t.Fatalf("unexpected number of metrics: %d", res)
+	}
+	if res := testutil.ToFloat64(counter.WithLabelValues(fmt.Sprint(treeID), fmt.Sprint(GetCheckpoint))); res != 1.0 {
+		t.Fatalf("unexpected number of metrics: %2f", res)
+	}
+}
+
+func TestPublishCheckpointInvalidTrillianResponse(t *testing.T) {
+	treeID := 1234
+	hostname := "rekor-test"
+	freq := 1
+	counter := prometheus.NewCounterVec(prometheus.CounterOpts{
+		Name: "rekor_checkpoint_publish",
+		Help: "Checkpoint publishing by shard and code",
+	}, []string{"shard", "code"})
+	priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	signer, _ := signature.LoadSigner(priv, crypto.SHA256)
+
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	// set no log root in response
+	mockTrillianLogClient := mockclient.NewMockTrillianLogClient(ctrl)
+	mockTrillianLogClient.EXPECT().GetLatestSignedLogRoot(gomock.Any(), gomock.Any()).
+		Return(&trillian.GetLatestSignedLogRootResponse{SignedLogRoot: &trillian.SignedLogRoot{LogRoot: []byte{}}}, nil)
+
+	redisClient, _ := redismock.NewClientMock()
+
+	publisher := NewCheckpointPublisher(context.Background(), mockTrillianLogClient, int64(treeID), hostname, signer, redisClient, uint(freq), counter)
+	ctx, cancel := context.WithCancel(context.Background())
+	publisher.StartPublisher(ctx)
+	defer cancel()
+
+	// wait for initial publish
+	time.Sleep(1 * time.Second)
+
+	if res := testutil.CollectAndCount(counter); res != 1 {
+		t.Fatalf("unexpected number of metrics: %d", res)
+	}
+	if res := testutil.ToFloat64(counter.WithLabelValues(fmt.Sprint(treeID), fmt.Sprint(UnmarshalCheckpoint))); res != 1.0 {
+		t.Fatalf("unexpected number of metrics: %2f", res)
+	}
+}
+
+func TestPublishCheckpointRedisFailure(t *testing.T) {
+	treeID := 1234
+	hostname := "rekor-test"
+	freq := 1
+	counter := prometheus.NewCounterVec(prometheus.CounterOpts{
+		Name: "rekor_checkpoint_publish",
+		Help: "Checkpoint publishing by shard and code",
+	}, []string{"shard", "code"})
+	priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	signer, _ := signature.LoadSigner(priv, crypto.SHA256)
+
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	root := &types.LogRootV1{TreeSize: 10, RootHash: []byte{1}, TimestampNanos: 123, Revision: 0}
+	mRoot, err := root.MarshalBinary()
+	if err != nil {
+		t.Fatalf("error marshalling log root: %v", err)
+	}
+
+	mockTrillianLogClient := mockclient.NewMockTrillianLogClient(ctrl)
+	mockTrillianLogClient.EXPECT().GetLatestSignedLogRoot(gomock.Any(), gomock.Any()).
+		Return(&trillian.GetLatestSignedLogRootResponse{SignedLogRoot: &trillian.SignedLogRoot{LogRoot: mRoot}}, nil)
+
+	redisClient, mock := redismock.NewClientMock()
+	// error on first redis call
+	mock.Regexp().ExpectSetNX(".+", "[0-9a-fA-F]+", 0).SetErr(errors.New("redis error"))
+
+	publisher := NewCheckpointPublisher(context.Background(), mockTrillianLogClient, int64(treeID), hostname, signer, redisClient, uint(freq), counter)
+	ctx, cancel := context.WithCancel(context.Background())
+	publisher.StartPublisher(ctx)
+	defer cancel()
+
+	// wait for initial publish
+	time.Sleep(1 * time.Second)
+
+	if res := testutil.CollectAndCount(counter); res != 1 {
+		t.Fatalf("unexpected number of metrics: %d", res)
+	}
+	if res := testutil.ToFloat64(counter.WithLabelValues(fmt.Sprint(treeID), fmt.Sprint(RedisFailure))); res != 1.0 {
+		t.Fatalf("unexpected number of metrics: %2f", res)
+	}
+}
+
+func TestPublishCheckpointRedisLatestFailure(t *testing.T) {
+	treeID := 1234
+	hostname := "rekor-test"
+	freq := 1
+	counter := prometheus.NewCounterVec(prometheus.CounterOpts{
+		Name: "rekor_checkpoint_publish",
+		Help: "Checkpoint publishing by shard and code",
+	}, []string{"shard", "code"})
+	priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	signer, _ := signature.LoadSigner(priv, crypto.SHA256)
+
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	root := &types.LogRootV1{TreeSize: 10, RootHash: []byte{1}, TimestampNanos: 123, Revision: 0}
+	mRoot, err := root.MarshalBinary()
+	if err != nil {
+		t.Fatalf("error marshalling log root: %v", err)
+	}
+
+	mockTrillianLogClient := mockclient.NewMockTrillianLogClient(ctrl)
+	mockTrillianLogClient.EXPECT().GetLatestSignedLogRoot(gomock.Any(), gomock.Any()).
+		Return(&trillian.GetLatestSignedLogRootResponse{SignedLogRoot: &trillian.SignedLogRoot{LogRoot: mRoot}}, nil)
+
+	redisClient, mock := redismock.NewClientMock()
+	mock.Regexp().ExpectSetNX(".+", "[0-9a-fA-F]+", 0).SetVal(true)
+	// error on second redis call
+	mock.Regexp().ExpectSet(".*", "[0-9a-fA-F]+", 0).SetErr(errors.New("error"))
+
+	publisher := NewCheckpointPublisher(context.Background(), mockTrillianLogClient, int64(treeID), hostname, signer, redisClient, uint(freq), counter)
+	ctx, cancel := context.WithCancel(context.Background())
+	publisher.StartPublisher(ctx)
+	defer cancel()
+
+	// wait for initial publish
+	time.Sleep(1 * time.Second)
+
+	// two metrics, one success for initial redis and one failure for latest
+	if res := testutil.CollectAndCount(counter); res != 2 {
+		t.Fatalf("unexpected number of metrics: %d", res)
+	}
+	if res := testutil.ToFloat64(counter.WithLabelValues(fmt.Sprint(treeID), fmt.Sprint(RedisLatestFailure))); res != 1.0 {
+		t.Fatalf("unexpected number of metrics: %2f", res)
+	}
+}
+
+func TestMain(m *testing.M) {
+	goleak.VerifyTestMain(m)
+}
diff --git a/tests/e2e_test.go b/tests/e2e_test.go
index 4c37392..7561c31 100644
--- a/tests/e2e_test.go
+++ b/tests/e2e_test.go
@@ -30,12 +30,10 @@ import (
 	"io/ioutil"
 	"net/http"
 	"os"
-	"os/exec"
 	"path/filepath"
 	"strconv"
 	"strings"
 	"testing"
-	"time"
 
 	"golang.org/x/sync/errgroup"
 
@@ -184,32 +182,6 @@ func TestGetCLI(t *testing.T) {
 	runCli(t, "get", "--format=json", "--uuid", entryID.ReturnEntryIDString())
 }
 
-func TestWatch(t *testing.T) {
-	td := t.TempDir()
-	cmd := exec.Command(server, "watch", "--interval=1s")
-	cmd.Env = append(os.Environ(), "REKOR_STH_BUCKET=file://"+td)
-	go func() {
-		b, err := cmd.CombinedOutput()
-		t.Log(string(b))
-		if cmd.ProcessState.Exited() && cmd.ProcessState.ExitCode() != 0 {
-			if err != nil {
-				t.Fatal(err)
-			}
-		}
-	}()
-
-	// Wait 3 intervals
-	time.Sleep(3 * time.Second)
-	cmd.Process.Kill()
-
-	// Check for files
-	fi, err := ioutil.ReadDir(td)
-	if err != nil || len(fi) == 0 {
-		t.Error("expected files")
-	}
-	fmt.Println(fi[0].Name())
-}
-
 func publicKeyFromRekorClient(ctx context.Context, c *generatedClient.Rekor) (*ecdsa.PublicKey, error) {
 	resp, err := c.Pubkey.GetPublicKey(&pubkey.GetPublicKeyParams{Context: ctx})
 	if err != nil {
diff --git a/tests/oss_fuzz.sh b/tests/oss_fuzz.sh
index e1eed1b..9369838 100755
--- a/tests/oss_fuzz.sh
+++ b/tests/oss_fuzz.sh
@@ -35,20 +35,31 @@ compile_native_go_fuzzer github.com/sigstore/rekor/pkg/sharding FuzzValidateTree
 compile_native_go_fuzzer github.com/sigstore/rekor/pkg/sharding FuzzValidateEntryID FuzzValidateEntryID
 compile_native_go_fuzzer github.com/sigstore/rekor/pkg/signer FuzzNewFile FuzzNewFile
 compile_native_go_fuzzer github.com/sigstore/rekor/pkg/types/cose/v0.0.1 FuzzCoseCreateProposedEntry FuzzCoseCreateProposedEntry
+compile_native_go_fuzzer github.com/sigstore/rekor/pkg/types/cose/v0.0.1 FuzzCoseUnmarshalAndCanonicalize FuzzCoseUnmarshalAndCanonicalize
+compile_native_go_fuzzer github.com/sigstore/rekor/pkg/types/hashedrekord FuzzHashedRekord FuzzHashedRekord
 compile_native_go_fuzzer github.com/sigstore/rekor/pkg/types/hashedrekord/v0.0.1 FuzzHashedRekordCreateProposedEntry FuzzHashedRekordCreateProposedEntry
+compile_native_go_fuzzer github.com/sigstore/rekor/pkg/types/hashedrekord/v0.0.1 FuzzHashedRekordUnmarshalAndCanonicalize FuzzHashedRekordUnmarshalAndCanonicalize
 compile_native_go_fuzzer github.com/sigstore/rekor/pkg/types/alpine FuzzPackageUnmarshal FuzzPackageUnmarshal
 compile_native_go_fuzzer github.com/sigstore/rekor/pkg/types/alpine/v0.0.1 FuzzAlpineCreateProposedEntry FuzzAlpineCreateProposedEntry
+compile_native_go_fuzzer github.com/sigstore/rekor/pkg/types/alpine/v0.0.1 FuzzAlpineUnmarshalAndCanonicalize FuzzAlpineUnmarshalAndCanonicalize
 compile_native_go_fuzzer github.com/sigstore/rekor/pkg/types/jar FuzzJarUnmarshal FuzzJarUnmarshal
 compile_native_go_fuzzer github.com/sigstore/rekor/pkg/types/jar/v0.0.1 FuzzJarCreateProposedEntry FuzzJarCreateProposedEntry
-compile_native_go_fuzzer github.com/sigstore/rekor/pkg/types/hashedrekord FuzzHashedRekord FuzzHashedRekord
+compile_native_go_fuzzer github.com/sigstore/rekor/pkg/types/jar/v0.0.1 FuzzJarUnmarshalAndCanonicalize FuzzJarUnmarshalAndCanonicalize
 compile_native_go_fuzzer github.com/sigstore/rekor/pkg/types/intoto/v0.0.1 FuzzIntotoCreateProposedEntry FuzzIntotoCreateProposedEntry_v001
+compile_native_go_fuzzer github.com/sigstore/rekor/pkg/types/intoto/v0.0.1 FuzzIntotoUnmarshalAndCanonicalize FuzzIntotoUnmarshalAndCanonicalize_v001
 compile_native_go_fuzzer github.com/sigstore/rekor/pkg/types/intoto/v0.0.2 FuzzIntotoCreateProposedEntry FuzzIntotoCreateProposedEntry_v002
+compile_native_go_fuzzer github.com/sigstore/rekor/pkg/types/intoto/v0.0.2 FuzzIntotoUnmarshalAndCanonicalize FuzzIntotoUnmarshalAndCanonicalize_v002
 compile_native_go_fuzzer github.com/sigstore/rekor/pkg/types/tuf/v0.0.1 FuzzTufCreateProposedEntry FuzzTufCreateProposedEntry
+compile_native_go_fuzzer github.com/sigstore/rekor/pkg/types/tuf/v0.0.1 FuzzTufUnmarshalAndCanonicalize FuzzTufUnmarshalAndCanonicalize
 compile_native_go_fuzzer github.com/sigstore/rekor/pkg/types/rfc3161/v0.0.1 FuzzRfc3161CreateProposedEntry FuzzRfc3161CreateProposedEntry
+compile_native_go_fuzzer github.com/sigstore/rekor/pkg/types/rfc3161/v0.0.1 FuzzRfc3161UnmarshalAndCanonicalize FuzzRfc3161UnmarshalAndCanonicalize
 compile_native_go_fuzzer github.com/sigstore/rekor/pkg/types/rpm/v0.0.1 FuzzRpmCreateProposedEntry FuzzRpmCreateProposedEntry
+compile_native_go_fuzzer github.com/sigstore/rekor/pkg/types/rpm/v0.0.1 FuzzRpmUnmarshalAndCanonicalize FuzzRpmUnmarshalAndCanonicalize
 compile_native_go_fuzzer github.com/sigstore/rekor/pkg/types/helm/v0.0.1 FuzzHelmCreateProposedEntry FuzzHelmCreateProposedEntry
+compile_native_go_fuzzer github.com/sigstore/rekor/pkg/types/helm/v0.0.1 FuzzHelmUnmarshalAndCanonicalize FuzzHelmUnmarshalAndCanonicalize
 compile_native_go_fuzzer github.com/sigstore/rekor/pkg/types/helm/v0.0.1 FuzzHelmProvenanceUnmarshal FuzzHelmProvenanceUnmarshal
 compile_native_go_fuzzer github.com/sigstore/rekor/pkg/types/rekord/v0.0.1 FuzzRekordCreateProposedEntry FuzzRekordCreateProposedEntry
+compile_native_go_fuzzer github.com/sigstore/rekor/pkg/types/rekord/v0.0.1 FuzzRekordUnmarshalAndCanonicalize FuzzRekordUnmarshalAndCanonicalize
 
 # Test 3rd party API that rekor/pkg/types/jar/v0.0.1 uses
 go mod edit -replace github.com/sassoftware/relic=$SRC/relic