From 5d11711ee406db2eba6bf0271db49fea29d0a4f0 Mon Sep 17 00:00:00 2001
From: asraa <asraa@google.com>
Date: Tue, 20 Apr 2021 16:26:00 -0400
Subject: [PATCH] add email search index (#276)

Signed-off-by: Asra Ali <asraa@google.com>
---
 cmd/rekor-cli/app/pflags.go            | 34 +++++++++++++-
 cmd/rekor-cli/app/pflags_test.go       | 18 ++++++-
 cmd/rekor-cli/app/search.go            |  6 ++-
 go.mod                                 |  1 +
 go.sum                                 |  5 ++
 openapi.yaml                           |  3 ++
 pkg/api/error.go                       |  1 -
 pkg/api/index.go                       | 14 +++---
 pkg/generated/models/search_index.go   | 20 ++++++++
 pkg/generated/restapi/embedded_spec.go |  8 ++++
 pkg/pki/minisign/minisign.go           |  5 ++
 pkg/pki/pgp/pgp.go                     | 17 +++++++
 pkg/pki/pgp/pgp_test.go                | 50 ++++++++++++++++++++
 pkg/pki/pkcs7/pkcs7.go                 | 24 ++++++++++
 pkg/pki/pkcs7/pkcs7_test.go            | 65 ++++++++++++++++++++++++++
 pkg/pki/pki.go                         |  1 +
 pkg/pki/ssh/ssh.go                     |  5 ++
 pkg/pki/x509/x509.go                   | 18 +++++++
 pkg/types/rekord/v0.0.1/entry.go       |  2 +
 pkg/types/rpm/v0.0.1/entry.go          |  2 +
 tests/e2e_test.go                      |  5 ++
 21 files changed, 293 insertions(+), 11 deletions(-)

diff --git a/cmd/rekor-cli/app/pflags.go b/cmd/rekor-cli/app/pflags.go
index f701139..66ec0b0 100644
--- a/cmd/rekor-cli/app/pflags.go
+++ b/cmd/rekor-cli/app/pflags.go
@@ -33,6 +33,7 @@ import (
 	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
 
+	"github.com/go-playground/validator"
 	"github.com/sigstore/rekor/pkg/generated/models"
 	jar_v001 "github.com/sigstore/rekor/pkg/types/jar/v0.0.1"
 	rekord_v001 "github.com/sigstore/rekor/pkg/types/rekord/v0.0.1"
@@ -47,6 +48,8 @@ func addSearchPFlags(cmd *cobra.Command) error {
 	cmd.Flags().Var(&fileOrURLFlag{}, "artifact", "path or URL to artifact file")
 
 	cmd.Flags().Var(&uuidFlag{}, "sha", "the SHA256 sum of the artifact")
+
+	cmd.Flags().Var(&emailFlag{}, "email", "email associated with the public key's subject")
 	return nil
 }
 
@@ -55,9 +58,10 @@ func validateSearchPFlags() error {
 
 	publicKey := viper.GetString("public-key")
 	sha := viper.GetString("sha")
+	email := viper.GetString("email")
 
-	if artifactStr == "" && publicKey == "" && sha == "" {
-		return errors.New("either 'sha' or 'artifact' or 'public-key' must be specified")
+	if artifactStr == "" && publicKey == "" && sha == "" && email == "" {
+		return errors.New("either 'sha' or 'artifact' or 'public-key' or 'email' must be specified")
 	}
 	if publicKey != "" {
 		if viper.GetString("pki-format") == "" {
@@ -530,3 +534,29 @@ func addLogIndexFlag(cmd *cobra.Command, required bool) error {
 	}
 	return nil
 }
+
+type emailFlag struct {
+	Email string `validate:"email"`
+}
+
+func (e *emailFlag) String() string {
+	return e.Email
+}
+
+func (e *emailFlag) Set(v string) error {
+	if v == "" {
+		return errors.New("flag must be specified")
+	}
+
+	e.Email = v
+	validate := validator.New()
+	if err := validate.Struct(e); err != nil {
+		return fmt.Errorf("error parsing --email: %s", err)
+	}
+
+	return nil
+}
+
+func (e *emailFlag) Type() string {
+	return "email"
+}
diff --git a/cmd/rekor-cli/app/pflags_test.go b/cmd/rekor-cli/app/pflags_test.go
index 2155aa5..ca314fa 100644
--- a/cmd/rekor-cli/app/pflags_test.go
+++ b/cmd/rekor-cli/app/pflags_test.go
@@ -420,6 +420,7 @@ func TestSearchPFlags(t *testing.T) {
 		artifact              string
 		publicKey             string
 		sha                   string
+		email                 string
 		pkiFormat             string
 		expectParseSuccess    bool
 		expectValidateSuccess bool
@@ -516,7 +517,19 @@ func TestSearchPFlags(t *testing.T) {
 			expectValidateSuccess: false,
 		},
 		{
-			caseDesc:              "no flags when either artifact, sha, or public key are needed",
+			caseDesc:              "valid email",
+			email:                 "cat@foo.com",
+			expectParseSuccess:    true,
+			expectValidateSuccess: true,
+		},
+		{
+			caseDesc:              "invalid email",
+			email:                 "SignaMeseCat",
+			expectParseSuccess:    false,
+			expectValidateSuccess: true,
+		},
+		{
+			caseDesc:              "no flags when either artifact, sha, public key, or email are needed",
 			expectParseSuccess:    true,
 			expectValidateSuccess: false,
 		},
@@ -542,6 +555,9 @@ func TestSearchPFlags(t *testing.T) {
 		if tc.sha != "" {
 			args = append(args, "--sha", tc.sha)
 		}
+		if tc.email != "" {
+			args = append(args, "--email", tc.email)
+		}
 
 		if err := blankCmd.ParseFlags(args); (err == nil) != tc.expectParseSuccess {
 			t.Errorf("unexpected result parsing '%v': %v", tc.caseDesc, err)
diff --git a/cmd/rekor-cli/app/search.go b/cmd/rekor-cli/app/search.go
index 160d780..41837d5 100644
--- a/cmd/rekor-cli/app/search.go
+++ b/cmd/rekor-cli/app/search.go
@@ -56,7 +56,7 @@ func (s *searchCmdOutput) String() string {
 var searchCmd = &cobra.Command{
 	Use:   "search",
 	Short: "Rekor search command",
-	Long:  `Searches the Rekor index to find entries by artifact or public key`,
+	Long:  `Searches the Rekor index to find entries by sha, artifact,  public key, or e-mail`,
 	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 {
@@ -150,6 +150,10 @@ var searchCmd = &cobra.Command{
 			}
 		}
 
+		emailStr := viper.GetString("email")
+		if emailStr != "" {
+			params.Query.Email = strfmt.Email(emailStr)
+		}
 		resp, err := rekorClient.Index.SearchIndex(params)
 		if err != nil {
 			switch t := err.(type) {
diff --git a/go.mod b/go.mod
index 8d8b74b..1a2b082 100644
--- a/go.mod
+++ b/go.mod
@@ -15,6 +15,7 @@ require (
 	github.com/go-openapi/strfmt v0.20.1
 	github.com/go-openapi/swag v0.19.15
 	github.com/go-openapi/validate v0.20.2
+	github.com/go-playground/validator v9.31.0+incompatible
 	github.com/google/rpmpack v0.0.0-20210107155803-d6befbf05148
 	github.com/google/trillian v1.3.14-0.20210413093047-5e12fb368c8f
 	github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c // indirect
diff --git a/go.sum b/go.sum
index efb2331..a103dc6 100644
--- a/go.sum
+++ b/go.sum
@@ -416,8 +416,12 @@ github.com/go-openapi/validate v0.20.1/go.mod h1:b60iJT+xNNLfaQJUqLI7946tYiFEOuE
 github.com/go-openapi/validate v0.20.2 h1:AhqDegYV3J3iQkMPJSXkvzymHKMTw0BST3RK3hTT4ts=
 github.com/go-openapi/validate v0.20.2/go.mod h1:e7OJoKNgd0twXZwIn0A43tHbvIcr/rZIVCbJBpTUoY0=
 github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
 github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
+github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
 github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
+github.com/go-playground/validator v9.31.0+incompatible h1:UA72EPEogEnq76ehGdEDp4Mit+3FDh548oRqwVgNsHA=
+github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig=
 github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
 github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
 github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
@@ -735,6 +739,7 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
 github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
 github.com/letsencrypt/pkcs11key v2.0.1-0.20170608213348-396559074696+incompatible/go.mod h1:iGYXKqDXt0cpBthCHdr9ZdsQwyGlYFh/+8xa4WzIQ34=
 github.com/letsencrypt/pkcs11key/v4 v4.0.0/go.mod h1:EFUvBDay26dErnNb70Nd0/VW3tJiIbETBPTl9ATXQag=
diff --git a/openapi.yaml b/openapi.yaml
index 0f9e240..b23a438 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -308,6 +308,9 @@ definitions:
   SearchIndex:
     type: object
     properties:
+      email:
+        type: string
+        format: email
       publicKey:
         type: object
         properties:
diff --git a/pkg/api/error.go b/pkg/api/error.go
index 78d9767..f47c18d 100644
--- a/pkg/api/error.go
+++ b/pkg/api/error.go
@@ -38,7 +38,6 @@ const (
 	entryAlreadyExists             = "An equivalent entry already exists in the transparency log with UUID %v"
 	firstSizeLessThanLastSize      = "firstSize(%d) must be less than lastSize(%d)"
 	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"
diff --git a/pkg/api/index.go b/pkg/api/index.go
index 6ed73d3..cda6bf4 100644
--- a/pkg/api/index.go
+++ b/pkg/api/index.go
@@ -19,11 +19,9 @@ import (
 	"context"
 	"crypto/sha256"
 	"encoding/hex"
-	"errors"
 	"net/http"
 	"strings"
 
-	"github.com/asaskevich/govalidator"
 	"github.com/go-openapi/runtime/middleware"
 	"github.com/go-openapi/swag"
 	radix "github.com/mediocregopher/radix/v4"
@@ -39,10 +37,7 @@ func SearchIndexHandler(params index.SearchIndexParams) middleware.Responder {
 
 	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)
-		}
+		// This must be a valid sha256 hash
 		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)
@@ -77,6 +72,13 @@ func SearchIndexHandler(params index.SearchIndexParams) middleware.Responder {
 		}
 		result = append(result, resultUUIDs...)
 	}
+	if params.Query.Email != "" {
+		var resultUUIDs []string
+		if err := redisClient.Do(httpReqCtx, radix.Cmd(&resultUUIDs, "LRANGE", strings.ToLower(params.Query.Email.String()), "0", "-1")); err != nil {
+			return handleRekorAPIError(params, http.StatusInternalServerError, err, redisUnexpectedResult)
+		}
+		result = append(result, resultUUIDs...)
+	}
 
 	return index.NewSearchIndexOK().WithPayload(result)
 }
diff --git a/pkg/generated/models/search_index.go b/pkg/generated/models/search_index.go
index c0427f6..9163f78 100644
--- a/pkg/generated/models/search_index.go
+++ b/pkg/generated/models/search_index.go
@@ -36,6 +36,10 @@ import (
 // swagger:model SearchIndex
 type SearchIndex struct {
 
+	// email
+	// Format: email
+	Email strfmt.Email `json:"email,omitempty"`
+
 	// hash
 	// Pattern: ^[0-9a-fA-F]{64}$
 	Hash string `json:"hash,omitempty"`
@@ -48,6 +52,10 @@ type SearchIndex struct {
 func (m *SearchIndex) Validate(formats strfmt.Registry) error {
 	var res []error
 
+	if err := m.validateEmail(formats); err != nil {
+		res = append(res, err)
+	}
+
 	if err := m.validateHash(formats); err != nil {
 		res = append(res, err)
 	}
@@ -62,6 +70,18 @@ func (m *SearchIndex) Validate(formats strfmt.Registry) error {
 	return nil
 }
 
+func (m *SearchIndex) validateEmail(formats strfmt.Registry) error {
+	if swag.IsZero(m.Email) { // not required
+		return nil
+	}
+
+	if err := validate.FormatOf("email", "body", "email", m.Email.String(), formats); err != nil {
+		return err
+	}
+
+	return nil
+}
+
 func (m *SearchIndex) validateHash(formats strfmt.Registry) error {
 	if swag.IsZero(m.Hash) { // not required
 		return nil
diff --git a/pkg/generated/restapi/embedded_spec.go b/pkg/generated/restapi/embedded_spec.go
index 71b9d36..9b33b8d 100644
--- a/pkg/generated/restapi/embedded_spec.go
+++ b/pkg/generated/restapi/embedded_spec.go
@@ -481,6 +481,10 @@ func init() {
     "SearchIndex": {
       "type": "object",
       "properties": {
+        "email": {
+          "type": "string",
+          "format": "email"
+        },
         "hash": {
           "type": "string",
           "pattern": "^[0-9a-fA-F]{64}$"
@@ -1559,6 +1563,10 @@ func init() {
     "SearchIndex": {
       "type": "object",
       "properties": {
+        "email": {
+          "type": "string",
+          "format": "email"
+        },
         "hash": {
           "type": "string",
           "pattern": "^[0-9a-fA-F]{64}$"
diff --git a/pkg/pki/minisign/minisign.go b/pkg/pki/minisign/minisign.go
index 0f5c71a..22bc3b0 100644
--- a/pkg/pki/minisign/minisign.go
+++ b/pkg/pki/minisign/minisign.go
@@ -148,3 +148,8 @@ func (k PublicKey) CanonicalValue() ([]byte, error) {
 	b64Key := base64.StdEncoding.EncodeToString(k.key.PublicKey[:])
 	return []byte(b64Key), nil
 }
+
+// EmailAddresses implements the pki.PublicKey interface
+func (k PublicKey) EmailAddresses() []string {
+	return nil
+}
diff --git a/pkg/pki/pgp/pgp.go b/pkg/pki/pgp/pgp.go
index e645f1e..7e3c7bb 100644
--- a/pkg/pki/pgp/pgp.go
+++ b/pkg/pki/pgp/pgp.go
@@ -24,6 +24,7 @@ import (
 	"io"
 	"net/http"
 
+	"github.com/go-playground/validator"
 	"golang.org/x/crypto/openpgp"
 	"golang.org/x/crypto/openpgp/armor"
 	"golang.org/x/crypto/openpgp/packet"
@@ -277,3 +278,19 @@ func (k PublicKey) KeyRing() (openpgp.KeyRing, error) {
 
 	return k.key, nil
 }
+
+// EmailAddresses implements the pki.PublicKey interface
+func (k PublicKey) EmailAddresses() []string {
+	var names []string
+	// Extract from cert
+	for _, entity := range k.key {
+		for _, identity := range entity.Identities {
+			validate := validator.New()
+			errs := validate.Var(identity.UserId.Email, "required,email")
+			if errs == nil {
+				names = append(names, identity.UserId.Email)
+			}
+		}
+	}
+	return names
+}
diff --git a/pkg/pki/pgp/pgp_test.go b/pkg/pki/pgp/pgp_test.go
index b534ac2..17c0b34 100644
--- a/pkg/pki/pgp/pgp_test.go
+++ b/pkg/pki/pgp/pgp_test.go
@@ -24,6 +24,8 @@ import (
 	"net/http"
 	"net/http/httptest"
 	"os"
+	"reflect"
+	"sort"
 	"testing"
 
 	"go.uber.org/goleak"
@@ -341,6 +343,54 @@ func TestCanonicalValuePublicKey(t *testing.T) {
 	}
 }
 
+func TestEmailAddresses(t *testing.T) {
+	type test struct {
+		caseDesc  string
+		inputFile string
+		emails    []string
+	}
+
+	var k PublicKey
+	if len(k.EmailAddresses()) != 0 {
+		t.Errorf("EmailAddresses for unitialized key should give empty slice")
+	}
+	tests := []test{
+		{caseDesc: "Valid armored public key", inputFile: "testdata/valid_armored_public.pgp", emails: []string{}},
+		{caseDesc: "Valid armored public key with multiple subentries", inputFile: "testdata/valid_armored_complex_public.pgp", emails: []string{"linux-packages-keymaster@google.com", "linux-packages-keymaster@google.com"}},
+		{caseDesc: "Valid binary public key", inputFile: "testdata/valid_binary_public.pgp", emails: []string{}},
+		{caseDesc: "Valid binary public key with multiple subentries", inputFile: "testdata/valid_binary_complex_public.pgp", emails: []string{"linux-packages-keymaster@google.com", "linux-packages-keymaster@google.com"}},
+	}
+
+	for _, tc := range tests {
+		var input io.Reader
+		var err error
+		input, err = os.Open(tc.inputFile)
+		if err != nil {
+			t.Errorf("%v: cannot open %v", tc.caseDesc, tc.inputFile)
+		}
+
+		inputKey, err := NewPublicKey(input)
+		if err != nil {
+			t.Errorf("%v: Error reading input for TestEmailAddresses: %v", tc.caseDesc, err)
+		}
+
+		emails := inputKey.EmailAddresses()
+
+		if len(emails) == len(tc.emails) {
+			if len(emails) > 0 {
+				sort.Strings(emails)
+				sort.Strings(tc.emails)
+				if !reflect.DeepEqual(emails, tc.emails) {
+					t.Errorf("%v: Error getting email addresses from keys, got %v, expected %v", tc.caseDesc, emails, tc.emails)
+				}
+			}
+		} else {
+			t.Errorf("%v: Error getting email addresses from keys length, got %v, expected %v", tc.caseDesc, len(emails), len(tc.emails))
+		}
+
+	}
+}
+
 func TestVerifySignature(t *testing.T) {
 	type test struct {
 		caseDesc string
diff --git a/pkg/pki/pkcs7/pkcs7.go b/pkg/pki/pkcs7/pkcs7.go
index 98f90a0..8e84262 100644
--- a/pkg/pki/pkcs7/pkcs7.go
+++ b/pkg/pki/pkcs7/pkcs7.go
@@ -20,15 +20,20 @@ import (
 	"bytes"
 	"crypto"
 	"crypto/x509"
+	"encoding/asn1"
 	"encoding/pem"
 	"errors"
 	"fmt"
 	"io"
 	"io/ioutil"
+	"strings"
 
 	"github.com/sassoftware/relic/lib/pkcs7"
 )
 
+// EmailAddressOID defined by https://oidref.com/1.2.840.113549.1.9.1
+var EmailAddressOID asn1.ObjectIdentifier = []int{1, 2, 840, 113549, 1, 9, 1}
+
 type Signature struct {
 	signedData pkcs7.SignedData
 	detached   bool
@@ -184,3 +189,22 @@ func (k PublicKey) CanonicalValue() ([]byte, error) {
 	}
 	return buf.Bytes(), nil
 }
+
+// EmailAddresses implements the pki.PublicKey interface
+func (k PublicKey) EmailAddresses() []string {
+	var names []string
+	// Get email address from Subject name in raw cert.
+	cert, err := x509.ParseCertificate(k.rawCert)
+	if err != nil {
+		// This should not happen from a valid PublicKey, but fail gracefully.
+		return names
+	}
+
+	for _, name := range cert.Subject.Names {
+		if name.Type.Equal(EmailAddressOID) {
+			names = append(names, strings.ToLower(name.Value.(string)))
+		}
+	}
+
+	return names
+}
diff --git a/pkg/pki/pkcs7/pkcs7_test.go b/pkg/pki/pkcs7/pkcs7_test.go
index 21386d7..3b77256 100644
--- a/pkg/pki/pkcs7/pkcs7_test.go
+++ b/pkg/pki/pkcs7/pkcs7_test.go
@@ -19,6 +19,8 @@ package pkcs7
 import (
 	"bytes"
 	"encoding/base64"
+	"reflect"
+	"sort"
 	"strings"
 	"testing"
 )
@@ -172,6 +174,26 @@ Cg0KTmFtZTogc2lnc3RvcmUvcGx1Z2luL0hlbHBNb2pvLmNsYXNzDQpTSEEtMjU2
 LURpZ2VzdDogU3ZPNkhibVlBSzBMVEhyVCtYbmRBOExJdUptZU5ub1dyYmVHS3dv
 TE9Pdz0NCg0K`
 
+const pkcsPEMEmail = `-----BEGIN PKCS7-----
+MIIDCgYJKoZIhvcNAQcCoIIC+zCCAvcCAQExADALBgkqhkiG9w0BBwGgggLdMIIC
+2TCCAjqgAwIBAgIUAL0Gw2SJvPW8PbXw+42XwmW8//owCgYIKoZIzj0EAwIwfTEL
+MAkGA1UEBhMCVVMxCzAJBgNVBAgMAk1BMQ8wDQYDVQQHDAZCb3N0b24xITAfBgNV
+BAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEOMAwGA1UEAwwFUmVrb3IxHTAb
+BgkqhkiG9w0BCQEWDnRlc3RAcmVrb3IuZGV2MCAXDTIxMDQxOTE0MTMyMFoYDzQ0
+ODUwNTMxMTQxMzIwWjB9MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTUExDzANBgNV
+BAcMBkJvc3RvbjEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMQ4w
+DAYDVQQDDAVSZWtvcjEdMBsGCSqGSIb3DQEJARYOdGVzdEByZWtvci5kZXYwgZsw
+EAYHKoZIzj0CAQYFK4EEACMDgYYABABN0k2SaX5iK6Ahw8m+wXbQml4E8GEL0qLA
+lA0Gu8thlhvAcOLdPzNxPl2tsM7bBzTrD2H4iLM4myvpT4x2NgbjyAClvhXfJTOY
+m7oTFcKq0kNf8LEV2fjBpfdrw9yiS1DV6YWHwCzc3TUrZIChGhMYnfZPVu997wzy
+euVBSUMeO5Lmp6NTMFEwHQYDVR0OBBYEFJPLiMMFN5Cm6/rjOTPR2HWbbO5PMB8G
+A1UdIwQYMBaAFJPLiMMFN5Cm6/rjOTPR2HWbbO5PMA8GA1UdEwEB/wQFMAMBAf8w
+CgYIKoZIzj0EAwIDgYwAMIGIAkIBmRqxw8sStWknjeOgdyKkd+vFehNuVaiHAKGs
+z+6KG3jPG5xN5+/Ws+OMTAp7Hv6HH5ChDO3LJ6t/sCun1otdWmICQgCUqg1ke+Rj
+nVqVlz1rUR7CTL2SlG9Xg1kAkYH4vMn/otEuAhnKf+GWLNB1l/dTFNEyysvIA6yd
+FG8HXGWcnVVIVaEAMQA=
+-----END PKCS7-----`
+
 func TestSignature_Verify(t *testing.T) {
 	tests := []struct {
 		name  string
@@ -245,3 +267,46 @@ func TestSignature_VerifyFail(t *testing.T) {
 		})
 	}
 }
+
+func TestEmailAddresses(t *testing.T) {
+	tests := []struct {
+		name   string
+		pkcs7  string
+		emails []string
+	}{
+		{
+			name:   "ec",
+			pkcs7:  pkcsECDSAPEM,
+			emails: []string{},
+		},
+		{
+			name:   "email",
+			pkcs7:  pkcsPEMEmail,
+			emails: []string{"test@rekor.dev"},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			pub, err := NewPublicKey(strings.NewReader(tt.pkcs7))
+			if err != nil {
+				t.Fatal(err)
+			}
+			emails := pub.EmailAddresses()
+
+			if len(emails) == len(tt.emails) {
+				if len(emails) > 0 {
+					sort.Strings(emails)
+					sort.Strings(tt.emails)
+					if !reflect.DeepEqual(emails, tt.emails) {
+						t.Errorf("%v: Error getting email addresses from keys, got %v, expected %v", tt.name, emails, tt.emails)
+					}
+				}
+			} else {
+				t.Errorf("%v: Error getting email addresses from keys, got %v, expected %v", tt.name, emails, tt.emails)
+			}
+
+		})
+	}
+
+}
diff --git a/pkg/pki/pki.go b/pkg/pki/pki.go
index 43fb9d7..4463838 100644
--- a/pkg/pki/pki.go
+++ b/pkg/pki/pki.go
@@ -30,6 +30,7 @@ import (
 // PublicKey Generic object representing a public key (regardless of format & algorithm)
 type PublicKey interface {
 	CanonicalValue() ([]byte, error)
+	EmailAddresses() []string
 }
 
 // Signature Generic object representing a signature (regardless of format & algorithm)
diff --git a/pkg/pki/ssh/ssh.go b/pkg/pki/ssh/ssh.go
index 05a4fb1..8474431 100644
--- a/pkg/pki/ssh/ssh.go
+++ b/pkg/pki/ssh/ssh.go
@@ -96,3 +96,8 @@ func (k PublicKey) CanonicalValue() ([]byte, error) {
 	}
 	return ssh.MarshalAuthorizedKey(k.key), nil
 }
+
+// EmailAddresses implements the pki.PublicKey interface
+func (k PublicKey) EmailAddresses() []string {
+	return nil
+}
diff --git a/pkg/pki/x509/x509.go b/pkg/pki/x509/x509.go
index bf0db8c..f272d44 100644
--- a/pkg/pki/x509/x509.go
+++ b/pkg/pki/x509/x509.go
@@ -23,13 +23,18 @@ import (
 	"crypto/rsa"
 	"crypto/sha256"
 	"crypto/x509"
+	"encoding/asn1"
 	"encoding/pem"
 	"errors"
 	"fmt"
 	"io"
 	"io/ioutil"
+	"strings"
 )
 
+// EmailAddressOID defined by https://oidref.com/1.2.840.113549.1.9.1
+var EmailAddressOID asn1.ObjectIdentifier = []int{1, 2, 840, 113549, 1, 9, 1}
+
 type Signature struct {
 	signature []byte
 }
@@ -166,3 +171,16 @@ func (k PublicKey) CanonicalValue() ([]byte, error) {
 	}
 	return buf.Bytes(), nil
 }
+
+// EmailAddresses implements the pki.PublicKey interface
+func (k PublicKey) EmailAddresses() []string {
+	var names []string
+	if k.cert != nil {
+		for _, name := range k.cert.c.Subject.Names {
+			if name.Type.Equal(EmailAddressOID) {
+				names = append(names, strings.ToLower(name.Value.(string)))
+			}
+		}
+	}
+	return names
+}
diff --git a/pkg/types/rekord/v0.0.1/entry.go b/pkg/types/rekord/v0.0.1/entry.go
index 59f70d6..3e5f55d 100644
--- a/pkg/types/rekord/v0.0.1/entry.go
+++ b/pkg/types/rekord/v0.0.1/entry.go
@@ -85,6 +85,8 @@ func (v V001Entry) IndexKeys() []string {
 		}
 	}
 
+	result = append(result, v.keyObj.EmailAddresses()...)
+
 	if v.RekordObj.Data.Hash != nil {
 		result = append(result, strings.ToLower(swag.StringValue(v.RekordObj.Data.Hash.Value)))
 	}
diff --git a/pkg/types/rpm/v0.0.1/entry.go b/pkg/types/rpm/v0.0.1/entry.go
index a22f2d5..b8ebbcb 100644
--- a/pkg/types/rpm/v0.0.1/entry.go
+++ b/pkg/types/rpm/v0.0.1/entry.go
@@ -89,6 +89,8 @@ func (v V001Entry) IndexKeys() []string {
 		}
 	}
 
+	result = append(result, v.keyObj.EmailAddresses()...)
+
 	if v.RPMModel.Package.Hash != nil {
 		result = append(result, strings.ToLower(swag.StringValue(v.RPMModel.Package.Hash.Value)))
 	}
diff --git a/tests/e2e_test.go b/tests/e2e_test.go
index dbfe808..60b5a71 100644
--- a/tests/e2e_test.go
+++ b/tests/e2e_test.go
@@ -300,6 +300,11 @@ func TestX509(t *testing.T) {
 	out = runCli(t, "upload", "--artifact", artifactPath, "--signature", sigPath,
 		"--public-key", certPath, "--pki-format", "x509")
 	outputContains(t, out, "Created entry at")
+	uuid := getUUIDFromUploadOutput(t, out)
+
+	// Search via email
+	out = runCli(t, "search", "--email", "test@rekor.dev")
+	outputContains(t, out, uuid)
 
 }
 
-- 
GitLab