From 0bc8bc54b1f4355d8ab186dfaf78985635d5b998 Mon Sep 17 00:00:00 2001
From: Bob Callaway <bobcallaway@users.noreply.github.com>
Date: Tue, 19 Jan 2021 09:58:04 -0500
Subject: [PATCH] add redis-based index & query endpoint (#112)

Signed-off-by: Bob Callaway <bcallawa@redhat.com>

Co-authored-by: Dan Lorenc <dlorenc@google.com>
---
 Dockerfile                                    |   4 +-
 cmd/cli/app/pflags.go                         |  44 +++-
 cmd/cli/app/pflags_test.go                    | 144 +++++++++++
 cmd/cli/app/search.go                         | 178 ++++++++++++++
 cmd/server/app/root.go                        |   4 +
 config/rekor.yaml                             |   2 +
 docker-compose.debug.yml                      |   3 +
 docker-compose.yml                            |  12 +
 go.mod                                        |   3 +-
 go.sum                                        |   8 +
 openapi.yaml                                  |  47 +++-
 pkg/api/api.go                                |  11 +-
 pkg/api/entries.go                            |  13 +
 pkg/api/error.go                              |  13 +
 pkg/api/index.go                              |  98 ++++++++
 .../client/entries/entries_client.go          |  10 +-
 pkg/generated/client/index/index_client.go    |  86 +++++++
 .../client/index/search_index_parameters.go   | 152 ++++++++++++
 .../client/index/search_index_responses.go    | 171 +++++++++++++
 pkg/generated/client/rekor_client.go          |   7 +-
 pkg/generated/client/tlog/tlog_client.go      |   6 +-
 pkg/generated/models/search_index.go          | 226 ++++++++++++++++++
 .../restapi/configure_rekor_server.go         |   8 +
 pkg/generated/restapi/doc.go                  |   1 -
 pkg/generated/restapi/embedded_spec.go        | 178 +++++++++++++-
 .../restapi/operations/index/search_index.go  |  75 ++++++
 .../index/search_index_parameters.go          |  94 ++++++++
 .../index/search_index_responses.go           | 180 ++++++++++++++
 .../index/search_index_urlbuilder.go          | 101 ++++++++
 .../restapi/operations/rekor_server_api.go    |  13 +
 pkg/generated/restapi/server.go               |   1 -
 pkg/types/rekord/rekord_test.go               |   4 +
 pkg/types/rekord/v0.0.1/entry.go              |  82 +++----
 pkg/types/types.go                            |   1 +
 pkg/util/fetch.go                             |  67 ++++++
 tests/e2e_test.go                             |  19 +-
 36 files changed, 1988 insertions(+), 78 deletions(-)
 create mode 100644 cmd/cli/app/search.go
 create mode 100644 pkg/api/index.go
 create mode 100644 pkg/generated/client/index/index_client.go
 create mode 100644 pkg/generated/client/index/search_index_parameters.go
 create mode 100644 pkg/generated/client/index/search_index_responses.go
 create mode 100644 pkg/generated/models/search_index.go
 create mode 100644 pkg/generated/restapi/operations/index/search_index.go
 create mode 100644 pkg/generated/restapi/operations/index/search_index_parameters.go
 create mode 100644 pkg/generated/restapi/operations/index/search_index_responses.go
 create mode 100644 pkg/generated/restapi/operations/index/search_index_urlbuilder.go
 create mode 100644 pkg/util/fetch.go

diff --git a/Dockerfile b/Dockerfile
index 9a2fa1b..848a7e5 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -13,7 +13,7 @@ RUN go build ./cmd/server
 RUN CGO_ENABLED=0 go build -gcflags "all=-N -l" -o server_debug ./cmd/server
 
 # Multi-Stage production build
-FROM registry.access.redhat.com/ubi8/ubi-minimal as deploy
+FROM golang:1.15 as deploy
 
 # Retrieve the binary from the previous stage
 COPY --from=builder /opt/app-root/src/server /usr/local/bin/rekor-server
@@ -23,7 +23,7 @@ CMD ["rekor-server", "serve"]
 
 # debug compile options & debugger
 FROM deploy as debug
+RUN go get github.com/go-delve/delve/cmd/dlv
 
 # overwrite server and include debugger
 COPY --from=builder /opt/app-root/src/server_debug /usr/local/bin/rekor-server
-COPY --from=builder /usr/bin/dlv /usr/local/bin/dlv
diff --git a/cmd/cli/app/pflags.go b/cmd/cli/app/pflags.go
index b2a2a4e..4d7ced9 100644
--- a/cmd/cli/app/pflags.go
+++ b/cmd/cli/app/pflags.go
@@ -36,9 +36,37 @@ import (
 	"github.com/spf13/viper"
 )
 
+func addSearchPFlags(cmd *cobra.Command) error {
+	cmd.Flags().Var(&pkiFormatFlag{value: "pgp"}, "pki-format", "format of the signature and/or public key")
+
+	cmd.Flags().Var(&fileOrURLFlag{}, "public-key", "path or URL to public key file")
+
+	cmd.Flags().Var(&fileOrURLFlag{}, "artifact", "path or URL to artifact file")
+
+	cmd.Flags().Var(&shaFlag{}, "sha", "the sha of the artifact")
+	return nil
+}
+
+func validateSearchPFlags() error {
+	artifactStr := viper.GetString("artifact")
+
+	publicKey := viper.GetString("public-key")
+	sha := viper.GetString("sha")
+
+	if artifactStr == "" && publicKey == "" && sha == "" {
+		return errors.New("either 'sha' or 'artifact' or 'public-key' must be specified")
+	}
+	if publicKey != "" {
+		if viper.GetString("pki-format") == "" {
+			return errors.New("pki-format must be specified if searching by public-key")
+		}
+	}
+	return nil
+}
+
 func addArtifactPFlags(cmd *cobra.Command) error {
 	cmd.Flags().Var(&fileOrURLFlag{}, "signature", "path or URL to detached signature file")
-	cmd.Flags().Var(&sigFormatFlag{value: "pgp"}, "signature-format", "format of the signature")
+	cmd.Flags().Var(&pkiFormatFlag{value: "pgp"}, "pki-format", "format of the signature and/or public key")
 
 	cmd.Flags().Var(&fileOrURLFlag{}, "public-key", "path or URL to public key file")
 
@@ -161,8 +189,8 @@ func CreateRekordFromPFlags() (models.ProposedEntry, error) {
 		}
 
 		re.RekordObj.Signature = &models.RekordV001SchemaSignature{}
-		sigFormat := viper.GetString("signature-format")
-		switch sigFormat {
+		pkiFormat := viper.GetString("pki-format")
+		switch pkiFormat {
 		case "pgp":
 			re.RekordObj.Signature.Format = models.RekordV001SchemaSignatureFormatPgp
 		case "minisign":
@@ -240,19 +268,19 @@ func (f *fileOrURLFlag) Type() string {
 	return "fileOrURLFlag"
 }
 
-type sigFormatFlag struct {
+type pkiFormatFlag struct {
 	value string
 }
 
-func (f *sigFormatFlag) Type() string {
-	return "sigFormat"
+func (f *pkiFormatFlag) Type() string {
+	return "pkiFormat"
 }
 
-func (f *sigFormatFlag) String() string {
+func (f *pkiFormatFlag) String() string {
 	return f.value
 }
 
-func (f *sigFormatFlag) Set(s string) error {
+func (f *pkiFormatFlag) Set(s string) error {
 	set := map[string]struct{}{
 		"pgp":      {},
 		"minisign": {},
diff --git a/cmd/cli/app/pflags_test.go b/cmd/cli/app/pflags_test.go
index f72f695..d18be1e 100644
--- a/cmd/cli/app/pflags_test.go
+++ b/cmd/cli/app/pflags_test.go
@@ -393,3 +393,147 @@ func TestValidateRekorServerURL(t *testing.T) {
 		}
 	}
 }
+
+func TestSearchPFlags(t *testing.T) {
+	type test struct {
+		caseDesc              string
+		artifact              string
+		publicKey             string
+		sha                   string
+		pkiFormat             string
+		expectParseSuccess    bool
+		expectValidateSuccess bool
+	}
+
+	testServer := httptest.NewServer(http.HandlerFunc(
+		func(w http.ResponseWriter, r *http.Request) {
+			file := []byte{}
+			var err error
+
+			switch r.URL.Path {
+			case "/artifact":
+				file, err = ioutil.ReadFile("../../../tests/test_file.txt")
+			case "/publicKey":
+				file, err = ioutil.ReadFile("../../../tests/test_public_key.key")
+			case "/not_found":
+				err = errors.New("file not found")
+			}
+			if err != nil {
+				w.WriteHeader(http.StatusNotFound)
+				return
+			}
+			w.WriteHeader(http.StatusOK)
+			_, _ = w.Write(file)
+		}))
+	defer testServer.Close()
+
+	tests := []test{
+		{
+			caseDesc:              "valid local artifact",
+			artifact:              "../../../tests/test_file.txt",
+			expectParseSuccess:    true,
+			expectValidateSuccess: true,
+		},
+		{
+			caseDesc:              "valid remote artifact",
+			artifact:              testServer.URL + "/artifact",
+			expectParseSuccess:    true,
+			expectValidateSuccess: true,
+		},
+		{
+			caseDesc:              "nonexistant local artifact",
+			artifact:              "../../../tests/not_a_file",
+			expectParseSuccess:    false,
+			expectValidateSuccess: false,
+		},
+		{
+			caseDesc:              "nonexistant remote artifact",
+			artifact:              testServer.URL + "/not_found",
+			expectParseSuccess:    true,
+			expectValidateSuccess: true,
+		},
+		{
+			caseDesc:              "valid local public key",
+			publicKey:             "../../../tests/test_public_key.key",
+			expectParseSuccess:    true,
+			expectValidateSuccess: true,
+		},
+		{
+			caseDesc:              "valid local minisign public key",
+			publicKey:             "../../../pkg/pki/minisign/testdata/minisign.pub",
+			pkiFormat:             "minisign",
+			expectParseSuccess:    true,
+			expectValidateSuccess: true,
+		},
+		{
+			caseDesc:              "valid remote public key",
+			publicKey:             testServer.URL + "/publicKey",
+			expectParseSuccess:    true,
+			expectValidateSuccess: true,
+		},
+		{
+			caseDesc:              "nonexistant local public key",
+			publicKey:             "../../../tests/not_a_file",
+			expectParseSuccess:    false,
+			expectValidateSuccess: false,
+		},
+		{
+			caseDesc:              "nonexistant remote public key",
+			publicKey:             testServer.URL + "/not_found",
+			expectParseSuccess:    true,
+			expectValidateSuccess: true,
+		},
+		{
+			caseDesc:              "valid SHA",
+			sha:                   "45c7b11fcbf07dec1694adecd8c5b85770a12a6c8dfdcf2580a2db0c47c31779",
+			expectParseSuccess:    true,
+			expectValidateSuccess: true,
+		},
+		{
+			caseDesc:              "invalid SHA",
+			sha:                   "45c7b11fcbf",
+			expectParseSuccess:    false,
+			expectValidateSuccess: false,
+		},
+		{
+			caseDesc:              "no flags when either artifact, sha, or public key are needed",
+			expectParseSuccess:    true,
+			expectValidateSuccess: false,
+		},
+	}
+
+	for _, tc := range tests {
+		var blankCmd = &cobra.Command{}
+		if err := addSearchPFlags(blankCmd); err != nil {
+			t.Fatalf("unexpected error adding flags in '%v': %v", tc.caseDesc, err)
+		}
+
+		args := []string{}
+
+		if tc.artifact != "" {
+			args = append(args, "--artifact", tc.artifact)
+		}
+		if tc.publicKey != "" {
+			args = append(args, "--public-key", tc.publicKey)
+		}
+		if tc.pkiFormat != "" {
+			args = append(args, "--pki-format", tc.pkiFormat)
+		}
+		if tc.sha != "" {
+			args = append(args, "--sha", tc.sha)
+		}
+
+		if err := blankCmd.ParseFlags(args); (err == nil) != tc.expectParseSuccess {
+			t.Errorf("unexpected result parsing '%v': %v", tc.caseDesc, err)
+			continue
+		}
+
+		if err := viper.BindPFlags(blankCmd.Flags()); err != nil {
+			t.Fatalf("unexpected result initializing viper in '%v': %v", tc.caseDesc, err)
+		}
+		if err := validateSearchPFlags(); (err == nil) != tc.expectValidateSuccess {
+			t.Errorf("unexpected result validating '%v': %v", tc.caseDesc, err)
+			continue
+		}
+	}
+}
diff --git a/cmd/cli/app/search.go b/cmd/cli/app/search.go
new file mode 100644
index 0000000..860dd18
--- /dev/null
+++ b/cmd/cli/app/search.go
@@ -0,0 +1,178 @@
+/*
+Copyright © 2021 Bob Callaway <bcallawa@redhat.com>
+
+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 (
+	"crypto/sha256"
+	"encoding/hex"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/go-openapi/swag"
+
+	"github.com/go-openapi/strfmt"
+
+	"github.com/projectrekor/rekor/cmd/cli/app/format"
+	"github.com/projectrekor/rekor/pkg/generated/client/index"
+	"github.com/projectrekor/rekor/pkg/generated/models"
+	"github.com/projectrekor/rekor/pkg/log"
+
+	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+)
+
+type searchCmdOutput struct {
+	uuids []string
+}
+
+func (s *searchCmdOutput) String() string {
+	str := "No matching entries were found\n"
+	for i, uuid := range s.uuids {
+		if i == 0 {
+			str = "Found matching entries (listed by UUID):\n"
+		}
+		str += fmt.Sprintf("%v\n", uuid)
+	}
+	return str
+}
+
+// searchCmd represents the get command
+var searchCmd = &cobra.Command{
+	Use:   "search",
+	Short: "Rekor search command",
+	Long:  `Searches the Rekor index to find entries by artifact or public key`,
+	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)
+		}
+		if err := validateSearchPFlags(); err != nil {
+			log.Logger.Error(err)
+			_ = cmd.Help()
+			os.Exit(1)
+		}
+	},
+	Run: format.WrapCmd(func(args []string) (interface{}, error) {
+		log := log.Logger
+		rekorClient, err := GetRekorClient(viper.GetString("rekor_server"))
+		if err != nil {
+			return nil, err
+		}
+
+		params := index.NewSearchIndexParams()
+		params.Query = &models.SearchIndex{}
+
+		artifactStr := viper.GetString("artifact")
+		sha := viper.GetString("sha")
+		if sha != "" {
+			params.Query.Hash = sha
+		} else if artifactStr != "" {
+			artifact := fileOrURLFlag{}
+			if err := artifact.Set(artifactStr); err != nil {
+				return nil, err
+			}
+
+			hasher := sha256.New()
+			var tee io.Reader
+			if artifact.IsURL {
+				/* #nosec G107 */
+				resp, err := http.Get(artifact.String())
+				if err != nil {
+					return nil, fmt.Errorf("error fetching '%v': %w", artifact.String(), err)
+				}
+				defer resp.Body.Close()
+				tee = io.TeeReader(resp.Body, hasher)
+			} else {
+				file, err := os.Open(filepath.Clean(artifact.String()))
+				if err != nil {
+					return nil, fmt.Errorf("error opening file '%v': %w", artifact.String(), err)
+				}
+				defer func() {
+					if err := file.Close(); err != nil {
+						log.Error(err)
+					}
+				}()
+
+				tee = io.TeeReader(file, hasher)
+			}
+			if _, err := ioutil.ReadAll(tee); err != nil {
+				return nil, fmt.Errorf("error processing '%v': %w", artifact.String(), err)
+			}
+
+			hashVal := strings.ToLower(hex.EncodeToString(hasher.Sum(nil)))
+			params.Query.Hash = hashVal
+		}
+
+		publicKeyStr := viper.GetString("public-key")
+		if publicKeyStr != "" {
+			params.Query.PublicKey = &models.SearchIndexPublicKey{}
+			pkiFormat := viper.GetString("pki-format")
+			switch pkiFormat {
+			case "pgp":
+				params.Query.PublicKey.Format = swag.String(models.SearchIndexPublicKeyFormatPgp)
+			case "minisign":
+				params.Query.PublicKey.Format = swag.String(models.SearchIndexPublicKeyFormatMinisign)
+			case "x509":
+				params.Query.PublicKey.Format = swag.String(models.SearchIndexPublicKeyFormatX509)
+			default:
+				return nil, fmt.Errorf("unknown pki-format %v", pkiFormat)
+			}
+			publicKey := fileOrURLFlag{}
+			if err := publicKey.Set(publicKeyStr); err != nil {
+				return nil, err
+			}
+			if publicKey.IsURL {
+				params.Query.PublicKey.URL = strfmt.URI(publicKey.String())
+			} else {
+				keyBytes, err := ioutil.ReadFile(filepath.Clean(publicKey.String()))
+				if err != nil {
+					return nil, fmt.Errorf("error reading public key file: %w", err)
+				}
+				params.Query.PublicKey.Content = strfmt.Base64(keyBytes)
+			}
+		}
+
+		resp, err := rekorClient.Index.SearchIndex(params)
+		if err != nil {
+			switch t := err.(type) {
+			case *index.SearchIndexDefault:
+				if t.Code() == http.StatusNotImplemented {
+					return nil, fmt.Errorf("search index not enabled on %v", viper.GetString("rekor_server"))
+				}
+				return nil, err
+			default:
+				return nil, err
+			}
+		}
+
+		return &searchCmdOutput{
+			uuids: resp.GetPayload(),
+		}, nil
+	}),
+}
+
+func init() {
+	if err := addSearchPFlags(searchCmd); err != nil {
+		log.Logger.Fatal("Error parsing cmd line args:", err)
+	}
+
+	rootCmd.AddCommand(searchCmd)
+}
diff --git a/cmd/server/app/root.go b/cmd/server/app/root.go
index 84033ee..dccf9a8 100644
--- a/cmd/server/app/root.go
+++ b/cmd/server/app/root.go
@@ -63,6 +63,10 @@ func init() {
 	rootCmd.PersistentFlags().String("rekor_server.address", "127.0.0.1", "Address to bind to")
 	rootCmd.PersistentFlags().Uint16("rekor_server.port", 3000, "Port to bind to")
 
+	rootCmd.PersistentFlags().Bool("enable_retrieve_api", true, "enables Redis-based index API endpoint")
+	rootCmd.PersistentFlags().String("redis_server.address", "127.0.0.1", "Redis server address")
+	rootCmd.PersistentFlags().Uint16("redis_server.port", 6379, "Redis server port")
+
 	if err := viper.BindPFlags(rootCmd.PersistentFlags()); err != nil {
 		log.Logger.Fatal(err)
 	}
diff --git a/config/rekor.yaml b/config/rekor.yaml
index 4c91a83..c340245 100644
--- a/config/rekor.yaml
+++ b/config/rekor.yaml
@@ -24,6 +24,8 @@ spec:
           "--trillian_log_server.address=trillian-server",
           "--trillian_log_server.port=8091",
           "--rekor_server.address=0.0.0.0",
+          "--redis_server.address=10.234.175.59",
+          "--redis_server.port=6379"
         ]
 ---
 apiVersion: v1
diff --git a/docker-compose.debug.yml b/docker-compose.debug.yml
index 3f74dbe..575a5d0 100644
--- a/docker-compose.debug.yml
+++ b/docker-compose.debug.yml
@@ -16,6 +16,8 @@ services:
       "serve",
       "--trillian_log_server.address=trillian-log-server",
       "--trillian_log_server.port=8091",
+      "--redis_server.address=redis-server",
+      "--redis_server.port=6379",
       "--rekor_server.address=0.0.0.0",
       ]
     restart: always # keep the server running
@@ -24,4 +26,5 @@ services:
       - "2345:2345"
     depends_on:
       - mysql
+      - redis-server
       - trillian-log-server
diff --git a/docker-compose.yml b/docker-compose.yml
index f63957a..2b4c315 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -8,6 +8,15 @@ services:
       - MYSQL_USER=test
       - MYSQL_PASSWORD=zaphod
     restart: always # keep the MySQL server running
+  redis-server:
+    image: docker.io/redis:5.0.10
+    command: [
+      "--bind",
+      "0.0.0.0",
+    ]
+    ports:
+      - "6379:6379"
+    restart: always # keep the redis server running
   trillian-log-server:
     image: gcr.io/trillian-opensource-ci/log_server
     command: [
@@ -47,6 +56,8 @@ services:
       "serve",
       "--trillian_log_server.address=trillian-log-server",
       "--trillian_log_server.port=8091",
+      "--redis_server.address=redis-server",
+      "--redis_server.port=6379",
       "--rekor_server.address=0.0.0.0",
       ]
     restart: always # keep the server running
@@ -54,4 +65,5 @@ services:
       - "3000:3000"
     depends_on:
       - mysql
+      - redis-server
       - trillian-log-server
diff --git a/go.mod b/go.mod
index 0cb0091..dff9352 100644
--- a/go.mod
+++ b/go.mod
@@ -12,7 +12,7 @@ require (
 	github.com/go-chi/chi v4.1.2+incompatible
 	github.com/go-openapi/errors v0.19.9
 	github.com/go-openapi/loads v0.20.0
-	github.com/go-openapi/runtime v0.19.24
+	github.com/go-openapi/runtime v0.19.26
 	github.com/go-openapi/spec v0.20.1
 	github.com/go-openapi/strfmt v0.20.0
 	github.com/go-openapi/swag v0.19.13
@@ -25,6 +25,7 @@ require (
 	github.com/jedisct1/go-minisign v0.0.0-20210106175330-e54e81d562c7
 	github.com/kr/pretty v0.2.1 // indirect
 	github.com/magiconair/properties v1.8.4 // indirect
+	github.com/mediocregopher/radix/v4 v4.0.0-beta.1
 	github.com/mitchellh/go-homedir v1.1.0
 	github.com/mitchellh/mapstructure v1.4.1
 	github.com/pelletier/go-toml v1.8.1 // indirect
diff --git a/go.sum b/go.sum
index a70a1ff..f62ae2e 100644
--- a/go.sum
+++ b/go.sum
@@ -227,6 +227,8 @@ github.com/go-openapi/runtime v0.19.16/go.mod h1:5P9104EJgYcizotuXhEuUrzVc+j1RiS
 github.com/go-openapi/runtime v0.19.20/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk=
 github.com/go-openapi/runtime v0.19.24 h1:TqagMVlRAOTwllE/7hNKx6rQ10O6T8ZzeJdMjSTKaD4=
 github.com/go-openapi/runtime v0.19.24/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk=
+github.com/go-openapi/runtime v0.19.26 h1:K/6PoVNj5WJXUnMk+VEbELeXjtBkCS1UxTDa04tdXE0=
+github.com/go-openapi/runtime v0.19.26/go.mod h1:BvrQtn6iVb2QmiVXRsFAm6ZCAZBpbVKFfN6QWCp582M=
 github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
 github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
 github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY=
@@ -562,6 +564,8 @@ github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsO
 github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/mediocregopher/radix/v4 v4.0.0-beta.1 h1:RDgQ4wCQ6f+pUsX20CIzjNJnI5P9KDw4j1sjOVHxo7Y=
+github.com/mediocregopher/radix/v4 v4.0.0-beta.1/go.mod h1:Z74pilm773ghbGV4EEoPvi6XWgkAfr0VCNkfa8gI1PU=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
 github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
@@ -618,6 +622,8 @@ github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGV
 github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
+github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
 github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
@@ -752,6 +758,8 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69
 github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
 github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU=
 github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
+github.com/tilinna/clock v1.0.2 h1:6BO2tyAC9JbPExKH/z9zl44FLu1lImh3nDNKA0kgrkI=
+github.com/tilinna/clock v1.0.2/go.mod h1:ZsP7BcY7sEEz7ktc0IVy8Us6boDrK8VradlKRUGfOao=
 github.com/timakin/bodyclose v0.0.0-20190721030226-87058b9bfcec/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ=
diff --git a/openapi.yaml b/openapi.yaml
index a91ffd2..0b2c941 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -7,7 +7,6 @@ info:
 host: api.rekor.dev
 schemes:
   - http
-  - https
 
 consumes:
   - application/json
@@ -17,6 +16,31 @@ produces:
   - application/yaml
 
 paths:
+  /api/v1/index/retrieve:
+    post:
+      summary: Searches index by entry metadata
+      operationId: searchIndex
+      tags:
+        - index
+      parameters:
+        - in: body
+          name: query
+          required: true
+          schema:
+            $ref: '#/definitions/SearchIndex'
+      responses:
+        200:
+          description: Returns zero or more entry UUIDs from the transparency log based on search query
+          schema:
+            type: array
+            items:
+              type: string
+              description: Entry UUID in transparency log
+              pattern: '^[0-9a-fA-F]{64}$'
+        400:
+          $ref: '#/responses/BadContent'
+        default:
+          $ref: '#/responses/InternalServerError'
   /api/v1/log:
     get:
       summary: Get information about the current state of the transparency log
@@ -249,6 +273,27 @@ definitions:
       required:
         - "body"
 
+  SearchIndex:
+    type: object
+    properties:
+      publicKey:
+        type: object
+        properties:
+          format:
+            type: string
+            enum: ['pgp','x509','minisign']
+          content:
+            type: string
+            format: byte
+          url:
+            type: string
+            format: uri
+        required:
+          - "format"
+      hash:
+        type: string
+        pattern: '^[0-9a-fA-F]{64}$'
+
   SearchLogQuery:
     type: object
     properties:
diff --git a/pkg/api/api.go b/pkg/api/api.go
index 7e70f51..a8b407b 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -23,6 +23,7 @@ import (
 
 	"github.com/google/trillian"
 	"github.com/google/trillian/crypto/keyspb"
+	radix "github.com/mediocregopher/radix/v4"
 	"github.com/projectrekor/rekor/pkg/log"
 	"github.com/spf13/viper"
 	"google.golang.org/grpc"
@@ -82,15 +83,23 @@ func NewAPI() (*API, error) {
 }
 
 var (
-	api *API
+	api         *API
+	redisClient radix.Client
 )
 
 func ConfigureAPI() {
+	cfg := radix.PoolConfig{}
 	var err error
 	api, err = NewAPI()
 	if err != nil {
 		log.Logger.Panic(err)
 	}
+	if viper.GetBool("enable_retrieve_api") {
+		redisClient, err = cfg.New(context.Background(), "tcp", fmt.Sprintf("%v:%v", viper.GetString("redis_server.address"), viper.GetUint64("redis_server.port")))
+		if err != nil {
+			log.Logger.Panic(err)
+		}
+	}
 }
 
 func NewTrillianClient(ctx context.Context) TrillianClient {
diff --git a/pkg/api/entries.go b/pkg/api/entries.go
index 02cce07..526b023 100644
--- a/pkg/api/entries.go
+++ b/pkg/api/entries.go
@@ -16,6 +16,7 @@ limitations under the License.
 package api
 
 import (
+	"context"
 	"crypto"
 	"crypto/x509"
 	"encoding/hex"
@@ -24,6 +25,7 @@ import (
 	"net/http"
 
 	"github.com/google/trillian"
+	"github.com/spf13/viper"
 	"golang.org/x/sync/errgroup"
 
 	"github.com/go-openapi/swag"
@@ -31,6 +33,7 @@ import (
 	"google.golang.org/genproto/googleapis/rpc/code"
 	"google.golang.org/grpc/codes"
 
+	"github.com/projectrekor/rekor/pkg/log"
 	"github.com/projectrekor/rekor/pkg/types"
 
 	"github.com/projectrekor/rekor/pkg/generated/models"
@@ -114,6 +117,16 @@ func CreateLogEntryHandler(params entries.CreateLogEntryParams) middleware.Respo
 		},
 	}
 
+	if viper.GetBool("enable_retrieve_api") {
+		go func() {
+			for _, key := range entry.IndexKeys() {
+				if err := addToIndex(context.Background(), key, uuid); err != nil {
+					log.RequestIDLogger(params.HTTPRequest).Error(err)
+				}
+			}
+		}()
+	}
+
 	location := strfmt.URI(fmt.Sprintf("%v/%v", httpReq.URL, uuid))
 	return entries.NewCreateLogEntryCreated().WithPayload(logEntry).WithLocation(location).WithETag(uuid)
 }
diff --git a/pkg/api/error.go b/pkg/api/error.go
index aee5c03..de92c06 100644
--- a/pkg/api/error.go
+++ b/pkg/api/error.go
@@ -24,6 +24,7 @@ import (
 	"github.com/mitchellh/mapstructure"
 	"github.com/projectrekor/rekor/pkg/generated/models"
 	"github.com/projectrekor/rekor/pkg/generated/restapi/operations/entries"
+	"github.com/projectrekor/rekor/pkg/generated/restapi/operations/index"
 	"github.com/projectrekor/rekor/pkg/generated/restapi/operations/tlog"
 	"github.com/projectrekor/rekor/pkg/log"
 )
@@ -35,6 +36,10 @@ const (
 	entryAlreadyExists             = "An equivalent entry already exists in the transparency log"
 	firstSizeLessThanLastSize      = "firstSize(%v) must be less than lastSize(%v)"
 	malformedUUID                  = "UUID must be a 64-character hexadecimal string"
+	malformedHash                  = "Hash must be a 64-character hexadecimal string created from SHA256 algorithm"
+	malformedPublicKey             = "Public key provided could not be parsed"
+	failedToGenerateCanonicalKey   = "Error generating canonicalized public key"
+	redisUnexpectedResult          = "Unexpected result from searching index"
 )
 
 func errorMsg(message string, code int) *models.Error {
@@ -118,6 +123,14 @@ func handleRekorAPIError(params interface{}, code int, err error, message string
 	case tlog.GetPublicKeyParams:
 		logMsg(params.HTTPRequest)
 		return tlog.NewGetPublicKeyDefault(code).WithPayload(errorMsg(message, code))
+	case index.SearchIndexParams:
+		logMsg(params.HTTPRequest)
+		switch code {
+		case http.StatusBadRequest:
+			return index.NewSearchIndexBadRequest()
+		default:
+			return index.NewSearchIndexDefault(code).WithPayload(errorMsg(message, code))
+		}
 	default:
 		log.Logger.Errorf("unable to find method for type %T; error: %v", params, err)
 		return middleware.Error(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
diff --git a/pkg/api/index.go b/pkg/api/index.go
new file mode 100644
index 0000000..5ebe8a5
--- /dev/null
+++ b/pkg/api/index.go
@@ -0,0 +1,98 @@
+/*
+Copyright © 2021 Bob Callaway <bcallawa@redhat.com>
+
+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 api
+
+import (
+	"context"
+	"crypto/sha256"
+	"encoding/hex"
+	"errors"
+	"net/http"
+	"strings"
+
+	"github.com/projectrekor/rekor/pkg/pki"
+
+	radix "github.com/mediocregopher/radix/v4"
+
+	"github.com/asaskevich/govalidator"
+	"github.com/go-openapi/runtime/middleware"
+	"github.com/go-openapi/swag"
+	"github.com/projectrekor/rekor/pkg/generated/models"
+	"github.com/projectrekor/rekor/pkg/generated/restapi/operations/index"
+	"github.com/projectrekor/rekor/pkg/util"
+)
+
+func SearchIndexHandler(params index.SearchIndexParams) middleware.Responder {
+	httpReqCtx := params.HTTPRequest.Context()
+
+	var result []string
+	if params.Query.Hash != "" {
+		//validate this is only a valid sha256 hash
+		if !govalidator.IsSHA256(params.Query.Hash) {
+			return handleRekorAPIError(params, http.StatusBadRequest, errors.New("invalid hash value specified"), malformedHash)
+		}
+		var resultUUIDs []string
+		if err := redisClient.Do(httpReqCtx, radix.Cmd(&resultUUIDs, "LRANGE", strings.ToLower(params.Query.Hash), "0", "-1")); err != nil {
+			return handleRekorAPIError(params, http.StatusInternalServerError, err, redisUnexpectedResult)
+		}
+		result = append(result, resultUUIDs...)
+	}
+	if params.Query.PublicKey != nil {
+		af := pki.NewArtifactFactory(swag.StringValue(params.Query.PublicKey.Format))
+		keyReader, err := util.FileOrURLReadCloser(httpReqCtx, params.Query.PublicKey.URL.String(), params.Query.PublicKey.Content, true)
+		if err != nil {
+			return handleRekorAPIError(params, http.StatusBadRequest, err, malformedPublicKey)
+		}
+		defer keyReader.Close()
+
+		key, err := af.NewPublicKey(keyReader)
+		if err != nil {
+			return handleRekorAPIError(params, http.StatusBadRequest, err, malformedPublicKey)
+		}
+		canonicalKey, err := key.CanonicalValue()
+		if err != nil {
+			return handleRekorAPIError(params, http.StatusInternalServerError, err, failedToGenerateCanonicalKey)
+		}
+
+		hasher := sha256.New()
+		if _, err := hasher.Write(canonicalKey); err != nil {
+			return handleRekorAPIError(params, http.StatusInternalServerError, err, failedToGenerateCanonicalKey)
+		}
+		keyHash := hasher.Sum(nil)
+		var resultUUIDs []string
+		if err := redisClient.Do(httpReqCtx, radix.Cmd(&resultUUIDs, "LRANGE", strings.ToLower(hex.EncodeToString(keyHash)), "0", "-1")); err != nil {
+			return handleRekorAPIError(params, http.StatusInternalServerError, err, redisUnexpectedResult)
+		}
+		result = append(result, resultUUIDs...)
+	}
+
+	return index.NewSearchIndexOK().WithPayload(result)
+}
+
+func SearchIndexNotImplementedHandler(params index.SearchIndexParams) middleware.Responder {
+	err := models.Error{
+		Code:    http.StatusNotImplemented,
+		Message: "Search Index API not enabled in this Rekor instance",
+	}
+
+	return index.NewSearchIndexDefault(http.StatusNotImplemented).WithPayload(&err)
+
+}
+
+func addToIndex(ctx context.Context, key, value string) error {
+	return redisClient.Do(ctx, radix.Cmd(nil, "LPUSH", key, value))
+}
diff --git a/pkg/generated/client/entries/entries_client.go b/pkg/generated/client/entries/entries_client.go
index 995df90..aa5d14b 100644
--- a/pkg/generated/client/entries/entries_client.go
+++ b/pkg/generated/client/entries/entries_client.go
@@ -73,7 +73,7 @@ func (a *Client) CreateLogEntry(params *CreateLogEntryParams) (*CreateLogEntryCr
 		PathPattern:        "/api/v1/log/entries",
 		ProducesMediaTypes: []string{"application/json;q=1", "application/yaml"},
 		ConsumesMediaTypes: []string{"application/json", "application/yaml"},
-		Schemes:            []string{"http", "https"},
+		Schemes:            []string{"http"},
 		Params:             params,
 		Reader:             &CreateLogEntryReader{formats: a.formats},
 		Context:            params.Context,
@@ -106,7 +106,7 @@ func (a *Client) GetLogEntryByIndex(params *GetLogEntryByIndexParams) (*GetLogEn
 		PathPattern:        "/api/v1/log/entries",
 		ProducesMediaTypes: []string{"application/json;q=1", "application/yaml"},
 		ConsumesMediaTypes: []string{"application/json", "application/yaml"},
-		Schemes:            []string{"http", "https"},
+		Schemes:            []string{"http"},
 		Params:             params,
 		Reader:             &GetLogEntryByIndexReader{formats: a.formats},
 		Context:            params.Context,
@@ -139,7 +139,7 @@ func (a *Client) GetLogEntryByUUID(params *GetLogEntryByUUIDParams) (*GetLogEntr
 		PathPattern:        "/api/v1/log/entries/{entryUUID}",
 		ProducesMediaTypes: []string{"application/json;q=1", "application/yaml"},
 		ConsumesMediaTypes: []string{"application/json", "application/yaml"},
-		Schemes:            []string{"http", "https"},
+		Schemes:            []string{"http"},
 		Params:             params,
 		Reader:             &GetLogEntryByUUIDReader{formats: a.formats},
 		Context:            params.Context,
@@ -174,7 +174,7 @@ func (a *Client) GetLogEntryProof(params *GetLogEntryProofParams) (*GetLogEntryP
 		PathPattern:        "/api/v1/log/entries/{entryUUID}/proof",
 		ProducesMediaTypes: []string{"application/json;q=1", "application/yaml"},
 		ConsumesMediaTypes: []string{"application/json", "application/yaml"},
-		Schemes:            []string{"http", "https"},
+		Schemes:            []string{"http"},
 		Params:             params,
 		Reader:             &GetLogEntryProofReader{formats: a.formats},
 		Context:            params.Context,
@@ -207,7 +207,7 @@ func (a *Client) SearchLogQuery(params *SearchLogQueryParams) (*SearchLogQueryOK
 		PathPattern:        "/api/v1/log/entries/retrieve",
 		ProducesMediaTypes: []string{"application/json;q=1", "application/yaml"},
 		ConsumesMediaTypes: []string{"application/json", "application/yaml"},
-		Schemes:            []string{"http", "https"},
+		Schemes:            []string{"http"},
 		Params:             params,
 		Reader:             &SearchLogQueryReader{formats: a.formats},
 		Context:            params.Context,
diff --git a/pkg/generated/client/index/index_client.go b/pkg/generated/client/index/index_client.go
new file mode 100644
index 0000000..f096243
--- /dev/null
+++ b/pkg/generated/client/index/index_client.go
@@ -0,0 +1,86 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+// /*
+// Copyright The Rekor 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 index
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"github.com/go-openapi/runtime"
+	"github.com/go-openapi/strfmt"
+)
+
+// New creates a new index API client.
+func New(transport runtime.ClientTransport, formats strfmt.Registry) ClientService {
+	return &Client{transport: transport, formats: formats}
+}
+
+/*
+Client for index API
+*/
+type Client struct {
+	transport runtime.ClientTransport
+	formats   strfmt.Registry
+}
+
+// ClientService is the interface for Client methods
+type ClientService interface {
+	SearchIndex(params *SearchIndexParams) (*SearchIndexOK, error)
+
+	SetTransport(transport runtime.ClientTransport)
+}
+
+/*
+  SearchIndex searches index by entry metadata
+*/
+func (a *Client) SearchIndex(params *SearchIndexParams) (*SearchIndexOK, error) {
+	// TODO: Validate the params before sending
+	if params == nil {
+		params = NewSearchIndexParams()
+	}
+
+	result, err := a.transport.Submit(&runtime.ClientOperation{
+		ID:                 "searchIndex",
+		Method:             "POST",
+		PathPattern:        "/api/v1/index/retrieve",
+		ProducesMediaTypes: []string{"application/json;q=1", "application/yaml"},
+		ConsumesMediaTypes: []string{"application/json", "application/yaml"},
+		Schemes:            []string{"http"},
+		Params:             params,
+		Reader:             &SearchIndexReader{formats: a.formats},
+		Context:            params.Context,
+		Client:             params.HTTPClient,
+	})
+	if err != nil {
+		return nil, err
+	}
+	success, ok := result.(*SearchIndexOK)
+	if ok {
+		return success, nil
+	}
+	// unexpected success response
+	unexpectedSuccess := result.(*SearchIndexDefault)
+	return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code())
+}
+
+// SetTransport changes the transport on the client
+func (a *Client) SetTransport(transport runtime.ClientTransport) {
+	a.transport = transport
+}
diff --git a/pkg/generated/client/index/search_index_parameters.go b/pkg/generated/client/index/search_index_parameters.go
new file mode 100644
index 0000000..ca2e075
--- /dev/null
+++ b/pkg/generated/client/index/search_index_parameters.go
@@ -0,0 +1,152 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+// /*
+// Copyright The Rekor 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 index
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"context"
+	"net/http"
+	"time"
+
+	"github.com/go-openapi/errors"
+	"github.com/go-openapi/runtime"
+	cr "github.com/go-openapi/runtime/client"
+	"github.com/go-openapi/strfmt"
+
+	"github.com/projectrekor/rekor/pkg/generated/models"
+)
+
+// NewSearchIndexParams creates a new SearchIndexParams object
+// with the default values initialized.
+func NewSearchIndexParams() *SearchIndexParams {
+	var ()
+	return &SearchIndexParams{
+
+		timeout: cr.DefaultTimeout,
+	}
+}
+
+// NewSearchIndexParamsWithTimeout creates a new SearchIndexParams object
+// with the default values initialized, and the ability to set a timeout on a request
+func NewSearchIndexParamsWithTimeout(timeout time.Duration) *SearchIndexParams {
+	var ()
+	return &SearchIndexParams{
+
+		timeout: timeout,
+	}
+}
+
+// NewSearchIndexParamsWithContext creates a new SearchIndexParams object
+// with the default values initialized, and the ability to set a context for a request
+func NewSearchIndexParamsWithContext(ctx context.Context) *SearchIndexParams {
+	var ()
+	return &SearchIndexParams{
+
+		Context: ctx,
+	}
+}
+
+// NewSearchIndexParamsWithHTTPClient creates a new SearchIndexParams object
+// with the default values initialized, and the ability to set a custom HTTPClient for a request
+func NewSearchIndexParamsWithHTTPClient(client *http.Client) *SearchIndexParams {
+	var ()
+	return &SearchIndexParams{
+		HTTPClient: client,
+	}
+}
+
+/*SearchIndexParams contains all the parameters to send to the API endpoint
+for the search index operation typically these are written to a http.Request
+*/
+type SearchIndexParams struct {
+
+	/*Query*/
+	Query *models.SearchIndex
+
+	timeout    time.Duration
+	Context    context.Context
+	HTTPClient *http.Client
+}
+
+// WithTimeout adds the timeout to the search index params
+func (o *SearchIndexParams) WithTimeout(timeout time.Duration) *SearchIndexParams {
+	o.SetTimeout(timeout)
+	return o
+}
+
+// SetTimeout adds the timeout to the search index params
+func (o *SearchIndexParams) SetTimeout(timeout time.Duration) {
+	o.timeout = timeout
+}
+
+// WithContext adds the context to the search index params
+func (o *SearchIndexParams) WithContext(ctx context.Context) *SearchIndexParams {
+	o.SetContext(ctx)
+	return o
+}
+
+// SetContext adds the context to the search index params
+func (o *SearchIndexParams) SetContext(ctx context.Context) {
+	o.Context = ctx
+}
+
+// WithHTTPClient adds the HTTPClient to the search index params
+func (o *SearchIndexParams) WithHTTPClient(client *http.Client) *SearchIndexParams {
+	o.SetHTTPClient(client)
+	return o
+}
+
+// SetHTTPClient adds the HTTPClient to the search index params
+func (o *SearchIndexParams) SetHTTPClient(client *http.Client) {
+	o.HTTPClient = client
+}
+
+// WithQuery adds the query to the search index params
+func (o *SearchIndexParams) WithQuery(query *models.SearchIndex) *SearchIndexParams {
+	o.SetQuery(query)
+	return o
+}
+
+// SetQuery adds the query to the search index params
+func (o *SearchIndexParams) SetQuery(query *models.SearchIndex) {
+	o.Query = query
+}
+
+// WriteToRequest writes these params to a swagger request
+func (o *SearchIndexParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {
+
+	if err := r.SetTimeout(o.timeout); err != nil {
+		return err
+	}
+	var res []error
+
+	if o.Query != nil {
+		if err := r.SetBodyParam(o.Query); err != nil {
+			return err
+		}
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
diff --git a/pkg/generated/client/index/search_index_responses.go b/pkg/generated/client/index/search_index_responses.go
new file mode 100644
index 0000000..dc6eed2
--- /dev/null
+++ b/pkg/generated/client/index/search_index_responses.go
@@ -0,0 +1,171 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+// /*
+// Copyright The Rekor 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 index
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"fmt"
+	"io"
+
+	"github.com/go-openapi/runtime"
+	"github.com/go-openapi/strfmt"
+
+	"github.com/projectrekor/rekor/pkg/generated/models"
+)
+
+// SearchIndexReader is a Reader for the SearchIndex structure.
+type SearchIndexReader struct {
+	formats strfmt.Registry
+}
+
+// ReadResponse reads a server response into the received o.
+func (o *SearchIndexReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
+	switch response.Code() {
+	case 200:
+		result := NewSearchIndexOK()
+		if err := result.readResponse(response, consumer, o.formats); err != nil {
+			return nil, err
+		}
+		return result, nil
+	case 400:
+		result := NewSearchIndexBadRequest()
+		if err := result.readResponse(response, consumer, o.formats); err != nil {
+			return nil, err
+		}
+		return nil, result
+	default:
+		result := NewSearchIndexDefault(response.Code())
+		if err := result.readResponse(response, consumer, o.formats); err != nil {
+			return nil, err
+		}
+		if response.Code()/100 == 2 {
+			return result, nil
+		}
+		return nil, result
+	}
+}
+
+// NewSearchIndexOK creates a SearchIndexOK with default headers values
+func NewSearchIndexOK() *SearchIndexOK {
+	return &SearchIndexOK{}
+}
+
+/*SearchIndexOK handles this case with default header values.
+
+Returns zero or more entry UUIDs from the transparency log based on search query
+*/
+type SearchIndexOK struct {
+	Payload []string
+}
+
+func (o *SearchIndexOK) Error() string {
+	return fmt.Sprintf("[POST /api/v1/index/retrieve][%d] searchIndexOK  %+v", 200, o.Payload)
+}
+
+func (o *SearchIndexOK) GetPayload() []string {
+	return o.Payload
+}
+
+func (o *SearchIndexOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
+
+	// response payload
+	if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF {
+		return err
+	}
+
+	return nil
+}
+
+// NewSearchIndexBadRequest creates a SearchIndexBadRequest with default headers values
+func NewSearchIndexBadRequest() *SearchIndexBadRequest {
+	return &SearchIndexBadRequest{}
+}
+
+/*SearchIndexBadRequest handles this case with default header values.
+
+The content supplied to the server was invalid
+*/
+type SearchIndexBadRequest struct {
+	Payload *models.Error
+}
+
+func (o *SearchIndexBadRequest) Error() string {
+	return fmt.Sprintf("[POST /api/v1/index/retrieve][%d] searchIndexBadRequest  %+v", 400, o.Payload)
+}
+
+func (o *SearchIndexBadRequest) GetPayload() *models.Error {
+	return o.Payload
+}
+
+func (o *SearchIndexBadRequest) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
+
+	o.Payload = new(models.Error)
+
+	// response payload
+	if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
+		return err
+	}
+
+	return nil
+}
+
+// NewSearchIndexDefault creates a SearchIndexDefault with default headers values
+func NewSearchIndexDefault(code int) *SearchIndexDefault {
+	return &SearchIndexDefault{
+		_statusCode: code,
+	}
+}
+
+/*SearchIndexDefault handles this case with default header values.
+
+There was an internal error in the server while processing the request
+*/
+type SearchIndexDefault struct {
+	_statusCode int
+
+	Payload *models.Error
+}
+
+// Code gets the status code for the search index default response
+func (o *SearchIndexDefault) Code() int {
+	return o._statusCode
+}
+
+func (o *SearchIndexDefault) Error() string {
+	return fmt.Sprintf("[POST /api/v1/index/retrieve][%d] searchIndex default  %+v", o._statusCode, o.Payload)
+}
+
+func (o *SearchIndexDefault) GetPayload() *models.Error {
+	return o.Payload
+}
+
+func (o *SearchIndexDefault) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
+
+	o.Payload = new(models.Error)
+
+	// response payload
+	if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
+		return err
+	}
+
+	return nil
+}
diff --git a/pkg/generated/client/rekor_client.go b/pkg/generated/client/rekor_client.go
index 458dc49..721331a 100644
--- a/pkg/generated/client/rekor_client.go
+++ b/pkg/generated/client/rekor_client.go
@@ -28,6 +28,7 @@ import (
 	"github.com/go-openapi/strfmt"
 
 	"github.com/projectrekor/rekor/pkg/generated/client/entries"
+	"github.com/projectrekor/rekor/pkg/generated/client/index"
 	"github.com/projectrekor/rekor/pkg/generated/client/tlog"
 )
 
@@ -44,7 +45,7 @@ const (
 )
 
 // DefaultSchemes are the default schemes found in Meta (info) section of spec file
-var DefaultSchemes = []string{"http", "https"}
+var DefaultSchemes = []string{"http"}
 
 // NewHTTPClient creates a new rekor HTTP client.
 func NewHTTPClient(formats strfmt.Registry) *Rekor {
@@ -74,6 +75,7 @@ func New(transport runtime.ClientTransport, formats strfmt.Registry) *Rekor {
 	cli := new(Rekor)
 	cli.Transport = transport
 	cli.Entries = entries.New(transport, formats)
+	cli.Index = index.New(transport, formats)
 	cli.Tlog = tlog.New(transport, formats)
 	return cli
 }
@@ -121,6 +123,8 @@ func (cfg *TransportConfig) WithSchemes(schemes []string) *TransportConfig {
 type Rekor struct {
 	Entries entries.ClientService
 
+	Index index.ClientService
+
 	Tlog tlog.ClientService
 
 	Transport runtime.ClientTransport
@@ -130,5 +134,6 @@ type Rekor struct {
 func (c *Rekor) SetTransport(transport runtime.ClientTransport) {
 	c.Transport = transport
 	c.Entries.SetTransport(transport)
+	c.Index.SetTransport(transport)
 	c.Tlog.SetTransport(transport)
 }
diff --git a/pkg/generated/client/tlog/tlog_client.go b/pkg/generated/client/tlog/tlog_client.go
index f758d72..e9dc37a 100644
--- a/pkg/generated/client/tlog/tlog_client.go
+++ b/pkg/generated/client/tlog/tlog_client.go
@@ -68,7 +68,7 @@ func (a *Client) GetLogInfo(params *GetLogInfoParams) (*GetLogInfoOK, error) {
 		PathPattern:        "/api/v1/log",
 		ProducesMediaTypes: []string{"application/json;q=1", "application/yaml"},
 		ConsumesMediaTypes: []string{"application/json", "application/yaml"},
-		Schemes:            []string{"http", "https"},
+		Schemes:            []string{"http"},
 		Params:             params,
 		Reader:             &GetLogInfoReader{formats: a.formats},
 		Context:            params.Context,
@@ -103,7 +103,7 @@ func (a *Client) GetLogProof(params *GetLogProofParams) (*GetLogProofOK, error)
 		PathPattern:        "/api/v1/log/proof",
 		ProducesMediaTypes: []string{"application/json;q=1", "application/yaml"},
 		ConsumesMediaTypes: []string{"application/json", "application/yaml"},
-		Schemes:            []string{"http", "https"},
+		Schemes:            []string{"http"},
 		Params:             params,
 		Reader:             &GetLogProofReader{formats: a.formats},
 		Context:            params.Context,
@@ -138,7 +138,7 @@ func (a *Client) GetPublicKey(params *GetPublicKeyParams) (*GetPublicKeyOK, erro
 		PathPattern:        "/api/v1/log/publicKey",
 		ProducesMediaTypes: []string{"application/x-pem-file"},
 		ConsumesMediaTypes: []string{"application/json", "application/yaml"},
-		Schemes:            []string{"http", "https"},
+		Schemes:            []string{"http"},
 		Params:             params,
 		Reader:             &GetPublicKeyReader{formats: a.formats},
 		Context:            params.Context,
diff --git a/pkg/generated/models/search_index.go b/pkg/generated/models/search_index.go
new file mode 100644
index 0000000..f0e2519
--- /dev/null
+++ b/pkg/generated/models/search_index.go
@@ -0,0 +1,226 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+// /*
+// Copyright The Rekor Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// */
+//
+
+package models
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"encoding/json"
+
+	"github.com/go-openapi/errors"
+	"github.com/go-openapi/strfmt"
+	"github.com/go-openapi/swag"
+	"github.com/go-openapi/validate"
+)
+
+// SearchIndex search index
+//
+// swagger:model SearchIndex
+type SearchIndex struct {
+
+	// hash
+	// Pattern: ^[0-9a-fA-F]{64}$
+	Hash string `json:"hash,omitempty"`
+
+	// public key
+	PublicKey *SearchIndexPublicKey `json:"publicKey,omitempty"`
+}
+
+// Validate validates this search index
+func (m *SearchIndex) Validate(formats strfmt.Registry) error {
+	var res []error
+
+	if err := m.validateHash(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if err := m.validatePublicKey(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+func (m *SearchIndex) validateHash(formats strfmt.Registry) error {
+
+	if swag.IsZero(m.Hash) { // not required
+		return nil
+	}
+
+	if err := validate.Pattern("hash", "body", string(m.Hash), `^[0-9a-fA-F]{64}$`); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (m *SearchIndex) validatePublicKey(formats strfmt.Registry) error {
+
+	if swag.IsZero(m.PublicKey) { // not required
+		return nil
+	}
+
+	if m.PublicKey != nil {
+		if err := m.PublicKey.Validate(formats); err != nil {
+			if ve, ok := err.(*errors.Validation); ok {
+				return ve.ValidateName("publicKey")
+			}
+			return err
+		}
+	}
+
+	return nil
+}
+
+// MarshalBinary interface implementation
+func (m *SearchIndex) MarshalBinary() ([]byte, error) {
+	if m == nil {
+		return nil, nil
+	}
+	return swag.WriteJSON(m)
+}
+
+// UnmarshalBinary interface implementation
+func (m *SearchIndex) UnmarshalBinary(b []byte) error {
+	var res SearchIndex
+	if err := swag.ReadJSON(b, &res); err != nil {
+		return err
+	}
+	*m = res
+	return nil
+}
+
+// SearchIndexPublicKey search index public key
+//
+// swagger:model SearchIndexPublicKey
+type SearchIndexPublicKey struct {
+
+	// content
+	// Format: byte
+	Content strfmt.Base64 `json:"content,omitempty"`
+
+	// format
+	// Required: true
+	// Enum: [pgp x509 minisign]
+	Format *string `json:"format"`
+
+	// url
+	// Format: uri
+	URL strfmt.URI `json:"url,omitempty"`
+}
+
+// Validate validates this search index public key
+func (m *SearchIndexPublicKey) Validate(formats strfmt.Registry) error {
+	var res []error
+
+	if err := m.validateFormat(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if err := m.validateURL(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+var searchIndexPublicKeyTypeFormatPropEnum []interface{}
+
+func init() {
+	var res []string
+	if err := json.Unmarshal([]byte(`["pgp","x509","minisign"]`), &res); err != nil {
+		panic(err)
+	}
+	for _, v := range res {
+		searchIndexPublicKeyTypeFormatPropEnum = append(searchIndexPublicKeyTypeFormatPropEnum, v)
+	}
+}
+
+const (
+
+	// SearchIndexPublicKeyFormatPgp captures enum value "pgp"
+	SearchIndexPublicKeyFormatPgp string = "pgp"
+
+	// SearchIndexPublicKeyFormatX509 captures enum value "x509"
+	SearchIndexPublicKeyFormatX509 string = "x509"
+
+	// SearchIndexPublicKeyFormatMinisign captures enum value "minisign"
+	SearchIndexPublicKeyFormatMinisign string = "minisign"
+)
+
+// prop value enum
+func (m *SearchIndexPublicKey) validateFormatEnum(path, location string, value string) error {
+	if err := validate.EnumCase(path, location, value, searchIndexPublicKeyTypeFormatPropEnum, true); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (m *SearchIndexPublicKey) validateFormat(formats strfmt.Registry) error {
+
+	if err := validate.Required("publicKey"+"."+"format", "body", m.Format); err != nil {
+		return err
+	}
+
+	// value enum
+	if err := m.validateFormatEnum("publicKey"+"."+"format", "body", *m.Format); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (m *SearchIndexPublicKey) validateURL(formats strfmt.Registry) error {
+
+	if swag.IsZero(m.URL) { // not required
+		return nil
+	}
+
+	if err := validate.FormatOf("publicKey"+"."+"url", "body", "uri", m.URL.String(), formats); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// MarshalBinary interface implementation
+func (m *SearchIndexPublicKey) MarshalBinary() ([]byte, error) {
+	if m == nil {
+		return nil, nil
+	}
+	return swag.WriteJSON(m)
+}
+
+// UnmarshalBinary interface implementation
+func (m *SearchIndexPublicKey) UnmarshalBinary(b []byte) error {
+	var res SearchIndexPublicKey
+	if err := swag.ReadJSON(b, &res); err != nil {
+		return err
+	}
+	*m = res
+	return nil
+}
diff --git a/pkg/generated/restapi/configure_rekor_server.go b/pkg/generated/restapi/configure_rekor_server.go
index 9ea5647..84fde0e 100644
--- a/pkg/generated/restapi/configure_rekor_server.go
+++ b/pkg/generated/restapi/configure_rekor_server.go
@@ -26,10 +26,12 @@ import (
 	"github.com/go-openapi/runtime"
 	"github.com/mitchellh/mapstructure"
 	"github.com/rs/cors"
+	"github.com/spf13/viper"
 
 	pkgapi "github.com/projectrekor/rekor/pkg/api"
 	"github.com/projectrekor/rekor/pkg/generated/restapi/operations"
 	"github.com/projectrekor/rekor/pkg/generated/restapi/operations/entries"
+	"github.com/projectrekor/rekor/pkg/generated/restapi/operations/index"
 	"github.com/projectrekor/rekor/pkg/generated/restapi/operations/tlog"
 	"github.com/projectrekor/rekor/pkg/log"
 	"github.com/projectrekor/rekor/pkg/util"
@@ -76,6 +78,12 @@ func configureAPI(api *operations.RekorServerAPI) http.Handler {
 	api.TlogGetLogProofHandler = tlog.GetLogProofHandlerFunc(pkgapi.GetLogProofHandler)
 	api.TlogGetPublicKeyHandler = tlog.GetPublicKeyHandlerFunc(pkgapi.GetPublicKeyHandler)
 
+	if viper.GetBool("enable_retrieve_api") {
+		api.IndexSearchIndexHandler = index.SearchIndexHandlerFunc(pkgapi.SearchIndexHandler)
+	} else {
+		api.IndexSearchIndexHandler = index.SearchIndexHandlerFunc(pkgapi.SearchIndexNotImplementedHandler)
+	}
+
 	api.PreServerShutdown = func() {}
 
 	api.ServerShutdown = func() {}
diff --git a/pkg/generated/restapi/doc.go b/pkg/generated/restapi/doc.go
index 93a1827..99ed0fd 100644
--- a/pkg/generated/restapi/doc.go
+++ b/pkg/generated/restapi/doc.go
@@ -21,7 +21,6 @@
 //  Rekor is a cryptographically secure, immutable transparency log for signed software releases.
 //  Schemes:
 //    http
-//    https
 //  Host: api.rekor.dev
 //  BasePath: /
 //  Version: 0.0.1
diff --git a/pkg/generated/restapi/embedded_spec.go b/pkg/generated/restapi/embedded_spec.go
index 8b669b6..432180c 100644
--- a/pkg/generated/restapi/embedded_spec.go
+++ b/pkg/generated/restapi/embedded_spec.go
@@ -44,8 +44,7 @@ func init() {
     "application/yaml"
   ],
   "schemes": [
-    "http",
-    "https"
+    "http"
   ],
   "swagger": "2.0",
   "info": {
@@ -55,6 +54,44 @@ func init() {
   },
   "host": "api.rekor.dev",
   "paths": {
+    "/api/v1/index/retrieve": {
+      "post": {
+        "tags": [
+          "index"
+        ],
+        "summary": "Searches index by entry metadata",
+        "operationId": "searchIndex",
+        "parameters": [
+          {
+            "name": "query",
+            "in": "body",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/SearchIndex"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Returns zero or more entry UUIDs from the transparency log based on search query",
+            "schema": {
+              "type": "array",
+              "items": {
+                "description": "Entry UUID in transparency log",
+                "type": "string",
+                "pattern": "^[0-9a-fA-F]{64}$"
+              }
+            }
+          },
+          "400": {
+            "$ref": "#/responses/BadContent"
+          },
+          "default": {
+            "$ref": "#/responses/InternalServerError"
+          }
+        }
+      }
+    },
     "/api/v1/log": {
       "get": {
         "description": "Returns the current root hash and size of the merkle tree used to store the log entries.",
@@ -468,6 +505,39 @@ func init() {
       },
       "discriminator": "kind"
     },
+    "SearchIndex": {
+      "type": "object",
+      "properties": {
+        "hash": {
+          "type": "string",
+          "pattern": "^[0-9a-fA-F]{64}$"
+        },
+        "publicKey": {
+          "type": "object",
+          "required": [
+            "format"
+          ],
+          "properties": {
+            "content": {
+              "type": "string",
+              "format": "byte"
+            },
+            "format": {
+              "type": "string",
+              "enum": [
+                "pgp",
+                "x509",
+                "minisign"
+              ]
+            },
+            "url": {
+              "type": "string",
+              "format": "uri"
+            }
+          }
+        }
+      }
+    },
     "SearchLogQuery": {
       "type": "object",
       "properties": {
@@ -556,8 +626,7 @@ func init() {
     "application/yaml"
   ],
   "schemes": [
-    "http",
-    "https"
+    "http"
   ],
   "swagger": "2.0",
   "info": {
@@ -567,6 +636,50 @@ func init() {
   },
   "host": "api.rekor.dev",
   "paths": {
+    "/api/v1/index/retrieve": {
+      "post": {
+        "tags": [
+          "index"
+        ],
+        "summary": "Searches index by entry metadata",
+        "operationId": "searchIndex",
+        "parameters": [
+          {
+            "name": "query",
+            "in": "body",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/SearchIndex"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Returns zero or more entry UUIDs from the transparency log based on search query",
+            "schema": {
+              "type": "array",
+              "items": {
+                "description": "Entry UUID in transparency log",
+                "type": "string",
+                "pattern": "^[0-9a-fA-F]{64}$"
+              }
+            }
+          },
+          "400": {
+            "description": "The content supplied to the server was invalid",
+            "schema": {
+              "$ref": "#/definitions/Error"
+            }
+          },
+          "default": {
+            "description": "There was an internal error in the server while processing the request",
+            "schema": {
+              "$ref": "#/definitions/Error"
+            }
+          }
+        }
+      }
+    },
     "/api/v1/log": {
       "get": {
         "description": "Returns the current root hash and size of the merkle tree used to store the log entries.",
@@ -1216,6 +1329,63 @@ func init() {
         }
       }
     },
+    "SearchIndex": {
+      "type": "object",
+      "properties": {
+        "hash": {
+          "type": "string",
+          "pattern": "^[0-9a-fA-F]{64}$"
+        },
+        "publicKey": {
+          "type": "object",
+          "required": [
+            "format"
+          ],
+          "properties": {
+            "content": {
+              "type": "string",
+              "format": "byte"
+            },
+            "format": {
+              "type": "string",
+              "enum": [
+                "pgp",
+                "x509",
+                "minisign"
+              ]
+            },
+            "url": {
+              "type": "string",
+              "format": "uri"
+            }
+          }
+        }
+      }
+    },
+    "SearchIndexPublicKey": {
+      "type": "object",
+      "required": [
+        "format"
+      ],
+      "properties": {
+        "content": {
+          "type": "string",
+          "format": "byte"
+        },
+        "format": {
+          "type": "string",
+          "enum": [
+            "pgp",
+            "x509",
+            "minisign"
+          ]
+        },
+        "url": {
+          "type": "string",
+          "format": "uri"
+        }
+      }
+    },
     "SearchLogQuery": {
       "type": "object",
       "properties": {
diff --git a/pkg/generated/restapi/operations/index/search_index.go b/pkg/generated/restapi/operations/index/search_index.go
new file mode 100644
index 0000000..8a2d7d4
--- /dev/null
+++ b/pkg/generated/restapi/operations/index/search_index.go
@@ -0,0 +1,75 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+// /*
+// Copyright The Rekor 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 index
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the generate command
+
+import (
+	"net/http"
+
+	"github.com/go-openapi/runtime/middleware"
+)
+
+// SearchIndexHandlerFunc turns a function with the right signature into a search index handler
+type SearchIndexHandlerFunc func(SearchIndexParams) middleware.Responder
+
+// Handle executing the request and returning a response
+func (fn SearchIndexHandlerFunc) Handle(params SearchIndexParams) middleware.Responder {
+	return fn(params)
+}
+
+// SearchIndexHandler interface for that can handle valid search index params
+type SearchIndexHandler interface {
+	Handle(SearchIndexParams) middleware.Responder
+}
+
+// NewSearchIndex creates a new http.Handler for the search index operation
+func NewSearchIndex(ctx *middleware.Context, handler SearchIndexHandler) *SearchIndex {
+	return &SearchIndex{Context: ctx, Handler: handler}
+}
+
+/*SearchIndex swagger:route POST /api/v1/index/retrieve index searchIndex
+
+Searches index by entry metadata
+
+*/
+type SearchIndex struct {
+	Context *middleware.Context
+	Handler SearchIndexHandler
+}
+
+func (o *SearchIndex) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
+	route, rCtx, _ := o.Context.RouteInfo(r)
+	if rCtx != nil {
+		r = rCtx
+	}
+	var Params = NewSearchIndexParams()
+
+	if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
+		o.Context.Respond(rw, r, route.Produces, route, err)
+		return
+	}
+
+	res := o.Handler.Handle(Params) // actually handle the request
+
+	o.Context.Respond(rw, r, route.Produces, route, res)
+
+}
diff --git a/pkg/generated/restapi/operations/index/search_index_parameters.go b/pkg/generated/restapi/operations/index/search_index_parameters.go
new file mode 100644
index 0000000..a186fde
--- /dev/null
+++ b/pkg/generated/restapi/operations/index/search_index_parameters.go
@@ -0,0 +1,94 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+// /*
+// Copyright The Rekor 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 index
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"io"
+	"net/http"
+
+	"github.com/go-openapi/errors"
+	"github.com/go-openapi/runtime"
+	"github.com/go-openapi/runtime/middleware"
+
+	"github.com/projectrekor/rekor/pkg/generated/models"
+)
+
+// NewSearchIndexParams creates a new SearchIndexParams object
+// no default values defined in spec.
+func NewSearchIndexParams() SearchIndexParams {
+
+	return SearchIndexParams{}
+}
+
+// SearchIndexParams contains all the bound params for the search index operation
+// typically these are obtained from a http.Request
+//
+// swagger:parameters searchIndex
+type SearchIndexParams struct {
+
+	// HTTP Request Object
+	HTTPRequest *http.Request `json:"-"`
+
+	/*
+	  Required: true
+	  In: body
+	*/
+	Query *models.SearchIndex
+}
+
+// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
+// for simple values it will use straight method calls.
+//
+// To ensure default values, the struct must have been initialized with NewSearchIndexParams() beforehand.
+func (o *SearchIndexParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
+	var res []error
+
+	o.HTTPRequest = r
+
+	if runtime.HasBody(r) {
+		defer r.Body.Close()
+		var body models.SearchIndex
+		if err := route.Consumer.Consume(r.Body, &body); err != nil {
+			if err == io.EOF {
+				res = append(res, errors.Required("query", "body", ""))
+			} else {
+				res = append(res, errors.NewParseError("query", "body", "", err))
+			}
+		} else {
+			// validate body object
+			if err := body.Validate(route.Formats); err != nil {
+				res = append(res, err)
+			}
+
+			if len(res) == 0 {
+				o.Query = &body
+			}
+		}
+	} else {
+		res = append(res, errors.Required("query", "body", ""))
+	}
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
diff --git a/pkg/generated/restapi/operations/index/search_index_responses.go b/pkg/generated/restapi/operations/index/search_index_responses.go
new file mode 100644
index 0000000..f9224f7
--- /dev/null
+++ b/pkg/generated/restapi/operations/index/search_index_responses.go
@@ -0,0 +1,180 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+// /*
+// Copyright The Rekor 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 index
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"net/http"
+
+	"github.com/go-openapi/runtime"
+
+	"github.com/projectrekor/rekor/pkg/generated/models"
+)
+
+// SearchIndexOKCode is the HTTP code returned for type SearchIndexOK
+const SearchIndexOKCode int = 200
+
+/*SearchIndexOK Returns zero or more entry UUIDs from the transparency log based on search query
+
+swagger:response searchIndexOK
+*/
+type SearchIndexOK struct {
+
+	/*
+	  In: Body
+	*/
+	Payload []string `json:"body,omitempty"`
+}
+
+// NewSearchIndexOK creates SearchIndexOK with default headers values
+func NewSearchIndexOK() *SearchIndexOK {
+
+	return &SearchIndexOK{}
+}
+
+// WithPayload adds the payload to the search index o k response
+func (o *SearchIndexOK) WithPayload(payload []string) *SearchIndexOK {
+	o.Payload = payload
+	return o
+}
+
+// SetPayload sets the payload to the search index o k response
+func (o *SearchIndexOK) SetPayload(payload []string) {
+	o.Payload = payload
+}
+
+// WriteResponse to the client
+func (o *SearchIndexOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
+
+	rw.WriteHeader(200)
+	payload := o.Payload
+	if payload == nil {
+		// return empty array
+		payload = make([]string, 0, 50)
+	}
+
+	if err := producer.Produce(rw, payload); err != nil {
+		panic(err) // let the recovery middleware deal with this
+	}
+}
+
+// SearchIndexBadRequestCode is the HTTP code returned for type SearchIndexBadRequest
+const SearchIndexBadRequestCode int = 400
+
+/*SearchIndexBadRequest The content supplied to the server was invalid
+
+swagger:response searchIndexBadRequest
+*/
+type SearchIndexBadRequest struct {
+
+	/*
+	  In: Body
+	*/
+	Payload *models.Error `json:"body,omitempty"`
+}
+
+// NewSearchIndexBadRequest creates SearchIndexBadRequest with default headers values
+func NewSearchIndexBadRequest() *SearchIndexBadRequest {
+
+	return &SearchIndexBadRequest{}
+}
+
+// WithPayload adds the payload to the search index bad request response
+func (o *SearchIndexBadRequest) WithPayload(payload *models.Error) *SearchIndexBadRequest {
+	o.Payload = payload
+	return o
+}
+
+// SetPayload sets the payload to the search index bad request response
+func (o *SearchIndexBadRequest) SetPayload(payload *models.Error) {
+	o.Payload = payload
+}
+
+// WriteResponse to the client
+func (o *SearchIndexBadRequest) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
+
+	rw.WriteHeader(400)
+	if o.Payload != nil {
+		payload := o.Payload
+		if err := producer.Produce(rw, payload); err != nil {
+			panic(err) // let the recovery middleware deal with this
+		}
+	}
+}
+
+/*SearchIndexDefault There was an internal error in the server while processing the request
+
+swagger:response searchIndexDefault
+*/
+type SearchIndexDefault struct {
+	_statusCode int
+
+	/*
+	  In: Body
+	*/
+	Payload *models.Error `json:"body,omitempty"`
+}
+
+// NewSearchIndexDefault creates SearchIndexDefault with default headers values
+func NewSearchIndexDefault(code int) *SearchIndexDefault {
+	if code <= 0 {
+		code = 500
+	}
+
+	return &SearchIndexDefault{
+		_statusCode: code,
+	}
+}
+
+// WithStatusCode adds the status to the search index default response
+func (o *SearchIndexDefault) WithStatusCode(code int) *SearchIndexDefault {
+	o._statusCode = code
+	return o
+}
+
+// SetStatusCode sets the status to the search index default response
+func (o *SearchIndexDefault) SetStatusCode(code int) {
+	o._statusCode = code
+}
+
+// WithPayload adds the payload to the search index default response
+func (o *SearchIndexDefault) WithPayload(payload *models.Error) *SearchIndexDefault {
+	o.Payload = payload
+	return o
+}
+
+// SetPayload sets the payload to the search index default response
+func (o *SearchIndexDefault) SetPayload(payload *models.Error) {
+	o.Payload = payload
+}
+
+// WriteResponse to the client
+func (o *SearchIndexDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
+
+	rw.WriteHeader(o._statusCode)
+	if o.Payload != nil {
+		payload := o.Payload
+		if err := producer.Produce(rw, payload); err != nil {
+			panic(err) // let the recovery middleware deal with this
+		}
+	}
+}
diff --git a/pkg/generated/restapi/operations/index/search_index_urlbuilder.go b/pkg/generated/restapi/operations/index/search_index_urlbuilder.go
new file mode 100644
index 0000000..419e816
--- /dev/null
+++ b/pkg/generated/restapi/operations/index/search_index_urlbuilder.go
@@ -0,0 +1,101 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+// /*
+// Copyright The Rekor 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 index
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the generate command
+
+import (
+	"errors"
+	"net/url"
+	golangswaggerpaths "path"
+)
+
+// SearchIndexURL generates an URL for the search index operation
+type SearchIndexURL struct {
+	_basePath string
+}
+
+// WithBasePath sets the base path for this url builder, only required when it's different from the
+// base path specified in the swagger spec.
+// When the value of the base path is an empty string
+func (o *SearchIndexURL) WithBasePath(bp string) *SearchIndexURL {
+	o.SetBasePath(bp)
+	return o
+}
+
+// SetBasePath sets the base path for this url builder, only required when it's different from the
+// base path specified in the swagger spec.
+// When the value of the base path is an empty string
+func (o *SearchIndexURL) SetBasePath(bp string) {
+	o._basePath = bp
+}
+
+// Build a url path and query string
+func (o *SearchIndexURL) Build() (*url.URL, error) {
+	var _result url.URL
+
+	var _path = "/api/v1/index/retrieve"
+
+	_basePath := o._basePath
+	_result.Path = golangswaggerpaths.Join(_basePath, _path)
+
+	return &_result, nil
+}
+
+// Must is a helper function to panic when the url builder returns an error
+func (o *SearchIndexURL) Must(u *url.URL, err error) *url.URL {
+	if err != nil {
+		panic(err)
+	}
+	if u == nil {
+		panic("url can't be nil")
+	}
+	return u
+}
+
+// String returns the string representation of the path with query string
+func (o *SearchIndexURL) String() string {
+	return o.Must(o.Build()).String()
+}
+
+// BuildFull builds a full url with scheme, host, path and query string
+func (o *SearchIndexURL) BuildFull(scheme, host string) (*url.URL, error) {
+	if scheme == "" {
+		return nil, errors.New("scheme is required for a full url on SearchIndexURL")
+	}
+	if host == "" {
+		return nil, errors.New("host is required for a full url on SearchIndexURL")
+	}
+
+	base, err := o.Build()
+	if err != nil {
+		return nil, err
+	}
+
+	base.Scheme = scheme
+	base.Host = host
+	return base, nil
+}
+
+// StringFull returns the string representation of a complete url
+func (o *SearchIndexURL) StringFull(scheme, host string) string {
+	return o.Must(o.BuildFull(scheme, host)).String()
+}
diff --git a/pkg/generated/restapi/operations/rekor_server_api.go b/pkg/generated/restapi/operations/rekor_server_api.go
index a25bc27..8cfc86e 100644
--- a/pkg/generated/restapi/operations/rekor_server_api.go
+++ b/pkg/generated/restapi/operations/rekor_server_api.go
@@ -39,6 +39,7 @@ import (
 	"github.com/go-openapi/swag"
 
 	"github.com/projectrekor/rekor/pkg/generated/restapi/operations/entries"
+	"github.com/projectrekor/rekor/pkg/generated/restapi/operations/index"
 	"github.com/projectrekor/rekor/pkg/generated/restapi/operations/tlog"
 )
 
@@ -90,6 +91,9 @@ func NewRekorServerAPI(spec *loads.Document) *RekorServerAPI {
 		TlogGetPublicKeyHandler: tlog.GetPublicKeyHandlerFunc(func(params tlog.GetPublicKeyParams) middleware.Responder {
 			return middleware.NotImplemented("operation tlog.GetPublicKey has not yet been implemented")
 		}),
+		IndexSearchIndexHandler: index.SearchIndexHandlerFunc(func(params index.SearchIndexParams) middleware.Responder {
+			return middleware.NotImplemented("operation index.SearchIndex has not yet been implemented")
+		}),
 		EntriesSearchLogQueryHandler: entries.SearchLogQueryHandlerFunc(func(params entries.SearchLogQueryParams) middleware.Responder {
 			return middleware.NotImplemented("operation entries.SearchLogQuery has not yet been implemented")
 		}),
@@ -150,6 +154,8 @@ type RekorServerAPI struct {
 	TlogGetLogProofHandler tlog.GetLogProofHandler
 	// TlogGetPublicKeyHandler sets the operation handler for the get public key operation
 	TlogGetPublicKeyHandler tlog.GetPublicKeyHandler
+	// IndexSearchIndexHandler sets the operation handler for the search index operation
+	IndexSearchIndexHandler index.SearchIndexHandler
 	// EntriesSearchLogQueryHandler sets the operation handler for the search log query operation
 	EntriesSearchLogQueryHandler entries.SearchLogQueryHandler
 	// ServeError is called when an error is received, there is a default handler
@@ -258,6 +264,9 @@ func (o *RekorServerAPI) Validate() error {
 	if o.TlogGetPublicKeyHandler == nil {
 		unregistered = append(unregistered, "tlog.GetPublicKeyHandler")
 	}
+	if o.IndexSearchIndexHandler == nil {
+		unregistered = append(unregistered, "index.SearchIndexHandler")
+	}
 	if o.EntriesSearchLogQueryHandler == nil {
 		unregistered = append(unregistered, "entries.SearchLogQueryHandler")
 	}
@@ -386,6 +395,10 @@ func (o *RekorServerAPI) initHandlerCache() {
 	if o.handlers["POST"] == nil {
 		o.handlers["POST"] = make(map[string]http.Handler)
 	}
+	o.handlers["POST"]["/api/v1/index/retrieve"] = index.NewSearchIndex(o.context, o.IndexSearchIndexHandler)
+	if o.handlers["POST"] == nil {
+		o.handlers["POST"] = make(map[string]http.Handler)
+	}
 	o.handlers["POST"]["/api/v1/log/entries/retrieve"] = entries.NewSearchLogQuery(o.context, o.EntriesSearchLogQueryHandler)
 }
 
diff --git a/pkg/generated/restapi/server.go b/pkg/generated/restapi/server.go
index cdfa026..c95a38e 100644
--- a/pkg/generated/restapi/server.go
+++ b/pkg/generated/restapi/server.go
@@ -56,7 +56,6 @@ var defaultSchemes []string
 func init() {
 	defaultSchemes = []string{
 		schemeHTTP,
-		schemeHTTPS,
 	}
 }
 
diff --git a/pkg/types/rekord/rekord_test.go b/pkg/types/rekord/rekord_test.go
index 729b300..42ff5f5 100644
--- a/pkg/types/rekord/rekord_test.go
+++ b/pkg/types/rekord/rekord_test.go
@@ -38,6 +38,10 @@ func (u UnmarshalTester) APIVersion() string {
 	return "2.0.1"
 }
 
+func (u UnmarshalTester) IndexKeys() []string {
+	return []string{}
+}
+
 func (u UnmarshalTester) Canonicalize(ctx context.Context) ([]byte, error) {
 	return nil, nil
 }
diff --git a/pkg/types/rekord/v0.0.1/entry.go b/pkg/types/rekord/v0.0.1/entry.go
index 88997ab..3eddc6f 100644
--- a/pkg/types/rekord/v0.0.1/entry.go
+++ b/pkg/types/rekord/v0.0.1/entry.go
@@ -16,9 +16,6 @@ limitations under the License.
 package rekord
 
 import (
-	"bufio"
-	"bytes"
-	"compress/gzip"
 	"context"
 	"crypto/sha256"
 	"encoding/base64"
@@ -27,12 +24,12 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
-	"net/http"
 	"reflect"
+	"strings"
 
 	"github.com/projectrekor/rekor/pkg/log"
 	"github.com/projectrekor/rekor/pkg/types"
+	"github.com/projectrekor/rekor/pkg/util"
 
 	"github.com/asaskevich/govalidator"
 
@@ -84,6 +81,35 @@ func Base64StringtoByteArray() mapstructure.DecodeHookFunc {
 	}
 }
 
+func (v V001Entry) IndexKeys() []string {
+	var result []string
+
+	if v.HasExternalEntities() {
+		if err := v.FetchExternalEntities(context.Background()); err != nil {
+			log.Logger.Error(err)
+			return result
+		}
+	}
+
+	key, err := v.keyObj.CanonicalValue()
+	if err != nil {
+		log.Logger.Error(err)
+	} else {
+		hasher := sha256.New()
+		if _, err := hasher.Write(key); err != nil {
+			log.Logger.Error(err)
+		} else {
+			result = append(result, strings.ToLower(hex.EncodeToString(hasher.Sum(nil))))
+		}
+	}
+
+	if v.RekordObj.Data.Hash != nil {
+		result = append(result, strings.ToLower(swag.StringValue(v.RekordObj.Data.Hash.Value)))
+	}
+
+	return result
+}
+
 func (v *V001Entry) Unmarshal(pe models.ProposedEntry) error {
 	rekord, ok := pe.(*models.Rekord)
 	if !ok {
@@ -129,46 +155,6 @@ func (v V001Entry) HasExternalEntities() bool {
 	return false
 }
 
-// fileOrURLReadCloser Note: caller is responsible for closing ReadCloser returned from method!
-func fileOrURLReadCloser(ctx context.Context, url string, content []byte, checkGZIP bool) (io.ReadCloser, error) {
-	var dataReader io.ReadCloser
-	if url != "" {
-		//TODO: set timeout here, SSL settings?
-		client := &http.Client{}
-		req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
-		if err != nil {
-			return nil, err
-		}
-		resp, err := client.Do(req)
-		if err != nil {
-			return nil, err
-		}
-		if resp.StatusCode < 200 || resp.StatusCode > 299 {
-			return nil, fmt.Errorf("error received while fetching artifact: %v", resp.Status)
-		}
-
-		if checkGZIP {
-			// read first 512 bytes to determine if content is gzip compressed
-			bufReader := bufio.NewReaderSize(resp.Body, 512)
-			ctBuf, err := bufReader.Peek(512)
-			if err != nil && err != bufio.ErrBufferFull && err != io.EOF {
-				return nil, err
-			}
-
-			if http.DetectContentType(ctBuf) == "application/x-gzip" {
-				dataReader, _ = gzip.NewReader(io.MultiReader(bufReader, resp.Body))
-			} else {
-				dataReader = ioutil.NopCloser(io.MultiReader(bufReader, resp.Body))
-			}
-		} else {
-			dataReader = resp.Body
-		}
-	} else {
-		dataReader = ioutil.NopCloser(bytes.NewReader(content))
-	}
-	return dataReader, nil
-}
-
 func (v *V001Entry) FetchExternalEntities(ctx context.Context) error {
 	if v.fetchedExternalEntities {
 		return nil
@@ -209,7 +195,7 @@ func (v *V001Entry) FetchExternalEntities(ctx context.Context) error {
 		defer hashW.Close()
 		defer sigW.Close()
 
-		dataReadCloser, err := fileOrURLReadCloser(ctx, v.RekordObj.Data.URL.String(), v.RekordObj.Data.Content, true)
+		dataReadCloser, err := util.FileOrURLReadCloser(ctx, v.RekordObj.Data.URL.String(), v.RekordObj.Data.Content, true)
 		if err != nil {
 			return closePipesOnError(err)
 		}
@@ -250,7 +236,7 @@ func (v *V001Entry) FetchExternalEntities(ctx context.Context) error {
 	g.Go(func() error {
 		defer close(sigResult)
 
-		sigReadCloser, err := fileOrURLReadCloser(ctx, v.RekordObj.Signature.URL.String(),
+		sigReadCloser, err := util.FileOrURLReadCloser(ctx, v.RekordObj.Signature.URL.String(),
 			v.RekordObj.Signature.Content, false)
 		if err != nil {
 			return closePipesOnError(err)
@@ -275,7 +261,7 @@ func (v *V001Entry) FetchExternalEntities(ctx context.Context) error {
 	g.Go(func() error {
 		defer close(keyResult)
 
-		keyReadCloser, err := fileOrURLReadCloser(ctx, v.RekordObj.Signature.PublicKey.URL.String(),
+		keyReadCloser, err := util.FileOrURLReadCloser(ctx, v.RekordObj.Signature.PublicKey.URL.String(),
 			v.RekordObj.Signature.PublicKey.Content, false)
 		if err != nil {
 			return closePipesOnError(err)
diff --git a/pkg/types/types.go b/pkg/types/types.go
index 465eaf5..0cbf39e 100644
--- a/pkg/types/types.go
+++ b/pkg/types/types.go
@@ -31,6 +31,7 @@ type TypeImpl interface {
 
 type EntryImpl interface {
 	APIVersion() string
+	IndexKeys() []string
 	Canonicalize(ctx context.Context) ([]byte, error)
 	FetchExternalEntities(ctx context.Context) error
 	HasExternalEntities() bool
diff --git a/pkg/util/fetch.go b/pkg/util/fetch.go
new file mode 100644
index 0000000..5b9a00f
--- /dev/null
+++ b/pkg/util/fetch.go
@@ -0,0 +1,67 @@
+/*
+Copyright © 2021 Bob Callaway <bcallawa@redhat.com>
+
+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 util
+
+import (
+	"bufio"
+	"bytes"
+	"compress/gzip"
+	"context"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+)
+
+// FileOrURLReadCloser Note: caller is responsible for closing ReadCloser returned from method!
+func FileOrURLReadCloser(ctx context.Context, url string, content []byte, checkGZIP bool) (io.ReadCloser, error) {
+	var dataReader io.ReadCloser
+	if url != "" {
+		//TODO: set timeout here, SSL settings?
+		client := &http.Client{}
+		req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
+		if err != nil {
+			return nil, err
+		}
+		resp, err := client.Do(req)
+		if err != nil {
+			return nil, err
+		}
+		if resp.StatusCode < 200 || resp.StatusCode > 299 {
+			return nil, fmt.Errorf("error received while fetching artifact: %v", resp.Status)
+		}
+
+		if checkGZIP {
+			// read first 512 bytes to determine if content is gzip compressed
+			bufReader := bufio.NewReaderSize(resp.Body, 512)
+			ctBuf, err := bufReader.Peek(512)
+			if err != nil && err != bufio.ErrBufferFull && err != io.EOF {
+				return nil, err
+			}
+
+			if http.DetectContentType(ctBuf) == "application/x-gzip" {
+				dataReader, _ = gzip.NewReader(io.MultiReader(bufReader, resp.Body))
+			} else {
+				dataReader = ioutil.NopCloser(io.MultiReader(bufReader, resp.Body))
+			}
+		} else {
+			dataReader = resp.Body
+		}
+	} else {
+		dataReader = ioutil.NopCloser(bytes.NewReader(content))
+	}
+	return dataReader, nil
+}
diff --git a/tests/e2e_test.go b/tests/e2e_test.go
index a77befc..de2c330 100644
--- a/tests/e2e_test.go
+++ b/tests/e2e_test.go
@@ -105,6 +105,13 @@ func TestGet(t *testing.T) {
 		t.Error(err)
 	}
 	// TODO: check the actual data in here.
+
+	// check index via the file and public key to ensure that the index has updated correctly
+	out = runCli(t, "search", "--artifact", artifactPath)
+	outputContains(t, out, uuid)
+
+	out = runCli(t, "search", "--public-key", pubPath)
+	outputContains(t, out, uuid)
 }
 
 func TestMinisign(t *testing.T) {
@@ -126,13 +133,21 @@ func TestMinisign(t *testing.T) {
 
 	// Now upload to the log!
 	out = runCli(t, "upload", "--artifact", artifactPath, "--signature", sigPath,
-		"--public-key", pubPath, "--signature-format", "minisign")
+		"--public-key", pubPath, "--pki-format", "minisign")
 	outputContains(t, out, "Created entry at")
 
+	// Output looks like "Created entry at $URL/UUID", so grab the UUID:
+	url := strings.Split(strings.TrimSpace(out), " ")[3]
+	splitUrl := strings.Split(url, "/")
+	uuid := splitUrl[len(splitUrl)-1]
+
 	// Wait and check it.
 	time.Sleep(3 * time.Second)
 
 	out = runCli(t, "verify", "--artifact", artifactPath, "--signature", sigPath,
-		"--public-key", pubPath, "--signature-format", "minisign")
+		"--public-key", pubPath, "--pki-format", "minisign")
 	outputContains(t, out, "Inclusion Proof")
+
+	out = runCli(t, "search", "--public-key", pubPath, "--pki-format", "minisign")
+	outputContains(t, out, uuid)
 }
-- 
GitLab