From 53d71cd8de3959e904b80c3d10637939c8b76927 Mon Sep 17 00:00:00 2001
From: Bob Callaway <bobcallaway@users.noreply.github.com>
Date: Wed, 14 Jul 2021 12:11:59 -0400
Subject: [PATCH] Improve separation between type implementations and CLI code
 (#339)

* Refactor PKI factory and add type checking

This allows for more DRY addition of new PKI types, and stricter
type checking. This also allows for simpler enumeration of
supported PKI formats which will be used in further updates to
simplify the CLI codebase.

Signed-off-by: Bob Callaway <bob.callaway@gmail.com>

* revamp CLI flags; support different versions for upload

Signed-off-by: Bob Callaway <bob.callaway@gmail.com>

* Add Alpine Package type

This adds support for the alpine package format used by Alpine Linux,
which is the concatenation of three tgz files (signature, control data,
and then the actual package files).

Signed-off-by: Bob Callaway <bob.callaway@gmail.com>

* use shaFlag for --artifact-hash

Signed-off-by: Bob Callaway <bob.callaway@gmail.com>

* change arg type to PKIFormat

Signed-off-by: Bob Callaway <bob.callaway@gmail.com>

* defer type-specific validation logic to type code (instead of in CLI); also use CliLogger throughout CLI

Signed-off-by: Bob Callaway <bob.callaway@gmail.com>

* refactor factory code

Signed-off-by: Bob Callaway <bob.callaway@gmail.com>

* review comments

Signed-off-by: Bob Callaway <bob.callaway@gmail.com>
---
 cmd/rekor-cli/app/format/wrap.go       |   6 +-
 cmd/rekor-cli/app/get.go               |   7 +-
 cmd/rekor-cli/app/log_info.go          |   1 +
 cmd/rekor-cli/app/log_proof.go         |   1 +
 cmd/rekor-cli/app/pflag_groups.go      | 177 +++++
 cmd/rekor-cli/app/pflags.go            | 883 +++++--------------------
 cmd/rekor-cli/app/pflags_test.go       |  33 +-
 cmd/rekor-cli/app/root.go              |  83 +--
 cmd/rekor-cli/app/search.go            |  68 +-
 cmd/rekor-cli/app/timestamp.go         |  84 +--
 cmd/rekor-cli/app/upload.go            |  70 +-
 cmd/rekor-cli/app/verify.go            |  54 +-
 docker-compose.yml                     |   2 +-
 pkg/api/error.go                       |   1 +
 pkg/api/index.go                       |   5 +-
 pkg/log/log.go                         |   1 +
 pkg/pki/factory.go                     | 116 ++++
 pkg/pki/factory_test.go                | 121 ++++
 pkg/pki/minisign/minisign.go           |   4 +-
 pkg/pki/minisign/minisign_test.go      |   4 -
 pkg/pki/pgp/pgp_test.go                |   4 -
 pkg/pki/pki.go                         |  50 --
 pkg/pki/pki_test.go                    | 108 ---
 pkg/pki/ssh/ssh.go                     |   2 +-
 pkg/pki/x509/x509.go                   |   4 +-
 pkg/types/alpine/alpine.go             |  22 +-
 pkg/types/alpine/alpine_test.go        |   4 +
 pkg/types/alpine/v0.0.1/entry.go       |  66 +-
 pkg/types/entries.go                   |  26 +
 pkg/types/helm/helm.go                 |  18 +-
 pkg/types/helm/helm_test.go            |   4 +
 pkg/types/helm/provenance_test.go      |   7 +-
 pkg/types/helm/v0.0.1/entry.go         |  67 +-
 pkg/types/intoto/intoto.go             |  18 +-
 pkg/types/intoto/intoto_test.go        |   4 +
 pkg/types/intoto/v0.0.1/entry.go       |  53 +-
 pkg/types/jar/jar.go                   |  18 +-
 pkg/types/jar/jar_test.go              |   4 +
 pkg/types/jar/v0.0.1/entry.go          |  56 +-
 pkg/types/rekord/rekord.go             |  18 +-
 pkg/types/rekord/rekord_test.go        |   4 +
 pkg/types/rekord/v0.0.1/entry.go       | 103 ++-
 pkg/types/rfc3161/rfc3161.go           |  30 +-
 pkg/types/rfc3161/rfc3161_test.go      |   3 +
 pkg/types/rfc3161/v0.0.1/entry.go      |  41 +-
 pkg/types/rfc3161/v0.0.1/entry_test.go |   6 +-
 pkg/types/rpm/rpm.go                   |  18 +-
 pkg/types/rpm/rpm_test.go              |   4 +
 pkg/types/rpm/v0.0.1/entry.go          |  68 +-
 pkg/types/types.go                     |  34 +-
 pkg/types/versionmap.go                |   9 +
 51 files changed, 1399 insertions(+), 1195 deletions(-)
 create mode 100644 cmd/rekor-cli/app/pflag_groups.go
 create mode 100644 pkg/pki/factory.go
 create mode 100644 pkg/pki/factory_test.go
 delete mode 100644 pkg/pki/pki_test.go

diff --git a/cmd/rekor-cli/app/format/wrap.go b/cmd/rekor-cli/app/format/wrap.go
index 99cf58b..5e81c37 100644
--- a/cmd/rekor-cli/app/format/wrap.go
+++ b/cmd/rekor-cli/app/format/wrap.go
@@ -18,8 +18,8 @@ package format
 import (
 	"encoding/json"
 	"fmt"
-	"log"
 
+	"github.com/sigstore/rekor/pkg/log"
 	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
 )
@@ -32,7 +32,7 @@ func WrapCmd(f formatCmd) CobraCmd {
 	return func(cmd *cobra.Command, args []string) {
 		obj, err := f(args)
 		if err != nil {
-			log.Fatal(err)
+			log.CliLogger.Fatal(err)
 		}
 
 		// TODO: add flags to control output formatting (JSON, plaintext, etc.)
@@ -53,7 +53,7 @@ func WrapCmd(f formatCmd) CobraCmd {
 func toJSON(i interface{}) string {
 	b, err := json.Marshal(i)
 	if err != nil {
-		log.Fatal(err)
+		log.CliLogger.Fatal(err)
 	}
 	return string(b)
 }
diff --git a/cmd/rekor-cli/app/get.go b/cmd/rekor-cli/app/get.go
index 424a6a7..85817ef 100644
--- a/cmd/rekor-cli/app/get.go
+++ b/cmd/rekor-cli/app/get.go
@@ -74,7 +74,7 @@ var getCmd = &cobra.Command{
 	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)
+			log.CliLogger.Fatal("Error initializing cmd line args: ", err)
 		}
 	},
 	Run: format.WrapCmd(func(args []string) (interface{}, error) {
@@ -161,11 +161,12 @@ func parseEntry(uuid string, e models.LogEntryAnon) (interface{}, error) {
 }
 
 func init() {
+	initializePFlagMap()
 	if err := addUUIDPFlags(getCmd, false); err != nil {
-		log.Logger.Fatal("Error parsing cmd line args:", err)
+		log.CliLogger.Fatal("Error parsing cmd line args: ", err)
 	}
 	if err := addLogIndexFlag(getCmd, false); err != nil {
-		log.Logger.Fatal("Error parsing cmd line args:", err)
+		log.CliLogger.Fatal("Error parsing cmd line args: ", err)
 	}
 
 	rootCmd.AddCommand(getCmd)
diff --git a/cmd/rekor-cli/app/log_info.go b/cmd/rekor-cli/app/log_info.go
index 3f57b3b..e7615f7 100644
--- a/cmd/rekor-cli/app/log_info.go
+++ b/cmd/rekor-cli/app/log_info.go
@@ -153,5 +153,6 @@ var logInfoCmd = &cobra.Command{
 }
 
 func init() {
+	initializePFlagMap()
 	rootCmd.AddCommand(logInfoCmd)
 }
diff --git a/cmd/rekor-cli/app/log_proof.go b/cmd/rekor-cli/app/log_proof.go
index 70b753b..b136572 100644
--- a/cmd/rekor-cli/app/log_proof.go
+++ b/cmd/rekor-cli/app/log_proof.go
@@ -98,6 +98,7 @@ var logProofCmd = &cobra.Command{
 }
 
 func init() {
+	initializePFlagMap()
 	logProofCmd.Flags().Uint64("first-size", 1, "the size of the log where the proof should begin")
 	logProofCmd.Flags().Uint64("last-size", 0, "the size of the log where the proof should end")
 	if err := logProofCmd.MarkFlagRequired("last-size"); err != nil {
diff --git a/cmd/rekor-cli/app/pflag_groups.go b/cmd/rekor-cli/app/pflag_groups.go
new file mode 100644
index 0000000..a005e26
--- /dev/null
+++ b/cmd/rekor-cli/app/pflag_groups.go
@@ -0,0 +1,177 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package app
+
+import (
+	"errors"
+	"fmt"
+	"net/url"
+	"strings"
+
+	"github.com/sigstore/rekor/pkg/pki"
+	"github.com/sigstore/rekor/pkg/types"
+	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+)
+
+// addFlagToCmd adds the specified command of a specified type to the command's flag set
+func addFlagToCmd(cmd *cobra.Command, required bool, flagType FlagType, flag, desc string) error {
+	cmd.Flags().Var(NewFlagValue(flagType, ""), flag, desc)
+	if required {
+		return cmd.MarkFlagRequired(flag)
+	}
+	return nil
+}
+
+// addLogIndexFlag adds the "log-index" command to the command's flag set
+func addLogIndexFlag(cmd *cobra.Command, required bool) error {
+	return addFlagToCmd(cmd, required, logIndexFlag, "log-index", "the index of the entry in the transparency log")
+}
+
+// addUUIDPFlags adds the "uuid" command to the command's flag set
+func addUUIDPFlags(cmd *cobra.Command, required bool) error {
+	return addFlagToCmd(cmd, required, uuidFlag, "uuid", "UUID of entry in transparency log (if known)")
+}
+
+func addArtifactPFlags(cmd *cobra.Command) error {
+	flags := map[string]struct {
+		flagType FlagType
+		desc     string
+		required bool
+	}{
+		"signature": {
+			fileOrURLFlag,
+			"path or URL to detached signature file",
+			false,
+		},
+		"type": {
+			typeFlag,
+			fmt.Sprintf("type of entry expressed as type(:version)?; supported types = %v", types.ListImplementedTypes()),
+			false,
+		},
+		"pki-format": {
+			pkiFormatFlag,
+			fmt.Sprintf("format of the signature and/or public key; options = %v", pki.SupportedFormats()),
+			false,
+		},
+		"public-key": {
+			fileOrURLFlag,
+			"path or URL to public key file",
+			false,
+		},
+		"artifact": {
+			fileOrURLFlag,
+			"path or URL to artifact file",
+			false,
+		},
+		"artifact-hash": {
+			shaFlag,
+			"hex encoded SHA256 hash of artifact (when using URL)",
+			false,
+		},
+		"entry": {
+			fileOrURLFlag,
+			"path or URL to pre-formatted entry file",
+			false,
+		},
+	}
+
+	for flag, flagVal := range flags {
+		if err := addFlagToCmd(cmd, flagVal.required, flagVal.flagType, flag, flagVal.desc); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func validateArtifactPFlags(uuidValid, indexValid bool) error {
+	uuidGiven := false
+	if uuidValid && viper.GetString("uuid") != "" {
+		uuidGiven = true
+	}
+	indexGiven := false
+	if indexValid && viper.GetString("log-index") != "" {
+		indexGiven = true
+	}
+
+	// if neither --entry or --artifact were given, then a reference to a uuid or index is needed
+	if viper.GetString("entry") == "" && viper.GetString("artifact") == "" {
+		if (uuidGiven && uuidValid) || (indexGiven && indexValid) {
+			return nil
+		}
+		return errors.New("either 'entry' or 'artifact' must be specified")
+	}
+
+	return nil
+}
+
+func CreatePropsFromPflags() *types.ArtifactProperties {
+	props := &types.ArtifactProperties{}
+
+	artifactString := viper.GetString("artifact")
+	if artifactString != "" {
+		if isURL(artifactString) {
+			props.ArtifactPath, _ = url.Parse(artifactString)
+		} else {
+			props.ArtifactPath = &url.URL{Path: artifactString}
+		}
+	}
+
+	props.ArtifactHash = viper.GetString("artifact-hash")
+
+	signatureString := viper.GetString("signature")
+	if signatureString != "" {
+		if isURL(signatureString) {
+			props.SignaturePath, _ = url.Parse(signatureString)
+		} else {
+			props.SignaturePath = &url.URL{Path: signatureString}
+		}
+	}
+
+	publicKeyString := viper.GetString("public-key")
+	if publicKeyString != "" {
+		if isURL(publicKeyString) {
+			props.PublicKeyPath, _ = url.Parse(publicKeyString)
+		} else {
+			props.PublicKeyPath = &url.URL{Path: publicKeyString}
+		}
+	}
+
+	props.PKIFormat = viper.GetString("pki-format")
+
+	return props
+}
+
+//TODO: add tests for this
+func ParseTypeFlag(typeStr string) (string, string, error) {
+	// typeStr can come in as:
+	// type -> use default version for this kind
+	// type:version_string -> attempt to use specified version string
+
+	typeStrings := strings.SplitN(typeStr, ":", 2)
+	if _, ok := types.TypeMap.Load(typeStrings[0]); !ok {
+		return "", "", fmt.Errorf("unknown type %v", typeStrings[0])
+	}
+
+	switch len(typeStrings) {
+	case 1:
+		return typeStrings[0], "", nil
+	case 2:
+		return typeStrings[0], typeStrings[1], nil
+	}
+	return "", "", errors.New("malformed type string")
+}
diff --git a/cmd/rekor-cli/app/pflags.go b/cmd/rekor-cli/app/pflags.go
index db7d1e8..0b55ad2 100644
--- a/cmd/rekor-cli/app/pflags.go
+++ b/cmd/rekor-cli/app/pflags.go
@@ -16,790 +16,223 @@
 package app
 
 import (
-	"context"
-	"encoding/hex"
-	"encoding/json"
-	"errors"
 	"fmt"
-	"io/ioutil"
-	"net/http"
-	"net/url"
-	"os"
-	"path/filepath"
+	"log"
 	"strconv"
 	"strings"
 
-	"github.com/go-openapi/strfmt"
-	"github.com/go-openapi/swag"
-	"github.com/spf13/cobra"
-	"github.com/spf13/viper"
+	"github.com/sigstore/rekor/pkg/pki"
+	"github.com/spf13/pflag"
 
 	"github.com/go-playground/validator"
-	"github.com/sigstore/rekor/pkg/generated/models"
-	alpine_v001 "github.com/sigstore/rekor/pkg/types/alpine/v0.0.1"
-	helm_v001 "github.com/sigstore/rekor/pkg/types/helm/v0.0.1"
-	intoto_v001 "github.com/sigstore/rekor/pkg/types/intoto/v0.0.1"
-	jar_v001 "github.com/sigstore/rekor/pkg/types/jar/v0.0.1"
-	rekord_v001 "github.com/sigstore/rekor/pkg/types/rekord/v0.0.1"
-	rfc3161_v001 "github.com/sigstore/rekor/pkg/types/rfc3161/v0.0.1"
-	rpm_v001 "github.com/sigstore/rekor/pkg/types/rpm/v0.0.1"
+	"github.com/pkg/errors"
 )
 
-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 SHA256 sum of the artifact")
-
-	cmd.Flags().Var(&emailFlag{}, "email", "email associated with the public key's subject")
-	return nil
-}
-
-func validateSearchPFlags() error {
-	artifactStr := viper.GetString("artifact")
-
-	publicKey := viper.GetString("public-key")
-	sha := viper.GetString("sha")
-	email := viper.GetString("email")
-
-	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") == "" {
-			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(&typeFlag{value: "rekord"}, "type", "type of entry")
-	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(&uuidFlag{}, "artifact-hash", "hex encoded SHA256 hash of artifact (when using URL)")
-
-	cmd.Flags().Var(&fileOrURLFlag{}, "entry", "path or URL to pre-formatted entry file")
-
-	return nil
-}
-
-func validateArtifactPFlags(uuidValid, indexValid bool) error {
-	uuidGiven := false
-	if uuidValid {
-		uuid := uuidFlag{}
-		uuidStr := viper.GetString("uuid")
-
-		if uuidStr != "" {
-			if err := uuid.Set(uuidStr); err != nil {
-				return err
-			}
-			uuidGiven = true
-		}
-	}
-	indexGiven := false
-	if indexValid {
-		logIndex := logIndexFlag{}
-		logIndexStr := viper.GetString("log-index")
-
-		if logIndexStr != "" {
-			if err := logIndex.Set(logIndexStr); err != nil {
-				return err
-			}
-			indexGiven = true
-		}
-	}
-	// we will need artifact, public-key, signature
-	typeStr := viper.GetString("type")
-	entry := viper.GetString("entry")
-
-	artifact := fileOrURLFlag{}
-	artifactStr := viper.GetString("artifact")
-	if artifactStr != "" {
-		if err := artifact.Set(artifactStr); err != nil {
-			return err
-		}
-	}
-
-	signature := viper.GetString("signature")
-	publicKey := viper.GetString("public-key")
-
-	if entry == "" && artifact.String() == "" {
-		if (uuidGiven && uuidValid) || (indexGiven && indexValid) {
-			return nil
-		}
-		return errors.New("either 'entry' or 'artifact' must be specified")
-	}
-
-	if entry == "" {
-		if signature == "" && typeStr == "rekord" {
-			return errors.New("--signature is required when --artifact is used")
-		}
-		if publicKey == "" && typeStr != "jar" && typeStr != "rfc3161" {
-			return errors.New("--public-key is required when --artifact is used")
-		}
-	}
-
-	return nil
-}
-
-func CreateHelmFromPFlags() (models.ProposedEntry, error) {
-	//TODO: how to select version of item to create
-	returnVal := models.Helm{}
-	re := new(helm_v001.V001Entry)
-
-	helm := viper.GetString("entry")
-	if helm != "" {
-		var helmBytes []byte
-		provURL, err := url.Parse(helm)
-		if err == nil && provURL.IsAbs() {
-			/* #nosec G107 */
-			helmResp, err := http.Get(helm)
-			if err != nil {
-				return nil, fmt.Errorf("error fetching 'helm': %w", err)
-			}
-			defer helmResp.Body.Close()
-			helmBytes, err = ioutil.ReadAll(helmResp.Body)
-			if err != nil {
-				return nil, fmt.Errorf("error fetching 'provenance file': %w", err)
-			}
-		} else {
-			helmBytes, err = ioutil.ReadFile(filepath.Clean(helm))
-			if err != nil {
-				return nil, fmt.Errorf("error processing 'helm' file: %w", err)
-			}
-		}
-		if err := json.Unmarshal(helmBytes, &returnVal); err != nil {
-			return nil, fmt.Errorf("error parsing helm file: %w", err)
-		}
-	} else {
-		// we will need provenance file and public-key
-		re.HelmObj = models.HelmV001Schema{}
-		re.HelmObj.Chart = &models.HelmV001SchemaChart{}
-		re.HelmObj.Chart.Provenance = &models.HelmV001SchemaChartProvenance{}
-
-		artifact := viper.GetString("artifact")
-		dataURL, err := url.Parse(artifact)
-		if err == nil && dataURL.IsAbs() {
-			re.HelmObj.Chart.Provenance.URL = strfmt.URI(artifact)
-		} else {
-			artifactBytes, err := ioutil.ReadFile(filepath.Clean(artifact))
-			if err != nil {
-				return nil, fmt.Errorf("error reading artifact file: %w", err)
-			}
-			re.HelmObj.Chart.Provenance.Content = strfmt.Base64(artifactBytes)
-		}
-
-		re.HelmObj.PublicKey = &models.HelmV001SchemaPublicKey{}
-		publicKey := viper.GetString("public-key")
-		keyURL, err := url.Parse(publicKey)
-		if err == nil && keyURL.IsAbs() {
-			re.HelmObj.PublicKey.URL = strfmt.URI(publicKey)
-		} else {
-			keyBytes, err := ioutil.ReadFile(filepath.Clean(publicKey))
-			if err != nil {
-				return nil, fmt.Errorf("error reading public key file: %w", err)
-			}
-			re.HelmObj.PublicKey.Content = strfmt.Base64(keyBytes)
-		}
-
-		if err := re.Validate(); err != nil {
-			return nil, err
-		}
-
-		if re.HasExternalEntities() {
-			if err := re.FetchExternalEntities(context.Background()); err != nil {
-				return nil, fmt.Errorf("error retrieving external entities: %v", err)
-			}
-		}
-
-		returnVal.APIVersion = swag.String(re.APIVersion())
-		returnVal.Spec = re.HelmObj
-	}
-
-	return &returnVal, nil
-}
-
-func CreateJarFromPFlags() (models.ProposedEntry, error) {
-	//TODO: how to select version of item to create
-	returnVal := models.Jar{}
-	re := new(jar_v001.V001Entry)
-
-	jar := viper.GetString("entry")
-	if jar != "" {
-		var jarBytes []byte
-		jarURL, err := url.Parse(jar)
-		if err == nil && jarURL.IsAbs() {
-			/* #nosec G107 */
-			jarResp, err := http.Get(jar)
-			if err != nil {
-				return nil, fmt.Errorf("error fetching 'jar': %w", err)
-			}
-			defer jarResp.Body.Close()
-			jarBytes, err = ioutil.ReadAll(jarResp.Body)
-			if err != nil {
-				return nil, fmt.Errorf("error fetching 'jar': %w", err)
-			}
-		} else {
-			jarBytes, err = ioutil.ReadFile(filepath.Clean(jar))
-			if err != nil {
-				return nil, fmt.Errorf("error processing 'jar' file: %w", err)
-			}
-		}
-		if err := json.Unmarshal(jarBytes, &returnVal); err != nil {
-			return nil, fmt.Errorf("error parsing jar file: %w", err)
-		}
-	} else {
-		// we will need only the artifact; public-key & signature are embedded in JAR
-		re.JARModel = models.JarV001Schema{}
-		re.JARModel.Archive = &models.JarV001SchemaArchive{}
-
-		artifact := viper.GetString("artifact")
-		dataURL, err := url.Parse(artifact)
-		if err == nil && dataURL.IsAbs() {
-			re.JARModel.Archive.URL = strfmt.URI(artifact)
-			re.JARModel.Archive.Hash = &models.JarV001SchemaArchiveHash{
-				Algorithm: swag.String(models.JarV001SchemaArchiveHashAlgorithmSha256),
-				Value:     swag.String(viper.GetString("artifact-hash")),
-			}
-		} else {
-			artifactBytes, err := ioutil.ReadFile(filepath.Clean(artifact))
-			if err != nil {
-				return nil, fmt.Errorf("error reading artifact file: %w", err)
-			}
-
-			//TODO: ensure this is a valid JAR file; look for META-INF/MANIFEST.MF?
-			re.JARModel.Archive.Content = strfmt.Base64(artifactBytes)
-		}
-
-		if err := re.Validate(); err != nil {
-			return nil, err
-		}
-
-		if re.HasExternalEntities() {
-			if err := re.FetchExternalEntities(context.Background()); err != nil {
-				return nil, fmt.Errorf("error retrieving external entities: %v", err)
-			}
-		}
-
-		returnVal.APIVersion = swag.String(re.APIVersion())
-		returnVal.Spec = re.JARModel
-	}
+type FlagType string
+
+const (
+	uuidFlag      FlagType = "uuid"
+	shaFlag       FlagType = "sha"
+	emailFlag     FlagType = "email"
+	logIndexFlag  FlagType = "logIndex"
+	pkiFormatFlag FlagType = "pkiFormat"
+	typeFlag      FlagType = "type"
+	fileFlag      FlagType = "file"
+	urlFlag       FlagType = "url"
+	fileOrURLFlag FlagType = "fileOrURL"
+	oidFlag       FlagType = "oid"
+	formatFlag    FlagType = "format"
+)
 
-	return &returnVal, nil
-}
+type newPFlagValueFunc func() pflag.Value
 
-func CreateIntotoFromPFlags() (models.ProposedEntry, error) {
-	//TODO: how to select version of item to create
-	returnVal := models.Intoto{}
+var pflagValueFuncMap map[FlagType]newPFlagValueFunc
 
-	intoto := viper.GetString("artifact")
-	b, err := ioutil.ReadFile(filepath.Clean(intoto))
-	if err != nil {
-		return nil, err
-	}
-	publicKey := viper.GetString("public-key")
-	keyBytes, err := ioutil.ReadFile(filepath.Clean(publicKey))
-	if err != nil {
-		return nil, fmt.Errorf("error reading public key file: %w", err)
-	}
-	kb := strfmt.Base64(keyBytes)
-
-	re := intoto_v001.V001Entry{
-		IntotoObj: models.IntotoV001Schema{
-			Content: &models.IntotoV001SchemaContent{
-				Envelope: string(b),
-			},
-			PublicKey: &kb,
+// TODO: unit tests for all of this
+func initializePFlagMap() {
+	pflagValueFuncMap = map[FlagType]newPFlagValueFunc{
+		uuidFlag: func() pflag.Value {
+			// this corresponds to the merkle leaf hash of entries, which is represented by a 64 character hexadecimal string
+			return valueFactory(uuidFlag, validateString("required,len=64,hexadecimal"), "")
+		},
+		shaFlag: func() pflag.Value {
+			// this validates a valid sha256 checksum which is optionally prefixed with 'sha256:'
+			return valueFactory(shaFlag, validateSHA256Value, "")
+		},
+		emailFlag: func() pflag.Value {
+			// this validates an email address
+			return valueFactory(emailFlag, validateString("required,email"), "")
+		},
+		logIndexFlag: func() pflag.Value {
+			// this checks for a valid integer >= 0
+			return valueFactory(logIndexFlag, validateLogIndex, "")
+		},
+		pkiFormatFlag: func() pflag.Value {
+			// this ensures a PKI implementation exists for the requested format
+			return valueFactory(pkiFormatFlag, validateString(fmt.Sprintf("required,oneof=%v", strings.Join(pki.SupportedFormats(), " "))), "pgp")
+		},
+		typeFlag: func() pflag.Value {
+			// this ensures the type of the log entry matches a type supported in the CLI
+			return valueFactory(typeFlag, validateTypeFlag, "rekord")
+		},
+		fileFlag: func() pflag.Value {
+			// this validates that the file exists and can be opened by the current uid
+			return valueFactory(fileFlag, validateString("required,file"), "")
+		},
+		urlFlag: func() pflag.Value {
+			// this validates that the string is a valid http/https URL
+			return valueFactory(urlFlag, validateString("required,url,startswith=http|startswith=https"), "")
+		},
+		fileOrURLFlag: func() pflag.Value {
+			// applies logic of fileFlag OR urlFlag validators from above
+			return valueFactory(fileOrURLFlag, validateFileOrURL, "")
+		},
+		oidFlag: func() pflag.Value {
+			// this validates for an OID, which is a sequence of positive integers separated by periods
+			return valueFactory(oidFlag, validateOID, "")
+		},
+		formatFlag: func() pflag.Value {
+			// this validates the output format requested
+			return valueFactory(formatFlag, validateString("required,oneof=json default"), "")
 		},
 	}
-
-	returnVal.Spec = re.IntotoObj
-	returnVal.APIVersion = swag.String(re.APIVersion())
-
-	return &returnVal, nil
-}
-
-func CreateRFC3161FromPFlags() (models.ProposedEntry, error) {
-	//TODO: how to select version of item to create
-	rfc3161 := viper.GetString("artifact")
-	b, err := ioutil.ReadFile(filepath.Clean(rfc3161))
-	if err != nil {
-		return nil, fmt.Errorf("error reading public key file: %w", err)
-	}
-
-	return rfc3161_v001.NewEntryFromBytes(b), nil
 }
 
-func CreateRpmFromPFlags() (models.ProposedEntry, error) {
-	//TODO: how to select version of item to create
-	returnVal := models.Rpm{}
-	re := new(rpm_v001.V001Entry)
-
-	rpm := viper.GetString("entry")
-	if rpm != "" {
-		var rpmBytes []byte
-		rpmURL, err := url.Parse(rpm)
-		if err == nil && rpmURL.IsAbs() {
-			/* #nosec G107 */
-			rpmResp, err := http.Get(rpm)
-			if err != nil {
-				return nil, fmt.Errorf("error fetching 'rpm': %w", err)
-			}
-			defer rpmResp.Body.Close()
-			rpmBytes, err = ioutil.ReadAll(rpmResp.Body)
-			if err != nil {
-				return nil, fmt.Errorf("error fetching 'rpm': %w", err)
-			}
-		} else {
-			rpmBytes, err = ioutil.ReadFile(filepath.Clean(rpm))
-			if err != nil {
-				return nil, fmt.Errorf("error processing 'rpm' file: %w", err)
-			}
-		}
-		if err := json.Unmarshal(rpmBytes, &returnVal); err != nil {
-			return nil, fmt.Errorf("error parsing rpm file: %w", err)
+// NewFlagValue creates a new pflag.Value for the specified type with the specified default value.
+// If a default value is not desired, pass "" for defaultVal.
+func NewFlagValue(flagType FlagType, defaultVal string) pflag.Value {
+	valFunc := pflagValueFuncMap[flagType]
+	val := valFunc()
+	if defaultVal != "" {
+		if err := val.Set(defaultVal); err != nil {
+			log.Fatal(errors.Wrap(err, "initializing flag"))
 		}
-	} else {
-		// we will need artifact, public-key, signature
-		re.RPMModel = models.RpmV001Schema{}
-		re.RPMModel.Package = &models.RpmV001SchemaPackage{}
-
-		artifact := viper.GetString("artifact")
-		dataURL, err := url.Parse(artifact)
-		if err == nil && dataURL.IsAbs() {
-			re.RPMModel.Package.URL = strfmt.URI(artifact)
-		} else {
-			artifactBytes, err := ioutil.ReadFile(filepath.Clean(artifact))
-			if err != nil {
-				return nil, fmt.Errorf("error reading artifact file: %w", err)
-			}
-			re.RPMModel.Package.Content = strfmt.Base64(artifactBytes)
-		}
-
-		re.RPMModel.PublicKey = &models.RpmV001SchemaPublicKey{}
-		publicKey := viper.GetString("public-key")
-		keyURL, err := url.Parse(publicKey)
-		if err == nil && keyURL.IsAbs() {
-			re.RPMModel.PublicKey.URL = strfmt.URI(publicKey)
-		} else {
-			keyBytes, err := ioutil.ReadFile(filepath.Clean(publicKey))
-			if err != nil {
-				return nil, fmt.Errorf("error reading public key file: %w", err)
-			}
-			re.RPMModel.PublicKey.Content = strfmt.Base64(keyBytes)
-		}
-
-		if err := re.Validate(); err != nil {
-			return nil, err
-		}
-
-		if re.HasExternalEntities() {
-			if err := re.FetchExternalEntities(context.Background()); err != nil {
-				return nil, fmt.Errorf("error retrieving external entities: %v", err)
-			}
-		}
-
-		returnVal.APIVersion = swag.String(re.APIVersion())
-		returnVal.Spec = re.RPMModel
 	}
-
-	return &returnVal, nil
+	return val
 }
 
-func CreateAlpineFromPFlags() (models.ProposedEntry, error) {
-	//TODO: how to select version of item to create
-	returnVal := models.Alpine{}
-	re := new(alpine_v001.V001Entry)
-
-	apk := viper.GetString("entry")
-	if apk != "" {
-		var apkBytes []byte
-		apkURL, err := url.Parse(apk)
-		if err == nil && apkURL.IsAbs() {
-			/* #nosec G107 */
-			apkResp, err := http.Get(apk)
-			if err != nil {
-				return nil, fmt.Errorf("error fetching 'alpine': %w", err)
-			}
-			defer apkResp.Body.Close()
-			apkBytes, err = ioutil.ReadAll(apkResp.Body)
-			if err != nil {
-				return nil, fmt.Errorf("error fetching 'alpine': %w", err)
-			}
-		} else {
-			apkBytes, err = ioutil.ReadFile(filepath.Clean(apk))
-			if err != nil {
-				return nil, fmt.Errorf("error processing 'alpine' file: %w", err)
-			}
-		}
-		if err := json.Unmarshal(apkBytes, &returnVal); err != nil {
-			return nil, fmt.Errorf("error parsing alpine file: %w", err)
-		}
-	} else {
-		// we will need artifact, public-key, signature
-		re.AlpineModel = models.AlpineV001Schema{}
-		re.AlpineModel.Package = &models.AlpineV001SchemaPackage{}
+type validationFunc func(string) error
 
-		artifact := viper.GetString("artifact")
-		dataURL, err := url.Parse(artifact)
-		if err == nil && dataURL.IsAbs() {
-			re.AlpineModel.Package.URL = strfmt.URI(artifact)
-		} else {
-			artifactBytes, err := ioutil.ReadFile(filepath.Clean(artifact))
-			if err != nil {
-				return nil, fmt.Errorf("error reading artifact file: %w", err)
-			}
-			re.AlpineModel.Package.Content = strfmt.Base64(artifactBytes)
-		}
-
-		re.AlpineModel.PublicKey = &models.AlpineV001SchemaPublicKey{}
-		publicKey := viper.GetString("public-key")
-		keyURL, err := url.Parse(publicKey)
-		if err == nil && keyURL.IsAbs() {
-			re.AlpineModel.PublicKey.URL = strfmt.URI(publicKey)
-		} else {
-			keyBytes, err := ioutil.ReadFile(filepath.Clean(publicKey))
-			if err != nil {
-				return nil, fmt.Errorf("error reading public key file: %w", err)
-			}
-			re.AlpineModel.PublicKey.Content = strfmt.Base64(keyBytes)
-		}
-
-		if err := re.Validate(); err != nil {
-			return nil, err
-		}
-
-		if re.HasExternalEntities() {
-			if err := re.FetchExternalEntities(context.Background()); err != nil {
-				return nil, fmt.Errorf("error retrieving external entities: %v", err)
-			}
-		}
-
-		returnVal.APIVersion = swag.String(re.APIVersion())
-		returnVal.Spec = re.AlpineModel
+func valueFactory(flagType FlagType, v validationFunc, defaultVal string) pflag.Value {
+	return &baseValue{
+		flagType:       flagType,
+		validationFunc: v,
+		value:          defaultVal,
 	}
-
-	return &returnVal, nil
 }
 
-func CreateRekordFromPFlags() (models.ProposedEntry, error) {
-	//TODO: how to select version of item to create
-	returnVal := models.Rekord{}
-	re := new(rekord_v001.V001Entry)
-
-	rekord := viper.GetString("entry")
-	if rekord != "" {
-		var rekordBytes []byte
-		rekordURL, err := url.Parse(rekord)
-		if err == nil && rekordURL.IsAbs() {
-			/* #nosec G107 */
-			rekordResp, err := http.Get(rekord)
-			if err != nil {
-				return nil, fmt.Errorf("error fetching 'rekord': %w", err)
-			}
-			defer rekordResp.Body.Close()
-			rekordBytes, err = ioutil.ReadAll(rekordResp.Body)
-			if err != nil {
-				return nil, fmt.Errorf("error fetching 'rekord': %w", err)
-			}
-		} else {
-			rekordBytes, err = ioutil.ReadFile(filepath.Clean(rekord))
-			if err != nil {
-				return nil, fmt.Errorf("error processing 'rekord' file: %w", err)
-			}
-		}
-		if err := json.Unmarshal(rekordBytes, &returnVal); err != nil {
-			return nil, fmt.Errorf("error parsing rekord file: %w", err)
-		}
-	} else {
-		// we will need artifact, public-key, signature
-		re.RekordObj.Data = &models.RekordV001SchemaData{}
-
-		artifact := viper.GetString("artifact")
-		dataURL, err := url.Parse(artifact)
-		if err == nil && dataURL.IsAbs() {
-			re.RekordObj.Data.URL = strfmt.URI(artifact)
-		} else {
-			artifactBytes, err := ioutil.ReadFile(filepath.Clean(artifact))
-			if err != nil {
-				return nil, fmt.Errorf("error reading artifact file: %w", err)
-			}
-			re.RekordObj.Data.Content = strfmt.Base64(artifactBytes)
-		}
-
-		re.RekordObj.Signature = &models.RekordV001SchemaSignature{}
-		pkiFormat := viper.GetString("pki-format")
-		switch pkiFormat {
-		case "pgp":
-			re.RekordObj.Signature.Format = models.RekordV001SchemaSignatureFormatPgp
-		case "minisign":
-			re.RekordObj.Signature.Format = models.RekordV001SchemaSignatureFormatMinisign
-		case "x509":
-			re.RekordObj.Signature.Format = models.RekordV001SchemaSignatureFormatX509
-		case "ssh":
-			re.RekordObj.Signature.Format = models.RekordV001SchemaSignatureFormatSSH
-		}
-		signature := viper.GetString("signature")
-		sigURL, err := url.Parse(signature)
-		if err == nil && sigURL.IsAbs() {
-			re.RekordObj.Signature.URL = strfmt.URI(signature)
-		} else {
-			signatureBytes, err := ioutil.ReadFile(filepath.Clean(signature))
-			if err != nil {
-				return nil, fmt.Errorf("error reading signature file: %w", err)
-			}
-			re.RekordObj.Signature.Content = strfmt.Base64(signatureBytes)
-		}
-
-		re.RekordObj.Signature.PublicKey = &models.RekordV001SchemaSignaturePublicKey{}
-		publicKey := viper.GetString("public-key")
-		keyURL, err := url.Parse(publicKey)
-		if err == nil && keyURL.IsAbs() {
-			re.RekordObj.Signature.PublicKey.URL = strfmt.URI(publicKey)
-		} else {
-			keyBytes, err := ioutil.ReadFile(filepath.Clean(publicKey))
-			if err != nil {
-				return nil, fmt.Errorf("error reading public key file: %w", err)
-			}
-			re.RekordObj.Signature.PublicKey.Content = strfmt.Base64(keyBytes)
-		}
-
-		if err := re.Validate(); err != nil {
-			return nil, err
-		}
-
-		if re.HasExternalEntities() {
-			if err := re.FetchExternalEntities(context.Background()); err != nil {
-				return nil, fmt.Errorf("error retrieving external entities: %v", err)
-			}
-		}
-
-		returnVal.APIVersion = swag.String(re.APIVersion())
-		returnVal.Spec = re.RekordObj
-	}
-
-	return &returnVal, nil
+// baseValue implements pflag.Value
+type baseValue struct {
+	flagType       FlagType
+	value          string
+	validationFunc validationFunc
 }
 
-type fileOrURLFlag struct {
-	value string
-	IsURL bool
+// Type returns the type of this Value
+func (b baseValue) Type() string {
+	return string(b.flagType)
 }
 
-func (f *fileOrURLFlag) String() string {
-	return f.value
+// String returns the string representation of this Value
+func (b baseValue) String() string {
+	return b.value
 }
 
-func (f *fileOrURLFlag) Set(s string) error {
-	if s == "" {
-		return errors.New("flag must be specified")
-	}
-	if _, err := os.Stat(filepath.Clean(s)); os.IsNotExist(err) {
-		u := urlFlag{}
-		if err := u.Set(s); err != nil {
-			return err
-		}
-		f.IsURL = true
+// Set validates the provided string against the appropriate validation rule
+// for b.flagType; if the string validates, it is stored in the Value and nil is returned.
+// Otherwise the validation error is returned but the state of the Value is not changed.
+func (b *baseValue) Set(s string) error {
+	if err := b.validationFunc(s); err != nil {
+		return err
 	}
-	f.value = s
+	b.value = s
 	return nil
 }
 
-func (f *fileOrURLFlag) Type() string {
-	return "fileOrURLFlag"
-}
-
-type typeFlag struct {
-	value string
-}
-
-func (t *typeFlag) Type() string {
-	return "typeFormat"
+// isURL returns true if the supplied value is a valid URL and false otherwise
+func isURL(v string) bool {
+	valGen := pflagValueFuncMap[urlFlag]
+	return valGen().Set(v) == nil
 }
 
-func (t *typeFlag) String() string {
-	return t.value
-}
+// validateSHA256Value ensures that the supplied string matches the following format:
+// [sha256:]<64 hexadecimal characters>
+// where [sha256:] is optional
+func validateSHA256Value(v string) error {
+	var prefix, hash string
 
-func (t *typeFlag) Set(s string) error {
-	set := map[string]struct{}{
-		"rekord":  {},
-		"rpm":     {},
-		"jar":     {},
-		"intoto":  {},
-		"rfc3161": {},
-		"alpine":  {},
-		"helm":    {},
-	}
-	if _, ok := set[s]; ok {
-		t.value = s
-		return nil
-	}
-	var types []string
-	for typeStr := range set {
-		types = append(types, typeStr)
+	split := strings.SplitN(v, ":", 2)
+	switch len(split) {
+	case 1:
+		hash = split[0]
+	case 2:
+		prefix = split[0]
+		hash = split[1]
 	}
-	return fmt.Errorf("value specified is invalid: [%s] supported values are: [%v]", s, strings.Join(types, ", "))
-}
 
-type pkiFormatFlag struct {
-	value string
-}
-
-func (f *pkiFormatFlag) Type() string {
-	return "pkiFormat"
-}
+	s := struct {
+		Prefix string `validate:"omitempty,oneof=sha256"`
+		Hash   string `validate:"required,len=64,hexadecimal"`
+	}{prefix, hash}
 
-func (f *pkiFormatFlag) String() string {
-	return f.value
+	return useValidator(shaFlag, s)
 }
 
-func (f *pkiFormatFlag) Set(s string) error {
-	set := map[string]struct{}{
-		"pgp":      {},
-		"minisign": {},
-		"x509":     {},
-		"ssh":      {},
-	}
-	if _, ok := set[s]; ok {
-		f.value = s
+// validateFileOrURL ensures the provided string is either a valid file path that can be opened or a valid URL
+func validateFileOrURL(v string) error {
+	valGen := pflagValueFuncMap[fileFlag]
+	if valGen().Set(v) == nil {
 		return nil
 	}
-	return fmt.Errorf("value specified is invalid: [%s] supported values are: [pgp, minisign, x509, ssh]", s)
-}
-
-type shaFlag struct {
-	hash string
-}
-
-func (s *shaFlag) String() string {
-	return s.hash
-}
-
-func (s *shaFlag) Set(v string) error {
-	if v == "" {
-		return errors.New("flag must be specified")
-	}
-	strToCheck := v
-	if strings.HasPrefix(v, "sha256:") {
-		strToCheck = strings.Replace(v, "sha256:", "", 1)
-	}
-	if _, err := hex.DecodeString(strToCheck); (err != nil) || (len(strToCheck) != 64) {
-		if err == nil {
-			err = errors.New("invalid length for value")
-		}
-		return fmt.Errorf("value specified is invalid: %w", err)
-	}
-	s.hash = v
-	return nil
-}
-
-func (s *shaFlag) Type() string {
-	return "sha"
-}
-
-type uuidFlag struct {
-	hash string
-}
-
-func (u *uuidFlag) String() string {
-	return u.hash
-}
-
-func (u *uuidFlag) Set(v string) error {
-	if v == "" {
-		return errors.New("flag must be specified")
-	}
-	if _, err := hex.DecodeString(v); (err != nil) || (len(v) != 64) {
-		if err == nil {
-			err = errors.New("invalid length for value")
-		}
-		return fmt.Errorf("value specified is invalid: %w", err)
-	}
-	u.hash = v
-	return nil
-}
-
-func (u *uuidFlag) Type() string {
-	return "uuid"
-}
-
-func addUUIDPFlags(cmd *cobra.Command, required bool) error {
-	cmd.Flags().Var(&uuidFlag{}, "uuid", "UUID of entry in transparency log (if known)")
-	if required {
-		if err := cmd.MarkFlagRequired("uuid"); err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-type logIndexFlag struct {
-	index string
+	valGen = pflagValueFuncMap[urlFlag]
+	return valGen().Set(v)
 }
 
-func (l *logIndexFlag) String() string {
-	return l.index
-}
-
-func (l *logIndexFlag) Set(v string) error {
-	if v == "" {
-		return errors.New("flag must be specified")
-	}
-	logIndexInt, err := strconv.ParseInt(v, 10, 0)
+// validateLogIndex ensures that the supplied string is a valid log index (integer >= 0)
+func validateLogIndex(v string) error {
+	i, err := strconv.Atoi(v)
 	if err != nil {
-		return fmt.Errorf("error parsing --log-index: %w", err)
-	} else if logIndexInt < 0 {
-		return errors.New("--log-index must be greater than or equal to 0")
+		return err
 	}
-	l.index = v
-	return nil
-}
+	l := struct {
+		Index int `validate:"required,gte=0"`
+	}{i}
 
-func (l *logIndexFlag) Type() string {
-	return "logIndex"
+	return useValidator(logIndexFlag, l)
 }
 
-func addLogIndexFlag(cmd *cobra.Command, required bool) error {
-	cmd.Flags().Var(&logIndexFlag{}, "log-index", "the index of the entry in the transparency log")
-	if required {
-		if err := cmd.MarkFlagRequired("log-index"); err != nil {
-			return err
-		}
-	}
-	return nil
-}
+// validateOID ensures that the supplied string is a valid ASN.1 object identifier
+func validateOID(v string) error {
+	o := struct {
+		Oid []string `validate:"dive,numeric"`
+	}{strings.Split(v, ".")}
 
-type emailFlag struct {
-	Email string `validate:"email"`
+	return useValidator(oidFlag, o)
 }
 
-func (e *emailFlag) String() string {
-	return e.Email
+// validateTypeFlag ensures that the string is in the format type(\.version)? and
+// that one of the types requested is implemented
+func validateTypeFlag(v string) error {
+	_, _, err := ParseTypeFlag(v)
+	return err
 }
 
-func (e *emailFlag) Set(v string) error {
-	if v == "" {
-		return errors.New("flag must be specified")
+// validateString returns a function that validates an input string against the specified tag,
+// as defined in the format supported by go-playground/validator
+func validateString(tag string) validationFunc {
+	return func(v string) error {
+		validator := validator.New()
+		return validator.Var(v, tag)
 	}
+}
 
-	e.Email = v
+// useValidator performs struct level validation on s as defined in the struct's tags using
+// the go-playground/validator library
+func useValidator(flagType FlagType, s interface{}) error {
 	validate := validator.New()
-	if err := validate.Struct(e); err != nil {
-		return fmt.Errorf("error parsing --email: %s", err)
+	if err := validate.Struct(s); err != nil {
+		return fmt.Errorf("error parsing %v flag: %w", flagType, 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 d56e7c8..625c749 100644
--- a/cmd/rekor-cli/app/pflags_test.go
+++ b/cmd/rekor-cli/app/pflags_test.go
@@ -16,6 +16,7 @@
 package app
 
 import (
+	"context"
 	"errors"
 	"io/ioutil"
 	"net/http"
@@ -25,7 +26,7 @@ import (
 	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
 
-	"github.com/sigstore/rekor/pkg/generated/models"
+	"github.com/sigstore/rekor/pkg/types"
 )
 
 func TestArtifactPFlags(t *testing.T) {
@@ -348,6 +349,7 @@ func TestArtifactPFlags(t *testing.T) {
 	}
 
 	for _, tc := range tests {
+		initializePFlagMap()
 		var blankCmd = &cobra.Command{}
 		if err := addArtifactPFlags(blankCmd); err != nil {
 			t.Fatalf("unexpected error adding flags in '%v': %v", tc.caseDesc, err)
@@ -396,27 +398,13 @@ func TestArtifactPFlags(t *testing.T) {
 				t.Errorf("unexpected result validating '%v': %v", tc.caseDesc, err)
 				continue
 			}
-			if !tc.uuidRequired && !tc.logIndexRequired {
-				var createFn func() (models.ProposedEntry, error)
-				switch tc.typeStr {
-				case "", "rekord":
-					createFn = CreateRekordFromPFlags
-				case "rpm":
-					createFn = CreateRpmFromPFlags
-				case "jar":
-					createFn = CreateJarFromPFlags
-				case "intoto":
-					createFn = CreateIntotoFromPFlags
-				case "rfc3161":
-					createFn = CreateRFC3161FromPFlags
-				case "alpine":
-					createFn = CreateAlpineFromPFlags
-				case "helm":
-					createFn = CreateHelmFromPFlags
-				default:
-					t.Fatalf("type %v not implemented", tc.typeStr)
+			if !tc.uuidRequired && !tc.logIndexRequired && tc.entry == "" {
+				typeStr, versionStr, err := ParseTypeFlag(viper.GetString("type"))
+				if err != nil {
+					t.Errorf("error parsing typeStr: %v", err)
 				}
-				if _, err := createFn(); err != nil {
+				props := CreatePropsFromPflags()
+				if _, err := types.NewProposedEntry(context.Background(), typeStr, versionStr, *props); err != nil {
 					t.Errorf("unexpected result in '%v' building entry: %v", tc.caseDesc, err)
 				}
 			}
@@ -577,7 +565,7 @@ func TestSearchPFlags(t *testing.T) {
 			caseDesc:              "invalid email",
 			email:                 "SignaMeseCat",
 			expectParseSuccess:    false,
-			expectValidateSuccess: true,
+			expectValidateSuccess: false,
 		},
 		{
 			caseDesc:              "no flags when either artifact, sha, public key, or email are needed",
@@ -587,6 +575,7 @@ func TestSearchPFlags(t *testing.T) {
 	}
 
 	for _, tc := range tests {
+		initializePFlagMap()
 		var blankCmd = &cobra.Command{}
 		if err := addSearchPFlags(blankCmd); err != nil {
 			t.Fatalf("unexpected error adding flags in '%v': %v", tc.caseDesc, err)
diff --git a/cmd/rekor-cli/app/root.go b/cmd/rekor-cli/app/root.go
index 4466edf..5114d18 100644
--- a/cmd/rekor-cli/app/root.go
+++ b/cmd/rekor-cli/app/root.go
@@ -16,16 +16,24 @@
 package app
 
 import (
-	"errors"
 	"fmt"
-	"net/url"
-	"os"
 	"strings"
 
 	homedir "github.com/mitchellh/go-homedir"
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
 	"github.com/spf13/viper"
+
+	"github.com/sigstore/rekor/pkg/log"
+
+	// these imports are to call the packages' init methods
+	_ "github.com/sigstore/rekor/pkg/types/alpine/v0.0.1"
+	_ "github.com/sigstore/rekor/pkg/types/helm/v0.0.1"
+	_ "github.com/sigstore/rekor/pkg/types/intoto/v0.0.1"
+	_ "github.com/sigstore/rekor/pkg/types/jar/v0.0.1"
+	_ "github.com/sigstore/rekor/pkg/types/rekord/v0.0.1"
+	_ "github.com/sigstore/rekor/pkg/types/rfc3161/v0.0.1"
+	_ "github.com/sigstore/rekor/pkg/types/rpm/v0.0.1"
 )
 
 var rootCmd = &cobra.Command{
@@ -40,24 +48,23 @@ var rootCmd = &cobra.Command{
 // Execute runs the base CLI
 func Execute() {
 	if err := rootCmd.Execute(); err != nil {
-		fmt.Println(err)
-		os.Exit(1)
+		log.CliLogger.Fatal(err)
 	}
 }
 
 func init() {
+	initializePFlagMap()
 	rootCmd.PersistentFlags().String("config", "", "config file (default is $HOME/.rekor.yaml)")
 	rootCmd.PersistentFlags().Bool("store_tree_state", true, "whether to store tree state in between invocations for additional verification")
 
-	rootCmd.PersistentFlags().Var(&urlFlag{url: "https://rekor.sigstore.dev"}, "rekor_server", "Server address:port")
-	rootCmd.PersistentFlags().Var(&formatFlag{format: "default"}, "format", "Command output format")
+	rootCmd.PersistentFlags().Var(NewFlagValue(urlFlag, "https://rekor.sigstore.dev"), "rekor_server", "Server address:port")
+	rootCmd.PersistentFlags().Var(NewFlagValue(formatFlag, "default"), "format", "Command output format")
 
 	rootCmd.PersistentFlags().String("api-key", "", "API key for rekor.sigstore.dev")
 
 	// these are bound here and not in PreRun so that all child commands can use them
 	if err := viper.BindPFlags(rootCmd.PersistentFlags()); err != nil {
-		fmt.Println(err)
-		os.Exit(1)
+		log.CliLogger.Fatal(err)
 	}
 }
 
@@ -102,64 +109,8 @@ func initConfig(cmd *cobra.Command) error {
 			return err
 		}
 	} else if viper.GetString("format") == "default" {
-		fmt.Println("Using config file:", viper.ConfigFileUsed())
+		log.CliLogger.Infof("Using config file:", viper.ConfigFileUsed())
 	}
 
 	return nil
 }
-
-type urlFlag struct {
-	url string
-}
-
-func (u *urlFlag) String() string {
-	return u.url
-}
-
-func (u *urlFlag) Set(s string) error {
-	if s == "" {
-		return errors.New("flag must be specified")
-	}
-	url, err := url.Parse(s)
-	if err != nil {
-		return fmt.Errorf("malformed URL: %w", err)
-	}
-	if !url.IsAbs() {
-		return fmt.Errorf("URL must be absolute, got %s", s)
-	}
-	lowercaseScheme := strings.ToLower(url.Scheme)
-	if lowercaseScheme != "http" && lowercaseScheme != "https" {
-		return fmt.Errorf("URL must be a valid HTTP or HTTPS URL, got %s", s)
-	}
-	u.url = s
-	return nil
-}
-
-func (u *urlFlag) Type() string {
-	return "url"
-}
-
-type formatFlag struct {
-	format string
-}
-
-func (f *formatFlag) String() string {
-	return f.format
-}
-
-func (f *formatFlag) Set(s string) error {
-	choices := map[string]struct{}{"default": {}, "json": {}}
-	if s == "" {
-		f.format = "default"
-		return nil
-	}
-	if _, ok := choices[s]; ok {
-		f.format = s
-		return nil
-	}
-	return fmt.Errorf("invalid flag value: %s, valid values are [default, json]", s)
-}
-
-func (f *formatFlag) Type() string {
-	return "format"
-}
diff --git a/cmd/rekor-cli/app/search.go b/cmd/rekor-cli/app/search.go
index d39792b..3bd49d9 100644
--- a/cmd/rekor-cli/app/search.go
+++ b/cmd/rekor-cli/app/search.go
@@ -18,6 +18,7 @@ package app
 import (
 	"crypto/sha256"
 	"encoding/hex"
+	"errors"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -53,6 +54,37 @@ func (s *searchCmdOutput) String() string {
 	return str
 }
 
+func addSearchPFlags(cmd *cobra.Command) error {
+	cmd.Flags().Var(NewFlagValue(pkiFormatFlag, ""), "pki-format", "format of the signature and/or public key")
+
+	cmd.Flags().Var(NewFlagValue(fileOrURLFlag, ""), "public-key", "path or URL to public key file")
+
+	cmd.Flags().Var(NewFlagValue(fileOrURLFlag, ""), "artifact", "path or URL to artifact file")
+
+	cmd.Flags().Var(NewFlagValue(shaFlag, ""), "sha", "the SHA256 sum of the artifact")
+
+	cmd.Flags().Var(NewFlagValue(emailFlag, ""), "email", "email associated with the public key's subject")
+	return nil
+}
+
+func validateSearchPFlags() error {
+	artifactStr := viper.GetString("artifact")
+
+	publicKey := viper.GetString("public-key")
+	sha := viper.GetString("sha")
+	email := viper.GetString("email")
+
+	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") == "" {
+			return errors.New("pki-format must be specified if searching by public-key")
+		}
+	}
+	return nil
+}
+
 // searchCmd represents the get command
 var searchCmd = &cobra.Command{
 	Use:   "search",
@@ -61,16 +93,16 @@ var searchCmd = &cobra.Command{
 	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)
+			log.CliLogger.Fatal("Error initializing cmd line args: ", err)
 		}
 		if err := validateSearchPFlags(); err != nil {
-			log.Logger.Error(err)
+			log.CliLogger.Error(err)
 			_ = cmd.Help()
 			os.Exit(1)
 		}
 	},
 	Run: format.WrapCmd(func(args []string) (interface{}, error) {
-		log := log.Logger
+		log := log.CliLogger
 		rekorClient, err := client.GetRekorClient(viper.GetString("rekor_server"))
 		if err != nil {
 			return nil, err
@@ -84,25 +116,21 @@ var searchCmd = &cobra.Command{
 		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 {
+			if isURL(artifactStr) {
 				/* #nosec G107 */
-				resp, err := http.Get(artifact.String())
+				resp, err := http.Get(artifactStr)
 				if err != nil {
-					return nil, fmt.Errorf("error fetching '%v': %w", artifact.String(), err)
+					return nil, fmt.Errorf("error fetching '%v': %w", artifactStr, err)
 				}
 				defer resp.Body.Close()
 				tee = io.TeeReader(resp.Body, hasher)
 			} else {
-				file, err := os.Open(filepath.Clean(artifact.String()))
+				file, err := os.Open(filepath.Clean(artifactStr))
 				if err != nil {
-					return nil, fmt.Errorf("error opening file '%v': %w", artifact.String(), err)
+					return nil, fmt.Errorf("error opening file '%v': %w", artifactStr, err)
 				}
 				defer func() {
 					if err := file.Close(); err != nil {
@@ -113,7 +141,7 @@ var searchCmd = &cobra.Command{
 				tee = io.TeeReader(file, hasher)
 			}
 			if _, err := ioutil.ReadAll(tee); err != nil {
-				return nil, fmt.Errorf("error processing '%v': %w", artifact.String(), err)
+				return nil, fmt.Errorf("error processing '%v': %w", artifactStr, err)
 			}
 
 			hashVal := strings.ToLower(hex.EncodeToString(hasher.Sum(nil)))
@@ -136,14 +164,11 @@ var searchCmd = &cobra.Command{
 			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())
+			publicKeyStr := viper.GetString("public-key")
+			if isURL(publicKeyStr) {
+				params.Query.PublicKey.URL = strfmt.URI(publicKeyStr)
 			} else {
-				keyBytes, err := ioutil.ReadFile(filepath.Clean(publicKey.String()))
+				keyBytes, err := ioutil.ReadFile(filepath.Clean(publicKeyStr))
 				if err != nil {
 					return nil, fmt.Errorf("error reading public key file: %w", err)
 				}
@@ -175,8 +200,9 @@ var searchCmd = &cobra.Command{
 }
 
 func init() {
+	initializePFlagMap()
 	if err := addSearchPFlags(searchCmd); err != nil {
-		log.Logger.Fatal("Error parsing cmd line args:", err)
+		log.CliLogger.Fatal("Error parsing cmd line args:", err)
 	}
 
 	rootCmd.AddCommand(searchCmd)
diff --git a/cmd/rekor-cli/app/timestamp.go b/cmd/rekor-cli/app/timestamp.go
index e081b14..9487093 100644
--- a/cmd/rekor-cli/app/timestamp.go
+++ b/cmd/rekor-cli/app/timestamp.go
@@ -23,7 +23,6 @@ import (
 	"errors"
 	"fmt"
 	"io/ioutil"
-	"os"
 	"path/filepath"
 	"strconv"
 	"strings"
@@ -40,59 +39,11 @@ import (
 	"github.com/spf13/viper"
 )
 
-type fileFlag struct {
-	value string
-}
-
-func (f *fileFlag) String() string {
-	return f.value
-}
-
-func (f *fileFlag) Set(s string) error {
-	if s == "" {
-		return errors.New("flag must be specified")
-	}
-	if _, err := os.Stat(filepath.Clean(s)); os.IsNotExist(err) {
-		return err
-	}
-	f.value = s
-	return nil
-}
-
-func (f *fileFlag) Type() string {
-	return "fileFlag"
-}
-
-type oidFlag struct {
-	value asn1.ObjectIdentifier
-}
-
-func (f *oidFlag) String() string {
-	return f.value.String()
-}
-
-func (f *oidFlag) Set(s string) error {
-	parts := strings.Split(s, ".")
-	f.value = make(asn1.ObjectIdentifier, len(parts))
-	for i, part := range parts {
-		num, err := strconv.Atoi(part)
-		if err != nil {
-			return errors.New("error parsing OID")
-		}
-		f.value[i] = num
-	}
-	return nil
-}
-
-func (f *oidFlag) Type() string {
-	return "oidFlag"
-}
-
 func addTimestampFlags(cmd *cobra.Command) error {
-	cmd.Flags().Var(&fileFlag{}, "artifact", "path to an artifact to timestamp")
-	cmd.Flags().Var(&uuidFlag{}, "artifact-hash", "hex encoded SHA256 hash of the the artifact to timestamp")
+	cmd.Flags().Var(NewFlagValue(fileFlag, ""), "artifact", "path to an artifact to timestamp")
+	cmd.Flags().Var(NewFlagValue(shaFlag, ""), "artifact-hash", "hex encoded SHA256 hash of the the artifact to timestamp")
 	cmd.Flags().Bool("nonce", true, "specify a pseudo-random nonce in the request")
-	cmd.Flags().Var(&oidFlag{}, "tsa-policy", "optional dotted OID notation for the policy that the TSA should use to create the response")
+	cmd.Flags().Var(NewFlagValue(oidFlag, ""), "tsa-policy", "optional dotted OID notation for the policy that the TSA should use to create the response")
 
 	cmd.Flags().String("out", "response.tsr", "path to a file to write response.")
 
@@ -108,21 +59,6 @@ func validateTimestampFlags() error {
 		return errors.New("artifact or hash to timestamp must be specified")
 	}
 
-	if digestStr != "" {
-		digest := uuidFlag{}
-		if err := digest.Set(digestStr); err != nil {
-			return err
-		}
-	}
-
-	policyStr := viper.GetString("tsa-policy")
-	if policyStr != "" {
-		oid := oidFlag{}
-		if err := oid.Set(policyStr); err != nil {
-			return err
-		}
-	}
-
 	return nil
 }
 
@@ -136,11 +72,12 @@ func createRequestFromFlags() (*pkcs9.TimeStampReq, error) {
 		Hash: crypto.SHA256,
 	}
 	if policyStr != "" {
-		oid := oidFlag{}
-		if err := oid.Set(policyStr); err != nil {
-			return nil, err
+		var oidInts []int
+		for _, v := range strings.Split(policyStr, ".") {
+			i, _ := strconv.Atoi(v)
+			oidInts = append(oidInts, i)
 		}
-		opts.TSAPolicyOid = oid.value
+		opts.TSAPolicyOid = oidInts
 	}
 	if viper.GetBool("nonce") {
 		opts.Nonce = x509tools.MakeSerial()
@@ -193,7 +130,7 @@ var timestampCmd = &cobra.Command{
 	Long:  "Generates and uploads (WIP) an RFC 3161 timestamp response to the log. The timestamp response can be verified locally using Rekor's timestamping cert chain.",
 	PreRunE: func(cmd *cobra.Command, args []string) error {
 		if err := viper.BindPFlags(cmd.Flags()); err != nil {
-			log.Logger.Fatal("Error initializing cmd line args: ", err)
+			log.CliLogger.Fatal("Error initializing cmd line args: ", err)
 		}
 		if err := validateTimestampFlags(); err != nil {
 			log.Logger.Error(err)
@@ -253,8 +190,9 @@ var timestampCmd = &cobra.Command{
 }
 
 func init() {
+	initializePFlagMap()
 	if err := addTimestampFlags(timestampCmd); err != nil {
-		log.Logger.Fatal("Error parsing cmd line args: ", err)
+		log.CliLogger.Fatal("Error parsing cmd line args: ", err)
 	}
 
 	rootCmd.AddCommand(timestampCmd)
diff --git a/cmd/rekor-cli/app/upload.go b/cmd/rekor-cli/app/upload.go
index efea229..f7a97de 100644
--- a/cmd/rekor-cli/app/upload.go
+++ b/cmd/rekor-cli/app/upload.go
@@ -19,7 +19,12 @@ import (
 	"context"
 	"crypto/ecdsa"
 	"crypto/sha256"
+	"encoding/json"
 	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"path/filepath"
 
 	"github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer"
 	"github.com/go-openapi/swag"
@@ -33,6 +38,7 @@ import (
 	"github.com/sigstore/rekor/pkg/generated/client/entries"
 	"github.com/sigstore/rekor/pkg/generated/models"
 	"github.com/sigstore/rekor/pkg/log"
+	"github.com/sigstore/rekor/pkg/types"
 	"github.com/sigstore/rekor/pkg/util"
 )
 
@@ -70,49 +76,46 @@ var uploadCmd = &cobra.Command{
 		if err != nil {
 			return nil, err
 		}
+		var entry models.ProposedEntry
 		params := entries.NewCreateLogEntryParams()
 
-		var entry models.ProposedEntry
-		switch viper.GetString("type") {
-		case "rekord":
-			entry, err = CreateRekordFromPFlags()
-			if err != nil {
-				return nil, err
-			}
-		case "rpm":
-			entry, err = CreateRpmFromPFlags()
-			if err != nil {
-				return nil, err
-			}
-		case "jar":
-			entry, err = CreateJarFromPFlags()
-			if err != nil {
-				return nil, err
+		entryStr := viper.GetString("entry")
+		if entryStr != "" {
+			var entryBytes []byte
+			entryURL, err := url.Parse(entryStr)
+			if err == nil && entryURL.IsAbs() {
+				/* #nosec G107 */
+				entryResp, err := http.Get(entryStr)
+				if err != nil {
+					return nil, fmt.Errorf("error fetching entry: %w", err)
+				}
+				defer entryResp.Body.Close()
+				entryBytes, err = ioutil.ReadAll(entryResp.Body)
+				if err != nil {
+					return nil, fmt.Errorf("error fetching entry: %w", err)
+				}
+			} else {
+				entryBytes, err = ioutil.ReadFile(filepath.Clean(entryStr))
+				if err != nil {
+					return nil, fmt.Errorf("error processing entry file: %w", err)
+				}
 			}
-		case "intoto":
-			entry, err = CreateIntotoFromPFlags()
-			if err != nil {
-				return nil, err
+			if err := json.Unmarshal(entryBytes, &entry); err != nil {
+				return nil, fmt.Errorf("error parsing entry file: %w", err)
 			}
-		case "rfc3161":
-			entry, err = CreateRFC3161FromPFlags()
+		} else {
+			typeStr, versionStr, err := ParseTypeFlag(viper.GetString("type"))
 			if err != nil {
 				return nil, err
 			}
-		case "alpine":
-			entry, err = CreateAlpineFromPFlags()
-			if err != nil {
-				return nil, err
-			}
-		case "helm":
-			entry, err = CreateHelmFromPFlags()
+
+			props := CreatePropsFromPflags()
+
+			entry, err = types.NewProposedEntry(context.Background(), typeStr, versionStr, *props)
 			if err != nil {
 				return nil, err
 			}
-		default:
-			return nil, errors.New("unknown type specified")
 		}
-
 		params.SetProposedEntry(entry)
 
 		resp, err := rekorClient.Entries.CreateLogEntry(params)
@@ -187,8 +190,9 @@ func verifyLogEntry(ctx context.Context, rekorClient *genclient.Rekor, logEntry
 }
 
 func init() {
+	initializePFlagMap()
 	if err := addArtifactPFlags(uploadCmd); err != nil {
-		log.Logger.Fatal("Error parsing cmd line args:", err)
+		log.CliLogger.Fatal("Error parsing cmd line args:", err)
 	}
 
 	rootCmd.AddCommand(uploadCmd)
diff --git a/cmd/rekor-cli/app/verify.go b/cmd/rekor-cli/app/verify.go
index f3005ad..1ef3bdb 100644
--- a/cmd/rekor-cli/app/verify.go
+++ b/cmd/rekor-cli/app/verify.go
@@ -16,8 +16,8 @@
 package app
 
 import (
+	"context"
 	"encoding/hex"
-	"errors"
 	"fmt"
 	"math/bits"
 	"strconv"
@@ -32,6 +32,7 @@ import (
 	"github.com/sigstore/rekor/pkg/generated/client/entries"
 	"github.com/sigstore/rekor/pkg/generated/models"
 	"github.com/sigstore/rekor/pkg/log"
+	"github.com/sigstore/rekor/pkg/types"
 )
 
 type verifyCmdOutput struct {
@@ -104,40 +105,16 @@ var verifyCmd = &cobra.Command{
 			}
 			searchLogQuery.LogIndexes = []*int64{&logIndexInt}
 		} else {
-			var entry models.ProposedEntry
-			switch viper.GetString("type") {
-			case "rekord":
-				entry, err = CreateRekordFromPFlags()
-				if err != nil {
-					return nil, err
-				}
-			case "rpm":
-				entry, err = CreateRpmFromPFlags()
-				if err != nil {
-					return nil, err
-				}
-			case "jar":
-				entry, err = CreateJarFromPFlags()
-				if err != nil {
-					return nil, err
-				}
-			case "intoto":
-				entry, err = CreateIntotoFromPFlags()
-				if err != nil {
-					return nil, err
-				}
-			case "rfc3161":
-				entry, err = CreateRFC3161FromPFlags()
-				if err != nil {
-					return nil, err
-				}
-			case "alpine":
-				entry, err = CreateAlpineFromPFlags()
-				if err != nil {
-					return nil, err
-				}
-			default:
-				return nil, errors.New("invalid type specified")
+			typeStr, versionStr, err := ParseTypeFlag(viper.GetString("type"))
+			if err != nil {
+				return nil, err
+			}
+
+			props := CreatePropsFromPflags()
+
+			entry, err := types.NewProposedEntry(context.Background(), typeStr, versionStr, *props)
+			if err != nil {
+				return nil, err
 			}
 
 			entries := []models.ProposedEntry{entry}
@@ -186,14 +163,15 @@ var verifyCmd = &cobra.Command{
 }
 
 func init() {
+	initializePFlagMap()
 	if err := addArtifactPFlags(verifyCmd); err != nil {
-		log.Logger.Fatal("Error parsing cmd line args:", err)
+		log.CliLogger.Fatal("Error parsing cmd line args:", err)
 	}
 	if err := addUUIDPFlags(verifyCmd, false); err != nil {
-		log.Logger.Fatal("Error parsing cmd line args:", err)
+		log.CliLogger.Fatal("Error parsing cmd line args:", err)
 	}
 	if err := addLogIndexFlag(verifyCmd, false); err != nil {
-		log.Logger.Fatal("Error parsing cmd line args:", err)
+		log.CliLogger.Fatal("Error parsing cmd line args:", err)
 	}
 
 	rootCmd.AddCommand(verifyCmd)
diff --git a/docker-compose.yml b/docker-compose.yml
index 82ceb9f..5a5af53 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -93,7 +93,7 @@ services:
       # "--log_type=prod",
       ]
     volumes:
-    - "/var/run/attestations:/var/run/attestations"
+    - "/var/run/attestations:/var/run/attestations:z"
     restart: always # keep the server running
     ports:
       - "3000:3000"
diff --git a/pkg/api/error.go b/pkg/api/error.go
index 9fd0543..bff642c 100644
--- a/pkg/api/error.go
+++ b/pkg/api/error.go
@@ -46,6 +46,7 @@ const (
 	signingError                      = "Error signing"
 	failedToGenerateTimestampResponse = "Error generating timestamp response"
 	sthGenerateError                  = "Error generating signed tree head"
+	unsupportedPKIFormat              = "The PKI format requested is not supported by this server"
 )
 
 func errorMsg(message string, code int) *models.Error {
diff --git a/pkg/api/index.go b/pkg/api/index.go
index 5815545..8aa088a 100644
--- a/pkg/api/index.go
+++ b/pkg/api/index.go
@@ -45,7 +45,10 @@ func SearchIndexHandler(params index.SearchIndexParams) middleware.Responder {
 		result = append(result, resultUUIDs...)
 	}
 	if params.Query.PublicKey != nil {
-		af := pki.NewArtifactFactory(swag.StringValue(params.Query.PublicKey.Format))
+		af, err := pki.NewArtifactFactory(pki.Format(swag.StringValue(params.Query.PublicKey.Format)))
+		if err != nil {
+			return handleRekorAPIError(params, http.StatusBadRequest, err, unsupportedPKIFormat)
+		}
 		keyReader, err := util.FileOrURLReadCloser(httpReqCtx, params.Query.PublicKey.URL.String(), params.Query.PublicKey.Content)
 		if err != nil {
 			return handleRekorAPIError(params, http.StatusBadRequest, err, malformedPublicKey)
diff --git a/pkg/log/log.go b/pkg/log/log.go
index ed3c684..62ad8bb 100644
--- a/pkg/log/log.go
+++ b/pkg/log/log.go
@@ -56,6 +56,7 @@ func createCliLogger() *zap.SugaredLogger {
 	cfg.EncoderConfig.TimeKey = ""
 	cfg.EncoderConfig.LevelKey = ""
 	cfg.DisableCaller = true
+	cfg.DisableStacktrace = true
 	logger, err := cfg.Build()
 	if err != nil {
 		log.Fatalln("createLogger", err)
diff --git a/pkg/pki/factory.go b/pkg/pki/factory.go
new file mode 100644
index 0000000..33df90b
--- /dev/null
+++ b/pkg/pki/factory.go
@@ -0,0 +1,116 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package pki
+
+import (
+	"fmt"
+	"io"
+
+	"github.com/sigstore/rekor/pkg/pki/minisign"
+	"github.com/sigstore/rekor/pkg/pki/pgp"
+	"github.com/sigstore/rekor/pkg/pki/pkcs7"
+	"github.com/sigstore/rekor/pkg/pki/ssh"
+	"github.com/sigstore/rekor/pkg/pki/x509"
+)
+
+type Format string
+
+const (
+	PGP      Format = "pgp"
+	Minisign Format = "minisign"
+	SSH      Format = "ssh"
+	X509     Format = "x509"
+	PKCS7    Format = "pkcs7"
+)
+
+type ArtifactFactory struct {
+	impl pkiImpl
+}
+
+func NewArtifactFactory(format Format) (*ArtifactFactory, error) {
+	if impl, ok := artifactFactoryMap[format]; ok {
+		return &ArtifactFactory{impl: impl}, nil
+	}
+	return nil, fmt.Errorf("%v is not a supported PKI format", format)
+}
+
+type pkiImpl struct {
+	newPubKey    func(io.Reader) (PublicKey, error)
+	newSignature func(io.Reader) (Signature, error)
+}
+
+var artifactFactoryMap map[Format]pkiImpl
+
+func init() {
+	artifactFactoryMap = map[Format]pkiImpl{
+		PGP: {
+			newPubKey: func(r io.Reader) (PublicKey, error) {
+				return pgp.NewPublicKey(r)
+			},
+			newSignature: func(r io.Reader) (Signature, error) {
+				return pgp.NewSignature(r)
+			},
+		},
+		Minisign: {
+			newPubKey: func(r io.Reader) (PublicKey, error) {
+				return minisign.NewPublicKey(r)
+			},
+			newSignature: func(r io.Reader) (Signature, error) {
+				return minisign.NewSignature(r)
+			},
+		},
+		SSH: {
+			newPubKey: func(r io.Reader) (PublicKey, error) {
+				return ssh.NewPublicKey(r)
+			},
+			newSignature: func(r io.Reader) (Signature, error) {
+				return ssh.NewSignature(r)
+			},
+		},
+		X509: {
+			newPubKey: func(r io.Reader) (PublicKey, error) {
+				return x509.NewPublicKey(r)
+			},
+			newSignature: func(r io.Reader) (Signature, error) {
+				return x509.NewSignature(r)
+			},
+		},
+		PKCS7: {
+			newPubKey: func(r io.Reader) (PublicKey, error) {
+				return pkcs7.NewPublicKey(r)
+			},
+			newSignature: func(r io.Reader) (Signature, error) {
+				return pkcs7.NewSignature(r)
+			},
+		},
+	}
+}
+
+func SupportedFormats() []string {
+	var formats []string
+	for f := range artifactFactoryMap {
+		formats = append(formats, string(f))
+	}
+	return formats
+}
+
+func (a ArtifactFactory) NewPublicKey(r io.Reader) (PublicKey, error) {
+	return a.impl.newPubKey(r)
+}
+
+func (a ArtifactFactory) NewSignature(r io.Reader) (Signature, error) {
+	return a.impl.newSignature(r)
+}
diff --git a/pkg/pki/factory_test.go b/pkg/pki/factory_test.go
new file mode 100644
index 0000000..a317427
--- /dev/null
+++ b/pkg/pki/factory_test.go
@@ -0,0 +1,121 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package pki
+
+import (
+	"os"
+	"testing"
+
+	"go.uber.org/goleak"
+)
+
+func TestMain(m *testing.M) {
+	goleak.VerifyTestMain(m)
+}
+
+func TestFactoryNewKey(t *testing.T) {
+	type TestCase struct {
+		name              string
+		format            string
+		keyFile           string
+		sigFile           string
+		expectSuccess     bool
+		expectValidFormat bool
+	}
+
+	testCases := []TestCase{
+		{
+			name:              "valid pgp",
+			format:            "pgp",
+			keyFile:           "pgp/testdata/valid_armored_public.pgp",
+			sigFile:           "pgp/testdata/hello_world.txt.asc.sig",
+			expectSuccess:     true,
+			expectValidFormat: true,
+		},
+		{
+			name:              "valid minisign",
+			format:            "minisign",
+			keyFile:           "minisign/testdata/minisign.pub",
+			sigFile:           "minisign/testdata/hello_world.txt.minisig",
+			expectSuccess:     true,
+			expectValidFormat: true,
+		},
+		{
+			name:              "valid x509",
+			format:            "x509",
+			keyFile:           "x509/testdata/ec.pub",
+			sigFile:           "x509/testdata/hello_world.txt.sig",
+			expectSuccess:     true,
+			expectValidFormat: true,
+		},
+		{
+			name:              "valid ssh",
+			format:            "ssh",
+			keyFile:           "ssh/testdata/id_rsa.pub",
+			sigFile:           "ssh/testdata/hello_world.txt.sig",
+			expectSuccess:     true,
+			expectValidFormat: true,
+		},
+		{
+			name:              "invalid ssh signature",
+			format:            "ssh",
+			keyFile:           "ssh/testdata/id_rsa.pub",
+			sigFile:           "ssh/testdata/hello_world.txt",
+			expectSuccess:     false,
+			expectValidFormat: true,
+		},
+		{
+			name:              "invalid ssh key",
+			format:            "ssh",
+			keyFile:           "ssh/testdata/hello_world.txt",
+			sigFile:           "ssh/testdata/hello_world.txt.sig",
+			expectSuccess:     false,
+			expectValidFormat: true,
+		},
+		{
+			format:            "bogus",
+			expectSuccess:     false,
+			expectValidFormat: false,
+		},
+	}
+
+	for _, tc := range testCases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			factory, err := NewArtifactFactory(Format(tc.format))
+			if tc.expectValidFormat != (err == nil) {
+				t.Fatalf("unexpected error initializing factory for %v", tc.format)
+			}
+			if factory != nil {
+				keyFile, _ := os.Open(tc.keyFile)
+				_, newKeyErr := factory.NewPublicKey(keyFile)
+
+				sigFile, _ := os.Open(tc.sigFile)
+				_, newSigErr := factory.NewSignature(sigFile)
+
+				if tc.expectSuccess {
+					if newKeyErr != nil || newSigErr != nil {
+						t.Errorf("unexpected error generating public key %v or signature %v", newKeyErr, newSigErr)
+					}
+				} else { // expect a failure{
+					if newKeyErr == nil && newSigErr == nil {
+						t.Error("expected error generating public key and signature. got none")
+					}
+				}
+			}
+		})
+	}
+}
diff --git a/pkg/pki/minisign/minisign.go b/pkg/pki/minisign/minisign.go
index 0bd6951..2697d96 100644
--- a/pkg/pki/minisign/minisign.go
+++ b/pkg/pki/minisign/minisign.go
@@ -23,7 +23,7 @@ import (
 	"strings"
 
 	minisign "github.com/jedisct1/go-minisign"
-	"github.com/sigstore/sigstore/pkg/signature"
+	sigsig "github.com/sigstore/sigstore/pkg/signature"
 )
 
 // Signature Signature that follows the minisign standard; supports both minisign and signify generated signatures
@@ -98,7 +98,7 @@ func (s Signature) Verify(r io.Reader, k interface{}) error {
 		return fmt.Errorf("minisign public key has not been initialized")
 	}
 
-	verifier, err := signature.LoadED25519Verifier(key.key.PublicKey[:])
+	verifier, err := sigsig.LoadED25519Verifier(key.key.PublicKey[:])
 	if err != nil {
 		return err
 	}
diff --git a/pkg/pki/minisign/minisign_test.go b/pkg/pki/minisign/minisign_test.go
index f7aecd9..a054aba 100644
--- a/pkg/pki/minisign/minisign_test.go
+++ b/pkg/pki/minisign/minisign_test.go
@@ -301,8 +301,4 @@ func TestVerifySignature(t *testing.T) {
 	if err := validSig.Verify(bytes.NewReader([]byte("irrelevant")), &emptyKey); err == nil {
 		t.Errorf("expected error when using empty key to verify")
 	}
-
-	if err := validSig.Verify(bytes.NewReader([]byte("irrelevant")), sigFile); err == nil {
-		t.Errorf("expected error when using non key to verify")
-	}
 }
diff --git a/pkg/pki/pgp/pgp_test.go b/pkg/pki/pgp/pgp_test.go
index 17c0b34..924d00e 100644
--- a/pkg/pki/pgp/pgp_test.go
+++ b/pkg/pki/pgp/pgp_test.go
@@ -455,8 +455,4 @@ func TestVerifySignature(t *testing.T) {
 	if err := validSig.Verify(bytes.NewReader([]byte("irrelevant")), &emptyKey); err == nil {
 		t.Errorf("expected error when using empty key to verify")
 	}
-
-	if err := validSig.Verify(bytes.NewReader([]byte("irrelevant")), sigFile); err == nil {
-		t.Errorf("expected error when using non key to verify")
-	}
 }
diff --git a/pkg/pki/pki.go b/pkg/pki/pki.go
index 4463838..cca1987 100644
--- a/pkg/pki/pki.go
+++ b/pkg/pki/pki.go
@@ -16,15 +16,7 @@
 package pki
 
 import (
-	"fmt"
 	"io"
-	"strings"
-
-	"github.com/sigstore/rekor/pkg/pki/minisign"
-	"github.com/sigstore/rekor/pkg/pki/pgp"
-	"github.com/sigstore/rekor/pkg/pki/pkcs7"
-	"github.com/sigstore/rekor/pkg/pki/ssh"
-	"github.com/sigstore/rekor/pkg/pki/x509"
 )
 
 // PublicKey Generic object representing a public key (regardless of format & algorithm)
@@ -38,45 +30,3 @@ type Signature interface {
 	CanonicalValue() ([]byte, error)
 	Verify(r io.Reader, k interface{}) error
 }
-
-type ArtifactFactory struct {
-	format string
-}
-
-func NewArtifactFactory(format string) *ArtifactFactory {
-	return &ArtifactFactory{
-		format: format,
-	}
-}
-
-func (a ArtifactFactory) NewPublicKey(r io.Reader) (PublicKey, error) {
-	switch strings.ToLower(a.format) {
-	case "pgp":
-		return pgp.NewPublicKey(r)
-	case "minisign":
-		return minisign.NewPublicKey(r)
-	case "x509":
-		return x509.NewPublicKey(r)
-	case "ssh":
-		return ssh.NewPublicKey(r)
-	case "pkcs7":
-		return pkcs7.NewPublicKey(r)
-	}
-	return nil, fmt.Errorf("unknown key format '%v'", a.format)
-}
-
-func (a ArtifactFactory) NewSignature(r io.Reader) (Signature, error) {
-	switch strings.ToLower(a.format) {
-	case "pgp":
-		return pgp.NewSignature(r)
-	case "minisign":
-		return minisign.NewSignature(r)
-	case "x509":
-		return x509.NewSignature(r)
-	case "ssh":
-		return ssh.NewSignature(r)
-	case "pkcs7":
-		return pkcs7.NewSignature(r)
-	}
-	return nil, fmt.Errorf("unknown key format '%v'", a.format)
-}
diff --git a/pkg/pki/pki_test.go b/pkg/pki/pki_test.go
deleted file mode 100644
index 0454a3e..0000000
--- a/pkg/pki/pki_test.go
+++ /dev/null
@@ -1,108 +0,0 @@
-//
-// Copyright 2021 The Sigstore Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package pki
-
-import (
-	"os"
-	"testing"
-
-	"go.uber.org/goleak"
-)
-
-func TestMain(m *testing.M) {
-	goleak.VerifyTestMain(m)
-}
-
-func TestFactoryNewKey(t *testing.T) {
-	type TestCase struct {
-		name          string
-		format        string
-		keyFile       string
-		sigFile       string
-		expectSuccess bool
-	}
-
-	testCases := []TestCase{
-		{
-			name:          "valid pgp",
-			format:        "pgp",
-			keyFile:       "pgp/testdata/valid_armored_public.pgp",
-			sigFile:       "pgp/testdata/hello_world.txt.asc.sig",
-			expectSuccess: true,
-		},
-		{
-			name:          "valid minisign",
-			format:        "minisign",
-			keyFile:       "minisign/testdata/minisign.pub",
-			sigFile:       "minisign/testdata/hello_world.txt.minisig",
-			expectSuccess: true,
-		},
-		{
-			name:          "valid x509",
-			format:        "x509",
-			keyFile:       "x509/testdata/ec.pub",
-			sigFile:       "x509/testdata/hello_world.txt.sig",
-			expectSuccess: true,
-		},
-		{
-			name:          "valid ssh",
-			format:        "ssh",
-			keyFile:       "ssh/testdata/id_rsa.pub",
-			sigFile:       "ssh/testdata/hello_world.txt.sig",
-			expectSuccess: true,
-		},
-		{
-			name:          "invalid ssh signature",
-			format:        "ssh",
-			keyFile:       "ssh/testdata/id_rsa.pub",
-			sigFile:       "ssh/testdata/hello_world.txt",
-			expectSuccess: false,
-		},
-		{
-			name:          "invalid ssh key",
-			format:        "ssh",
-			keyFile:       "ssh/testdata/hello_world.txt",
-			sigFile:       "ssh/testdata/hello_world.txt.sig",
-			expectSuccess: false,
-		},
-		{
-			format:        "bogus",
-			expectSuccess: false,
-		},
-	}
-
-	for _, tc := range testCases {
-		tc := tc
-		t.Run(tc.name, func(t *testing.T) {
-			factory := NewArtifactFactory(tc.format)
-			keyFile, _ := os.Open(tc.keyFile)
-			_, newKeyErr := factory.NewPublicKey(keyFile)
-
-			sigFile, _ := os.Open(tc.sigFile)
-			_, newSigErr := factory.NewSignature(sigFile)
-
-			if tc.expectSuccess {
-				if newKeyErr != nil || newSigErr != nil {
-					t.Errorf("unexpected error generating public key %v or signature %v", newKeyErr, newSigErr)
-				}
-			} else { // expect a failure{
-				if newKeyErr == nil && newSigErr == nil {
-					t.Error("expected error generating public key and signature. got none")
-				}
-			}
-		})
-	}
-}
diff --git a/pkg/pki/ssh/ssh.go b/pkg/pki/ssh/ssh.go
index 8474431..faa4d6d 100644
--- a/pkg/pki/ssh/ssh.go
+++ b/pkg/pki/ssh/ssh.go
@@ -55,7 +55,7 @@ func (s Signature) Verify(r io.Reader, k interface{}) error {
 
 	key, ok := k.(*PublicKey)
 	if !ok {
-		return fmt.Errorf("Invalid public key type for: %v", k)
+		return fmt.Errorf("invalid public key type for: %v", k)
 	}
 
 	ck, err := key.CanonicalValue()
diff --git a/pkg/pki/x509/x509.go b/pkg/pki/x509/x509.go
index c452614..1534d14 100644
--- a/pkg/pki/x509/x509.go
+++ b/pkg/pki/x509/x509.go
@@ -29,7 +29,7 @@ import (
 
 	"github.com/go-playground/validator"
 	"github.com/sigstore/sigstore/pkg/cryptoutils"
-	"github.com/sigstore/sigstore/pkg/signature"
+	sigsig "github.com/sigstore/sigstore/pkg/signature"
 )
 
 // EmailAddressOID defined by https://oidref.com/1.2.840.113549.1.9.1
@@ -72,7 +72,7 @@ func (s Signature) Verify(r io.Reader, k interface{}) error {
 		p = key.cert.c.PublicKey
 	}
 
-	verifier, err := signature.LoadVerifier(p, crypto.SHA256)
+	verifier, err := sigsig.LoadVerifier(p, crypto.SHA256)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/types/alpine/alpine.go b/pkg/types/alpine/alpine.go
index ee3e2ff..2aafa48 100644
--- a/pkg/types/alpine/alpine.go
+++ b/pkg/types/alpine/alpine.go
@@ -16,8 +16,9 @@
 package alpine
 
 import (
-	"errors"
+	"context"
 
+	"github.com/pkg/errors"
 	"github.com/sigstore/rekor/pkg/generated/models"
 	"github.com/sigstore/rekor/pkg/types"
 )
@@ -43,7 +44,7 @@ func New() types.TypeImpl {
 
 var VersionMap = types.NewSemVerEntryFactoryMap()
 
-func (brt *BaseAlpineType) UnmarshalEntry(pe models.ProposedEntry) (types.EntryImpl, error) {
+func (bat *BaseAlpineType) UnmarshalEntry(pe models.ProposedEntry) (types.EntryImpl, error) {
 	if pe == nil {
 		return nil, errors.New("proposed entry cannot be nil")
 	}
@@ -53,5 +54,20 @@ func (brt *BaseAlpineType) UnmarshalEntry(pe models.ProposedEntry) (types.EntryI
 		return nil, errors.New("cannot unmarshal non-Alpine types")
 	}
 
-	return brt.VersionedUnmarshal(apk, *apk.APIVersion)
+	return bat.VersionedUnmarshal(apk, *apk.APIVersion)
+}
+
+func (bat *BaseAlpineType) CreateProposedEntry(ctx context.Context, version string, props types.ArtifactProperties) (models.ProposedEntry, error) {
+	if version == "" {
+		version = bat.DefaultVersion()
+	}
+	ei, err := bat.VersionedUnmarshal(nil, version)
+	if err != nil {
+		return nil, errors.Wrap(err, "fetching Intoto version implementation")
+	}
+	return ei.CreateFromArtifactProperties(ctx, props)
+}
+
+func (bat BaseAlpineType) DefaultVersion() string {
+	return "0.0.1"
 }
diff --git a/pkg/types/alpine/alpine_test.go b/pkg/types/alpine/alpine_test.go
index 749116e..331306b 100644
--- a/pkg/types/alpine/alpine_test.go
+++ b/pkg/types/alpine/alpine_test.go
@@ -66,6 +66,10 @@ func (u UnmarshalTester) Attestation() (string, []byte) {
 	return "", nil
 }
 
+func (u UnmarshalTester) CreateFromArtifactProperties(_ context.Context, _ types.ArtifactProperties) (models.ProposedEntry, error) {
+	return nil, nil
+}
+
 type UnmarshalFailsTester struct {
 	UnmarshalTester
 }
diff --git a/pkg/types/alpine/v0.0.1/entry.go b/pkg/types/alpine/v0.0.1/entry.go
index 87480a8..eaaf581 100644
--- a/pkg/types/alpine/v0.0.1/entry.go
+++ b/pkg/types/alpine/v0.0.1/entry.go
@@ -23,6 +23,8 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"io/ioutil"
+	"path/filepath"
 	"strings"
 
 	"github.com/asaskevich/govalidator"
@@ -158,7 +160,7 @@ func (v *V001Entry) FetchExternalEntities(ctx context.Context) error {
 	if v.AlpineModel.Package.Hash != nil && v.AlpineModel.Package.Hash.Value != nil {
 		oldSHA = swag.StringValue(v.AlpineModel.Package.Hash.Value)
 	}
-	artifactFactory := pki.NewArtifactFactory("x509")
+	artifactFactory, _ := pki.NewArtifactFactory(pki.X509)
 
 	g.Go(func() error {
 		defer hashW.Close()
@@ -338,3 +340,65 @@ func (v V001Entry) Validate() error {
 func (v V001Entry) Attestation() (string, []byte) {
 	return "", nil
 }
+
+func (v V001Entry) CreateFromArtifactProperties(ctx context.Context, props types.ArtifactProperties) (models.ProposedEntry, error) {
+	returnVal := models.Alpine{}
+	re := V001Entry{}
+
+	// we will need artifact, public-key, signature
+	re.AlpineModel = models.AlpineV001Schema{}
+	re.AlpineModel.Package = &models.AlpineV001SchemaPackage{}
+
+	var err error
+	artifactBytes := props.ArtifactBytes
+	if artifactBytes == nil {
+		if props.ArtifactPath.IsAbs() {
+			re.AlpineModel.Package.URL = strfmt.URI(props.ArtifactPath.String())
+			if props.ArtifactHash != "" {
+				re.AlpineModel.Package.Hash = &models.AlpineV001SchemaPackageHash{
+					Algorithm: swag.String(models.AlpineV001SchemaPackageHashAlgorithmSha256),
+					Value:     swag.String(props.ArtifactHash),
+				}
+			}
+		} else {
+			artifactBytes, err = ioutil.ReadFile(filepath.Clean(props.ArtifactPath.Path))
+			if err != nil {
+				return nil, fmt.Errorf("error reading artifact file: %w", err)
+			}
+			re.AlpineModel.Package.Content = strfmt.Base64(artifactBytes)
+		}
+	} else {
+		re.AlpineModel.Package.Content = strfmt.Base64(artifactBytes)
+	}
+
+	re.AlpineModel.PublicKey = &models.AlpineV001SchemaPublicKey{}
+	publicKeyBytes := props.PublicKeyBytes
+	if publicKeyBytes == nil {
+		if props.PublicKeyPath.IsAbs() {
+			re.AlpineModel.PublicKey.URL = strfmt.URI(props.PublicKeyPath.String())
+		} else {
+			publicKeyBytes, err = ioutil.ReadFile(filepath.Clean(props.PublicKeyPath.Path))
+			if err != nil {
+				return nil, fmt.Errorf("error reading public key file: %w", err)
+			}
+			re.AlpineModel.PublicKey.Content = strfmt.Base64(publicKeyBytes)
+		}
+	} else {
+		re.AlpineModel.PublicKey.Content = strfmt.Base64(publicKeyBytes)
+	}
+
+	if err := re.Validate(); err != nil {
+		return nil, err
+	}
+
+	if re.HasExternalEntities() {
+		if err := re.FetchExternalEntities(ctx); err != nil {
+			return nil, fmt.Errorf("error retrieving external entities: %v", err)
+		}
+	}
+
+	returnVal.APIVersion = swag.String(re.APIVersion())
+	returnVal.Spec = re.AlpineModel
+
+	return &returnVal, nil
+}
diff --git a/pkg/types/entries.go b/pkg/types/entries.go
index a22f790..7eafef9 100644
--- a/pkg/types/entries.go
+++ b/pkg/types/entries.go
@@ -20,6 +20,7 @@ import (
 	"encoding/base64"
 	"errors"
 	"fmt"
+	"net/url"
 	"reflect"
 
 	"github.com/mitchellh/mapstructure"
@@ -36,11 +37,23 @@ type EntryImpl interface {
 	Unmarshal(e models.ProposedEntry) error           // unmarshal the abstract entry into the specific struct for this versioned type
 	Validate() error                                  // performs any cross-field validation that is not expressed in the OpenAPI spec
 	Attestation() (string, []byte)
+	CreateFromArtifactProperties(context.Context, ArtifactProperties) (models.ProposedEntry, error)
 }
 
 // EntryFactory describes a factory function that can generate structs for a specific versioned type
 type EntryFactory func() EntryImpl
 
+func NewProposedEntry(ctx context.Context, kind, version string, props ArtifactProperties) (models.ProposedEntry, error) {
+	if tf, found := TypeMap.Load(kind); found {
+		t := tf.(func() TypeImpl)()
+		if t == nil {
+			return nil, fmt.Errorf("error generating object for kind '%v'", kind)
+		}
+		return t.CreateProposedEntry(ctx, version, props)
+	}
+	return nil, fmt.Errorf("could not create entry for kind '%v'", kind)
+}
+
 // NewEntry returns the specific instance for the type and version specified in the doc
 func NewEntry(pe models.ProposedEntry) (EntryImpl, error) {
 	if pe == nil {
@@ -88,3 +101,16 @@ func DecodeEntry(input, output interface{}) error {
 
 	return dec.Decode(input)
 }
+
+// ArtifactProperties provide a consistent struct for passing values from
+// CLI flags to the type+version specific CreateProposeEntry() methods
+type ArtifactProperties struct {
+	ArtifactPath   *url.URL
+	ArtifactHash   string
+	ArtifactBytes  []byte
+	SignaturePath  *url.URL
+	SignatureBytes []byte
+	PublicKeyPath  *url.URL
+	PublicKeyBytes []byte
+	PKIFormat      string
+}
diff --git a/pkg/types/helm/helm.go b/pkg/types/helm/helm.go
index 6c20e6f..3cfbc2f 100644
--- a/pkg/types/helm/helm.go
+++ b/pkg/types/helm/helm.go
@@ -16,8 +16,9 @@
 package helm
 
 import (
-	"errors"
+	"context"
 
+	"github.com/pkg/errors"
 	"github.com/sigstore/rekor/pkg/generated/models"
 	"github.com/sigstore/rekor/pkg/types"
 )
@@ -55,3 +56,18 @@ func (it BaseHelmType) UnmarshalEntry(pe models.ProposedEntry) (types.EntryImpl,
 
 	return it.VersionedUnmarshal(in, *in.APIVersion)
 }
+
+func (it *BaseHelmType) CreateProposedEntry(ctx context.Context, version string, props types.ArtifactProperties) (models.ProposedEntry, error) {
+	if version == "" {
+		version = it.DefaultVersion()
+	}
+	ei, err := it.VersionedUnmarshal(nil, version)
+	if err != nil {
+		return nil, errors.Wrap(err, "fetching Rekord version implementation")
+	}
+	return ei.CreateFromArtifactProperties(ctx, props)
+}
+
+func (it BaseHelmType) DefaultVersion() string {
+	return "0.0.1"
+}
diff --git a/pkg/types/helm/helm_test.go b/pkg/types/helm/helm_test.go
index 33ff86d..b4402b9 100644
--- a/pkg/types/helm/helm_test.go
+++ b/pkg/types/helm/helm_test.go
@@ -66,6 +66,10 @@ func (u UnmarshalTester) Attestation() (string, []byte) {
 	return "", nil
 }
 
+func (u UnmarshalTester) CreateFromArtifactProperties(_ context.Context, _ types.ArtifactProperties) (models.ProposedEntry, error) {
+	return nil, nil
+}
+
 type UnmarshalFailsTester struct {
 	UnmarshalTester
 }
diff --git a/pkg/types/helm/provenance_test.go b/pkg/types/helm/provenance_test.go
index 50fac2e..b126543 100644
--- a/pkg/types/helm/provenance_test.go
+++ b/pkg/types/helm/provenance_test.go
@@ -57,9 +57,12 @@ func TestProvenance(t *testing.T) {
 		t.Fatalf("failed to parse public key: %v", err)
 	}
 
-	artifactFactory := pki.NewArtifactFactory("pgp")
-	sig, err := artifactFactory.NewSignature(provenance.Block.ArmoredSignature.Body)
+	artifactFactory, err := pki.NewArtifactFactory(pki.PGP)
+	if err != nil {
+		t.Fatalf("Failed to create PGP pki factory %v", err)
+	}
 
+	sig, err := artifactFactory.NewSignature(provenance.Block.ArmoredSignature.Body)
 	if err != nil {
 		t.Fatalf("Failed to create signature %v", err)
 	}
diff --git a/pkg/types/helm/v0.0.1/entry.go b/pkg/types/helm/v0.0.1/entry.go
index 36a8272..fe630ba 100644
--- a/pkg/types/helm/v0.0.1/entry.go
+++ b/pkg/types/helm/v0.0.1/entry.go
@@ -23,6 +23,8 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
+	"io/ioutil"
+	"path/filepath"
 	"strings"
 
 	"github.com/pkg/errors"
@@ -144,6 +146,11 @@ func (v *V001Entry) FetchExternalEntities(ctx context.Context) error {
 		return err
 	}
 
+	artifactFactory, err := pki.NewArtifactFactory(pki.PGP)
+	if err != nil {
+		return err
+	}
+
 	g, ctx := errgroup.WithContext(ctx)
 
 	provenanceR, provenanceW := io.Pipe()
@@ -164,8 +171,6 @@ func (v *V001Entry) FetchExternalEntities(ctx context.Context) error {
 		return err
 	}
 
-	artifactFactory := pki.NewArtifactFactory("pgp")
-
 	g.Go(func() error {
 		defer provenanceW.Close()
 
@@ -347,3 +352,61 @@ func (v V001Entry) Validate() error {
 func (v V001Entry) Attestation() (string, []byte) {
 	return "", nil
 }
+
+func (v V001Entry) CreateFromArtifactProperties(ctx context.Context, props types.ArtifactProperties) (models.ProposedEntry, error) {
+	//TODO: how to select version of item to create
+	returnVal := models.Helm{}
+	re := V001Entry{}
+
+	// we will need provenance file and public-key
+	re.HelmObj = models.HelmV001Schema{}
+	re.HelmObj.Chart = &models.HelmV001SchemaChart{}
+	re.HelmObj.Chart.Provenance = &models.HelmV001SchemaChartProvenance{}
+
+	var err error
+	artifactBytes := props.ArtifactBytes
+	if artifactBytes == nil {
+		if props.ArtifactPath.IsAbs() {
+			re.HelmObj.Chart.Provenance.URL = strfmt.URI(props.ArtifactPath.String())
+		} else {
+			artifactBytes, err = ioutil.ReadFile(filepath.Clean(props.ArtifactPath.Path))
+			if err != nil {
+				return nil, fmt.Errorf("error reading artifact file: %w", err)
+			}
+			re.HelmObj.Chart.Provenance.Content = strfmt.Base64(artifactBytes)
+		}
+	} else {
+		re.HelmObj.Chart.Provenance.Content = strfmt.Base64(artifactBytes)
+	}
+
+	re.HelmObj.PublicKey = &models.HelmV001SchemaPublicKey{}
+	publicKeyBytes := props.PublicKeyBytes
+	if publicKeyBytes == nil {
+		if props.PublicKeyPath.IsAbs() {
+			re.HelmObj.PublicKey.URL = strfmt.URI(props.PublicKeyPath.String())
+		} else {
+			publicKeyBytes, err = ioutil.ReadFile(filepath.Clean(props.PublicKeyPath.Path))
+			if err != nil {
+				return nil, fmt.Errorf("error reading public key file: %w", err)
+			}
+			re.HelmObj.PublicKey.Content = strfmt.Base64(publicKeyBytes)
+		}
+	} else {
+		re.HelmObj.PublicKey.Content = strfmt.Base64(publicKeyBytes)
+	}
+
+	if err := re.Validate(); err != nil {
+		return nil, err
+	}
+
+	if re.HasExternalEntities() {
+		if err := re.FetchExternalEntities(ctx); err != nil {
+			return nil, fmt.Errorf("error retrieving external entities: %v", err)
+		}
+	}
+
+	returnVal.APIVersion = swag.String(re.APIVersion())
+	returnVal.Spec = re.HelmObj
+
+	return &returnVal, nil
+}
diff --git a/pkg/types/intoto/intoto.go b/pkg/types/intoto/intoto.go
index 1b2138e..5016ce9 100644
--- a/pkg/types/intoto/intoto.go
+++ b/pkg/types/intoto/intoto.go
@@ -16,8 +16,9 @@
 package intoto
 
 import (
-	"errors"
+	"context"
 
+	"github.com/pkg/errors"
 	"github.com/sigstore/rekor/pkg/generated/models"
 	"github.com/sigstore/rekor/pkg/types"
 )
@@ -55,3 +56,18 @@ func (it BaseIntotoType) UnmarshalEntry(pe models.ProposedEntry) (types.EntryImp
 
 	return it.VersionedUnmarshal(in, *in.APIVersion)
 }
+
+func (it *BaseIntotoType) CreateProposedEntry(ctx context.Context, version string, props types.ArtifactProperties) (models.ProposedEntry, error) {
+	if version == "" {
+		version = it.DefaultVersion()
+	}
+	ei, err := it.VersionedUnmarshal(nil, version)
+	if err != nil {
+		return nil, errors.Wrap(err, "fetching Intoto version implementation")
+	}
+	return ei.CreateFromArtifactProperties(ctx, props)
+}
+
+func (it BaseIntotoType) DefaultVersion() string {
+	return "0.0.1"
+}
diff --git a/pkg/types/intoto/intoto_test.go b/pkg/types/intoto/intoto_test.go
index 01b91d1..35b1d04 100644
--- a/pkg/types/intoto/intoto_test.go
+++ b/pkg/types/intoto/intoto_test.go
@@ -66,6 +66,10 @@ func (u UnmarshalTester) Attestation() (string, []byte) {
 	return "", nil
 }
 
+func (u UnmarshalTester) CreateFromArtifactProperties(_ context.Context, _ types.ArtifactProperties) (models.ProposedEntry, error) {
+	return nil, nil
+}
+
 type UnmarshalFailsTester struct {
 	UnmarshalTester
 }
diff --git a/pkg/types/intoto/v0.0.1/entry.go b/pkg/types/intoto/v0.0.1/entry.go
index 72cc524..b1926ef 100644
--- a/pkg/types/intoto/v0.0.1/entry.go
+++ b/pkg/types/intoto/v0.0.1/entry.go
@@ -24,6 +24,9 @@ import (
 	"encoding/hex"
 	"encoding/json"
 	"errors"
+	"fmt"
+	"io/ioutil"
+	"path/filepath"
 
 	"github.com/in-toto/in-toto-golang/in_toto"
 	"github.com/in-toto/in-toto-golang/pkg/ssl"
@@ -121,7 +124,11 @@ func (v *V001Entry) Unmarshal(pe models.ProposedEntry) error {
 	}
 
 	// Only support x509 signatures for intoto attestations
-	af := pki.NewArtifactFactory("x509")
+	af, err := pki.NewArtifactFactory(pki.X509)
+	if err != nil {
+		return err
+	}
+
 	v.keyObj, err = af.NewPublicKey(bytes.NewReader(*v.IntotoObj.PublicKey))
 	if err != nil {
 		return err
@@ -243,3 +250,47 @@ func (v *verifier) Verify(keyID string, data, sig []byte) error {
 	}
 	return v.v.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data))
 }
+
+func (v V001Entry) CreateFromArtifactProperties(_ context.Context, props types.ArtifactProperties) (models.ProposedEntry, error) {
+	returnVal := models.Intoto{}
+
+	var err error
+	artifactBytes := props.ArtifactBytes
+	if artifactBytes == nil {
+		if props.ArtifactPath == nil {
+			return nil, errors.New("path to artifact file must be specified")
+		}
+		if props.ArtifactPath.IsAbs() {
+			return nil, errors.New("intoto envelopes cannot be fetched over HTTP(S)")
+		}
+		artifactBytes, err = ioutil.ReadFile(filepath.Clean(props.ArtifactPath.Path))
+		if err != nil {
+			return nil, err
+		}
+	}
+	publicKeyBytes := props.PublicKeyBytes
+	if publicKeyBytes == nil {
+		if props.PublicKeyPath == nil {
+			return nil, errors.New("public key must be provided to verify signature")
+		}
+		publicKeyBytes, err = ioutil.ReadFile(filepath.Clean(props.PublicKeyPath.Path))
+		if err != nil {
+			return nil, fmt.Errorf("error reading public key file: %w", err)
+		}
+	}
+	kb := strfmt.Base64(publicKeyBytes)
+
+	re := V001Entry{
+		IntotoObj: models.IntotoV001Schema{
+			Content: &models.IntotoV001SchemaContent{
+				Envelope: string(artifactBytes),
+			},
+			PublicKey: &kb,
+		},
+	}
+
+	returnVal.Spec = re.IntotoObj
+	returnVal.APIVersion = swag.String(re.APIVersion())
+
+	return &returnVal, nil
+}
diff --git a/pkg/types/jar/jar.go b/pkg/types/jar/jar.go
index 346fb1e..0570800 100644
--- a/pkg/types/jar/jar.go
+++ b/pkg/types/jar/jar.go
@@ -16,8 +16,9 @@
 package jar
 
 import (
-	"errors"
+	"context"
 
+	"github.com/pkg/errors"
 	"github.com/sigstore/rekor/pkg/generated/models"
 	"github.com/sigstore/rekor/pkg/types"
 )
@@ -55,3 +56,18 @@ func (bjt *BaseJARType) UnmarshalEntry(pe models.ProposedEntry) (types.EntryImpl
 
 	return bjt.VersionedUnmarshal(jar, *jar.APIVersion)
 }
+
+func (bjt *BaseJARType) CreateProposedEntry(ctx context.Context, version string, props types.ArtifactProperties) (models.ProposedEntry, error) {
+	if version == "" {
+		version = bjt.DefaultVersion()
+	}
+	ei, err := bjt.VersionedUnmarshal(nil, version)
+	if err != nil {
+		return nil, errors.Wrap(err, "fetching JAR version implementation")
+	}
+	return ei.CreateFromArtifactProperties(ctx, props)
+}
+
+func (bjt BaseJARType) DefaultVersion() string {
+	return "0.0.1"
+}
diff --git a/pkg/types/jar/jar_test.go b/pkg/types/jar/jar_test.go
index 9c3a609..683a31c 100644
--- a/pkg/types/jar/jar_test.go
+++ b/pkg/types/jar/jar_test.go
@@ -65,6 +65,10 @@ func (u UnmarshalTester) Attestation() (string, []byte) {
 	return "", nil
 }
 
+func (u UnmarshalTester) CreateFromArtifactProperties(_ context.Context, _ types.ArtifactProperties) (models.ProposedEntry, error) {
+	return nil, nil
+}
+
 type UnmarshalFailsTester struct {
 	UnmarshalTester
 }
diff --git a/pkg/types/jar/v0.0.1/entry.go b/pkg/types/jar/v0.0.1/entry.go
index 1f4f821..560ce73 100644
--- a/pkg/types/jar/v0.0.1/entry.go
+++ b/pkg/types/jar/v0.0.1/entry.go
@@ -27,6 +27,7 @@ import (
 	"io"
 	"io/ioutil"
 	"path"
+	"path/filepath"
 	"strings"
 
 	"github.com/sigstore/rekor/pkg/log"
@@ -178,7 +179,10 @@ func (v *V001Entry) FetchExternalEntities(ctx context.Context) error {
 	}
 	v.jarObj = jarObj[0]
 
-	af := pki.NewArtifactFactory("pkcs7")
+	af, err := pki.NewArtifactFactory(pki.PKCS7)
+	if err != nil {
+		return err
+	}
 	// we need to find and extract the PKCS7 bundle from the JAR file manually
 	sigPKCS7, err := extractPKCS7SignatureFromJAR(zipReader)
 	if err != nil {
@@ -316,3 +320,53 @@ func extractPKCS7SignatureFromJAR(inz *zip.Reader) ([]byte, error) {
 func (v V001Entry) Attestation() (string, []byte) {
 	return "", nil
 }
+
+func (v V001Entry) CreateFromArtifactProperties(ctx context.Context, props types.ArtifactProperties) (models.ProposedEntry, error) {
+	returnVal := models.Jar{}
+	re := V001Entry{}
+
+	// we will need only the artifact; public-key & signature are embedded in JAR
+	re.JARModel = models.JarV001Schema{}
+	re.JARModel.Archive = &models.JarV001SchemaArchive{}
+
+	var err error
+	artifactBytes := props.ArtifactBytes
+	if artifactBytes == nil {
+		if props.ArtifactPath == nil {
+			return nil, errors.New("path to JAR archive (file or URL) must be specified")
+		}
+		if props.ArtifactPath.IsAbs() {
+			re.JARModel.Archive.URL = strfmt.URI(props.ArtifactPath.String())
+			if props.ArtifactHash != "" {
+				re.JARModel.Archive.Hash = &models.JarV001SchemaArchiveHash{
+					Algorithm: swag.String(models.JarV001SchemaArchiveHashAlgorithmSha256),
+					Value:     swag.String(props.ArtifactHash),
+				}
+			}
+		} else {
+			artifactBytes, err = ioutil.ReadFile(filepath.Clean(props.ArtifactPath.Path))
+			if err != nil {
+				return nil, fmt.Errorf("error reading JAR file: %w", err)
+			}
+			//TODO: ensure this is a valid JAR file; look for META-INF/MANIFEST.MF?
+			re.JARModel.Archive.Content = strfmt.Base64(artifactBytes)
+		}
+	} else {
+		re.JARModel.Archive.Content = strfmt.Base64(artifactBytes)
+	}
+
+	if err := re.Validate(); err != nil {
+		return nil, err
+	}
+
+	if re.HasExternalEntities() {
+		if err := re.FetchExternalEntities(ctx); err != nil {
+			return nil, fmt.Errorf("error retrieving external entities: %v", err)
+		}
+	}
+
+	returnVal.APIVersion = swag.String(re.APIVersion())
+	returnVal.Spec = re.JARModel
+
+	return &returnVal, nil
+}
diff --git a/pkg/types/rekord/rekord.go b/pkg/types/rekord/rekord.go
index 0741782..3708cb9 100644
--- a/pkg/types/rekord/rekord.go
+++ b/pkg/types/rekord/rekord.go
@@ -16,8 +16,9 @@
 package rekord
 
 import (
-	"errors"
+	"context"
 
+	"github.com/pkg/errors"
 	"github.com/sigstore/rekor/pkg/generated/models"
 	"github.com/sigstore/rekor/pkg/types"
 )
@@ -55,3 +56,18 @@ func (rt BaseRekordType) UnmarshalEntry(pe models.ProposedEntry) (types.EntryImp
 
 	return rt.VersionedUnmarshal(rekord, *rekord.APIVersion)
 }
+
+func (rt *BaseRekordType) CreateProposedEntry(ctx context.Context, version string, props types.ArtifactProperties) (models.ProposedEntry, error) {
+	if version == "" {
+		version = rt.DefaultVersion()
+	}
+	ei, err := rt.VersionedUnmarshal(nil, version)
+	if err != nil {
+		return nil, errors.Wrap(err, "fetching Rekord version implementation")
+	}
+	return ei.CreateFromArtifactProperties(ctx, props)
+}
+
+func (rt BaseRekordType) DefaultVersion() string {
+	return "0.0.1"
+}
diff --git a/pkg/types/rekord/rekord_test.go b/pkg/types/rekord/rekord_test.go
index 9463bca..20b2923 100644
--- a/pkg/types/rekord/rekord_test.go
+++ b/pkg/types/rekord/rekord_test.go
@@ -66,6 +66,10 @@ func (u UnmarshalTester) Attestation() (string, []byte) {
 	return "", nil
 }
 
+func (u UnmarshalTester) CreateFromArtifactProperties(_ context.Context, _ types.ArtifactProperties) (models.ProposedEntry, error) {
+	return nil, nil
+}
+
 type UnmarshalFailsTester struct {
 	UnmarshalTester
 }
diff --git a/pkg/types/rekord/v0.0.1/entry.go b/pkg/types/rekord/v0.0.1/entry.go
index 5373f13..6635971 100644
--- a/pkg/types/rekord/v0.0.1/entry.go
+++ b/pkg/types/rekord/v0.0.1/entry.go
@@ -23,6 +23,8 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"io/ioutil"
+	"path/filepath"
 	"strings"
 
 	"github.com/asaskevich/govalidator"
@@ -161,7 +163,10 @@ func (v *V001Entry) FetchExternalEntities(ctx context.Context) error {
 	if v.RekordObj.Data.Hash != nil && v.RekordObj.Data.Hash.Value != nil {
 		oldSHA = swag.StringValue(v.RekordObj.Data.Hash.Value)
 	}
-	artifactFactory := pki.NewArtifactFactory(v.RekordObj.Signature.Format)
+	artifactFactory, err := pki.NewArtifactFactory(pki.Format(v.RekordObj.Signature.Format))
+	if err != nil {
+		return err
+	}
 
 	g.Go(func() error {
 		defer hashW.Close()
@@ -383,3 +388,99 @@ func (v V001Entry) Validate() error {
 func (v V001Entry) Attestation() (string, []byte) {
 	return "", nil
 }
+
+func (v V001Entry) CreateFromArtifactProperties(ctx context.Context, props types.ArtifactProperties) (models.ProposedEntry, error) {
+	returnVal := models.Rekord{}
+	re := V001Entry{}
+
+	// we will need artifact, public-key, signature
+	re.RekordObj.Data = &models.RekordV001SchemaData{}
+
+	var err error
+	artifactBytes := props.ArtifactBytes
+	if artifactBytes == nil {
+		if props.ArtifactPath == nil {
+			return nil, errors.New("path to artifact (file or URL) must be specified")
+		}
+		if props.ArtifactPath.IsAbs() {
+			re.RekordObj.Data.URL = strfmt.URI(props.ArtifactPath.String())
+			if props.ArtifactHash != "" {
+				re.RekordObj.Data.Hash = &models.RekordV001SchemaDataHash{
+					Algorithm: swag.String(models.RekordV001SchemaDataHashAlgorithmSha256),
+					Value:     swag.String(props.ArtifactHash),
+				}
+			}
+		} else {
+			artifactBytes, err := ioutil.ReadFile(filepath.Clean(props.ArtifactPath.Path))
+			if err != nil {
+				return nil, fmt.Errorf("error reading artifact file: %w", err)
+			}
+			re.RekordObj.Data.Content = strfmt.Base64(artifactBytes)
+		}
+	} else {
+		re.RekordObj.Data.Content = strfmt.Base64(artifactBytes)
+	}
+
+	re.RekordObj.Signature = &models.RekordV001SchemaSignature{}
+	switch props.PKIFormat {
+	case "pgp":
+		re.RekordObj.Signature.Format = models.RekordV001SchemaSignatureFormatPgp
+	case "minisign":
+		re.RekordObj.Signature.Format = models.RekordV001SchemaSignatureFormatMinisign
+	case "x509":
+		re.RekordObj.Signature.Format = models.RekordV001SchemaSignatureFormatX509
+	case "ssh":
+		re.RekordObj.Signature.Format = models.RekordV001SchemaSignatureFormatSSH
+	}
+	sigBytes := props.SignatureBytes
+	if sigBytes == nil {
+		if props.SignaturePath == nil {
+			return nil, errors.New("a detached signature must be provided")
+		}
+		if props.SignaturePath.IsAbs() {
+			re.RekordObj.Signature.URL = strfmt.URI(props.SignaturePath.String())
+		} else {
+			sigBytes, err = ioutil.ReadFile(filepath.Clean(props.SignaturePath.Path))
+			if err != nil {
+				return nil, fmt.Errorf("error reading signature file: %w", err)
+			}
+			re.RekordObj.Signature.Content = strfmt.Base64(sigBytes)
+		}
+	} else {
+		re.RekordObj.Signature.Content = strfmt.Base64(sigBytes)
+	}
+
+	re.RekordObj.Signature.PublicKey = &models.RekordV001SchemaSignaturePublicKey{}
+	publicKeyBytes := props.PublicKeyBytes
+	if publicKeyBytes == nil {
+		if props.PublicKeyPath == nil {
+			return nil, errors.New("public key must be provided to verify detached signature")
+		}
+		if props.PublicKeyPath.IsAbs() {
+			re.RekordObj.Signature.PublicKey.URL = strfmt.URI(props.PublicKeyPath.String())
+		} else {
+			publicKeyBytes, err = ioutil.ReadFile(filepath.Clean(props.PublicKeyPath.Path))
+			if err != nil {
+				return nil, fmt.Errorf("error reading public key file: %w", err)
+			}
+			re.RekordObj.Signature.PublicKey.Content = strfmt.Base64(publicKeyBytes)
+		}
+	} else {
+		re.RekordObj.Signature.PublicKey.Content = strfmt.Base64(publicKeyBytes)
+	}
+
+	if err := re.Validate(); err != nil {
+		return nil, err
+	}
+
+	if re.HasExternalEntities() {
+		if err := re.FetchExternalEntities(ctx); err != nil {
+			return nil, fmt.Errorf("error retrieving external entities: %v", err)
+		}
+	}
+
+	returnVal.APIVersion = swag.String(re.APIVersion())
+	returnVal.Spec = re.RekordObj
+
+	return &returnVal, nil
+}
diff --git a/pkg/types/rfc3161/rfc3161.go b/pkg/types/rfc3161/rfc3161.go
index f3e2ee9..55882ea 100644
--- a/pkg/types/rfc3161/rfc3161.go
+++ b/pkg/types/rfc3161/rfc3161.go
@@ -16,8 +16,9 @@
 package rfc3161
 
 import (
-	"errors"
+	"context"
 
+	"github.com/pkg/errors"
 	"github.com/sigstore/rekor/pkg/generated/models"
 	"github.com/sigstore/rekor/pkg/types"
 )
@@ -35,15 +36,15 @@ func init() {
 }
 
 func New() types.TypeImpl {
-	brt := BaseTimestampType{}
-	brt.Kind = KIND
-	brt.VersionMap = VersionMap
-	return &brt
+	btt := BaseTimestampType{}
+	btt.Kind = KIND
+	btt.VersionMap = VersionMap
+	return &btt
 }
 
 var VersionMap = types.NewSemVerEntryFactoryMap()
 
-func (rt BaseTimestampType) UnmarshalEntry(pe models.ProposedEntry) (types.EntryImpl, error) {
+func (btt BaseTimestampType) UnmarshalEntry(pe models.ProposedEntry) (types.EntryImpl, error) {
 	if pe == nil {
 		return nil, errors.New("proposed entry cannot be nil")
 	}
@@ -53,5 +54,20 @@ func (rt BaseTimestampType) UnmarshalEntry(pe models.ProposedEntry) (types.Entry
 		return nil, errors.New("cannot unmarshal non-Timestamp types")
 	}
 
-	return rt.VersionedUnmarshal(rfc3161, *rfc3161.APIVersion)
+	return btt.VersionedUnmarshal(rfc3161, *rfc3161.APIVersion)
+}
+
+func (btt *BaseTimestampType) CreateProposedEntry(ctx context.Context, version string, props types.ArtifactProperties) (models.ProposedEntry, error) {
+	if version == "" {
+		version = btt.DefaultVersion()
+	}
+	ei, err := btt.VersionedUnmarshal(nil, version)
+	if err != nil {
+		return nil, errors.Wrap(err, "fetching RFC3161 version implementation")
+	}
+	return ei.CreateFromArtifactProperties(ctx, props)
+}
+
+func (btt BaseTimestampType) DefaultVersion() string {
+	return "0.0.1"
 }
diff --git a/pkg/types/rfc3161/rfc3161_test.go b/pkg/types/rfc3161/rfc3161_test.go
index 0ed66a1..17b447d 100644
--- a/pkg/types/rfc3161/rfc3161_test.go
+++ b/pkg/types/rfc3161/rfc3161_test.go
@@ -69,6 +69,9 @@ func (u UnmarshalTester) Unmarshal(pe models.ProposedEntry) error {
 func (u UnmarshalFailsTester) Attestation() (string, []byte) {
 	return "", nil
 }
+func (u UnmarshalTester) CreateFromArtifactProperties(_ context.Context, _ types.ArtifactProperties) (models.ProposedEntry, error) {
+	return nil, nil
+}
 
 type UnmarshalFailsTester struct {
 	UnmarshalTester
diff --git a/pkg/types/rfc3161/v0.0.1/entry.go b/pkg/types/rfc3161/v0.0.1/entry.go
index 7728085..b22c9d1 100644
--- a/pkg/types/rfc3161/v0.0.1/entry.go
+++ b/pkg/types/rfc3161/v0.0.1/entry.go
@@ -24,6 +24,8 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"io/ioutil"
+	"path/filepath"
 
 	"github.com/sigstore/rekor/pkg/types/rfc3161"
 
@@ -175,14 +177,14 @@ func (v V001Entry) Validate() error {
 		return err
 	}
 	if tsr.Status.Status != pkcs9.StatusGranted && tsr.Status.Status != pkcs9.StatusGrantedWithMods {
-		return fmt.Errorf("Tsr status not granted: %v", tsr.Status.Status)
+		return fmt.Errorf("tsr status not granted: %v", tsr.Status.Status)
 	}
 	if !tsr.TimeStampToken.ContentType.Equal(asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 2}) {
-		return fmt.Errorf("Tsr wrong content type: %v", tsr.TimeStampToken.ContentType)
+		return fmt.Errorf("tsr wrong content type: %v", tsr.TimeStampToken.ContentType)
 	}
 	_, err = tsr.TimeStampToken.Content.Verify(nil, false)
 	if err != nil {
-		return fmt.Errorf("Tsr verification error: %v", err)
+		return fmt.Errorf("tsr verification error: %v", err)
 	}
 
 	return nil
@@ -191,3 +193,36 @@ func (v V001Entry) Validate() error {
 func (v V001Entry) Attestation() (string, []byte) {
 	return "", nil
 }
+
+func (v V001Entry) CreateFromArtifactProperties(_ context.Context, props types.ArtifactProperties) (models.ProposedEntry, error) {
+	returnVal := models.Rfc3161{}
+
+	var err error
+	artifactBytes := props.ArtifactBytes
+	if artifactBytes == nil {
+		if props.ArtifactPath == nil {
+			return nil, errors.New("path to artifact file must be specified")
+		}
+		if props.ArtifactPath.IsAbs() {
+			return nil, errors.New("RFC3161 timestamps cannot be fetched over HTTP(S)")
+		}
+		artifactBytes, err = ioutil.ReadFile(filepath.Clean(props.ArtifactPath.Path))
+		if err != nil {
+			return nil, fmt.Errorf("error reading artifact file: %w", err)
+		}
+	}
+
+	b64 := strfmt.Base64(artifactBytes)
+	re := V001Entry{
+		Rfc3161Obj: models.Rfc3161V001Schema{
+			Tsr: &models.Rfc3161V001SchemaTsr{
+				Content: &b64,
+			},
+		},
+	}
+
+	returnVal.Spec = re.Rfc3161Obj
+	returnVal.APIVersion = swag.String(re.APIVersion())
+
+	return &returnVal, nil
+}
diff --git a/pkg/types/rfc3161/v0.0.1/entry_test.go b/pkg/types/rfc3161/v0.0.1/entry_test.go
index a208566..e2d2711 100644
--- a/pkg/types/rfc3161/v0.0.1/entry_test.go
+++ b/pkg/types/rfc3161/v0.0.1/entry_test.go
@@ -126,7 +126,7 @@ func TestCrossFieldValidation(t *testing.T) {
 			hasExtEntities:               false,
 			expectUnmarshalSuccess:       false,
 			expectCanonicalizeSuccess:    true,
-			expectValidationErrorMessage: "Tsr status not granted: 2",
+			expectValidationErrorMessage: "tsr status not granted: 2",
 		},
 		{
 			caseDesc: "invalid obj - bad content type",
@@ -140,7 +140,7 @@ func TestCrossFieldValidation(t *testing.T) {
 			hasExtEntities:               false,
 			expectUnmarshalSuccess:       false,
 			expectCanonicalizeSuccess:    true,
-			expectValidationErrorMessage: "Tsr wrong content type: 0.0.0.0.42",
+			expectValidationErrorMessage: "tsr wrong content type: 0.0.0.0.42",
 		},
 		{
 			caseDesc: "invalid obj - bad content",
@@ -154,7 +154,7 @@ func TestCrossFieldValidation(t *testing.T) {
 			hasExtEntities:               false,
 			expectUnmarshalSuccess:       false,
 			expectCanonicalizeSuccess:    true,
-			expectValidationErrorMessage: "Tsr verification error",
+			expectValidationErrorMessage: "tsr verification error",
 		},
 		{
 			caseDesc: "valid obj with extra data",
diff --git a/pkg/types/rpm/rpm.go b/pkg/types/rpm/rpm.go
index 42a671c..efba6cb 100644
--- a/pkg/types/rpm/rpm.go
+++ b/pkg/types/rpm/rpm.go
@@ -16,8 +16,9 @@
 package rpm
 
 import (
-	"errors"
+	"context"
 
+	"github.com/pkg/errors"
 	"github.com/sigstore/rekor/pkg/generated/models"
 	"github.com/sigstore/rekor/pkg/types"
 )
@@ -55,3 +56,18 @@ func (brt *BaseRPMType) UnmarshalEntry(pe models.ProposedEntry) (types.EntryImpl
 
 	return brt.VersionedUnmarshal(rpm, *rpm.APIVersion)
 }
+
+func (brt *BaseRPMType) CreateProposedEntry(ctx context.Context, version string, props types.ArtifactProperties) (models.ProposedEntry, error) {
+	if version == "" {
+		version = brt.DefaultVersion()
+	}
+	ei, err := brt.VersionedUnmarshal(nil, version)
+	if err != nil {
+		return nil, errors.Wrap(err, "fetching RPM version implementation")
+	}
+	return ei.CreateFromArtifactProperties(ctx, props)
+}
+
+func (brt BaseRPMType) DefaultVersion() string {
+	return "0.0.1"
+}
diff --git a/pkg/types/rpm/rpm_test.go b/pkg/types/rpm/rpm_test.go
index e1ff77b..8fe48d1 100644
--- a/pkg/types/rpm/rpm_test.go
+++ b/pkg/types/rpm/rpm_test.go
@@ -66,6 +66,10 @@ func (u UnmarshalTester) Attestation() (string, []byte) {
 	return "", nil
 }
 
+func (u UnmarshalTester) CreateFromArtifactProperties(_ context.Context, _ types.ArtifactProperties) (models.ProposedEntry, error) {
+	return nil, nil
+}
+
 type UnmarshalFailsTester struct {
 	UnmarshalTester
 }
diff --git a/pkg/types/rpm/v0.0.1/entry.go b/pkg/types/rpm/v0.0.1/entry.go
index e37178f..ac6d4bd 100644
--- a/pkg/types/rpm/v0.0.1/entry.go
+++ b/pkg/types/rpm/v0.0.1/entry.go
@@ -24,6 +24,7 @@ import (
 	"fmt"
 	"io"
 	"io/ioutil"
+	"path/filepath"
 	"strconv"
 	"strings"
 
@@ -163,7 +164,10 @@ func (v *V001Entry) FetchExternalEntities(ctx context.Context) error {
 	if v.RPMModel.Package.Hash != nil && v.RPMModel.Package.Hash.Value != nil {
 		oldSHA = swag.StringValue(v.RPMModel.Package.Hash.Value)
 	}
-	artifactFactory := pki.NewArtifactFactory("pgp")
+	artifactFactory, err := pki.NewArtifactFactory(pki.PGP)
+	if err != nil {
+		return err
+	}
 
 	g.Go(func() error {
 		defer hashW.Close()
@@ -358,3 +362,65 @@ func (v V001Entry) Validate() error {
 func (v V001Entry) Attestation() (string, []byte) {
 	return "", nil
 }
+
+func (v V001Entry) CreateFromArtifactProperties(ctx context.Context, props types.ArtifactProperties) (models.ProposedEntry, error) {
+	returnVal := models.Rpm{}
+	re := V001Entry{}
+
+	// we will need artifact, public-key, signature
+	re.RPMModel = models.RpmV001Schema{}
+	re.RPMModel.Package = &models.RpmV001SchemaPackage{}
+
+	var err error
+	artifactBytes := props.ArtifactBytes
+	if artifactBytes == nil {
+		if props.ArtifactPath == nil {
+			return nil, errors.New("path to RPM file (file or URL) must be specified")
+		}
+		if props.ArtifactPath.IsAbs() {
+			re.RPMModel.Package.URL = strfmt.URI(props.ArtifactPath.String())
+		} else {
+			artifactBytes, err = ioutil.ReadFile(filepath.Clean(props.ArtifactPath.Path))
+			if err != nil {
+				return nil, fmt.Errorf("error reading RPM file: %w", err)
+			}
+			re.RPMModel.Package.Content = strfmt.Base64(artifactBytes)
+		}
+	} else {
+		re.RPMModel.Package.Content = strfmt.Base64(artifactBytes)
+	}
+
+	re.RPMModel.PublicKey = &models.RpmV001SchemaPublicKey{}
+	publicKeyBytes := props.PublicKeyBytes
+	if publicKeyBytes == nil {
+		if props.PublicKeyPath == nil {
+			return nil, errors.New("public key must be provided to verify RPM signature")
+		}
+		if props.PublicKeyPath.IsAbs() {
+			re.RPMModel.PublicKey.URL = strfmt.URI(props.PublicKeyPath.String())
+		} else {
+			publicKeyBytes, err = ioutil.ReadFile(filepath.Clean(props.PublicKeyPath.Path))
+			if err != nil {
+				return nil, fmt.Errorf("error reading public key file: %w", err)
+			}
+			re.RPMModel.PublicKey.Content = strfmt.Base64(publicKeyBytes)
+		}
+	} else {
+		re.RPMModel.PublicKey.Content = strfmt.Base64(publicKeyBytes)
+	}
+
+	if err := re.Validate(); err != nil {
+		return nil, err
+	}
+
+	if re.HasExternalEntities() {
+		if err := re.FetchExternalEntities(context.Background()); err != nil {
+			return nil, fmt.Errorf("error retrieving external entities: %v", err)
+		}
+	}
+
+	returnVal.APIVersion = swag.String(re.APIVersion())
+	returnVal.Spec = re.RPMModel
+
+	return &returnVal, nil
+}
diff --git a/pkg/types/types.go b/pkg/types/types.go
index 8095c39..85961f8 100644
--- a/pkg/types/types.go
+++ b/pkg/types/types.go
@@ -16,6 +16,7 @@
 package types
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"sync"
@@ -37,26 +38,43 @@ type RekorType struct {
 // TypeImpl is implemented by all types to support the polymorphic conversion of abstract
 // proposed entry to a working implementation for the versioned type requested, if supported
 type TypeImpl interface {
+	CreateProposedEntry(context.Context, string, ArtifactProperties) (models.ProposedEntry, error)
+	DefaultVersion() string
+	SupportedVersions() []string
 	UnmarshalEntry(pe models.ProposedEntry) (EntryImpl, error)
 }
 
 // VersionedUnmarshal extracts the correct implementing factory function from the type's version map,
 // creates an entry of that versioned type and then calls that versioned type's unmarshal method
 func (rt *RekorType) VersionedUnmarshal(pe models.ProposedEntry, version string) (EntryImpl, error) {
-	if pe == nil {
-		return nil, errors.New("proposed entry cannot be nil")
-	}
-
 	ef, err := rt.VersionMap.GetEntryFactory(version)
 	if err != nil {
-		return nil, fmt.Errorf("%s implementation for version '%v' not found: %w", pe.Kind(), version, err)
+		return nil, fmt.Errorf("%s implementation for version '%v' not found: %w", rt.Kind, version, err)
 	}
 	entry := ef()
 	if entry == nil {
 		return nil, errors.New("failure generating object")
 	}
-	if err := entry.Unmarshal(pe); err != nil {
-		return nil, err
+	if pe == nil {
+		return entry, nil
 	}
-	return entry, nil
+	return entry, entry.Unmarshal(pe)
+}
+
+func (rt *RekorType) SupportedVersions() []string {
+	return rt.VersionMap.SupportedVersions()
+}
+
+// ListImplementedTypes returns a list of all type strings currently known to
+// be implemented
+func ListImplementedTypes() []string {
+	retVal := []string{}
+	TypeMap.Range(func(k interface{}, v interface{}) bool {
+		tf := v.(func() TypeImpl)
+		for _, verStr := range tf().SupportedVersions() {
+			retVal = append(retVal, fmt.Sprintf("%v:%v", k.(string), verStr))
+		}
+		return true
+	})
+	return retVal
 }
diff --git a/pkg/types/versionmap.go b/pkg/types/versionmap.go
index 628576b..5d8c140 100644
--- a/pkg/types/versionmap.go
+++ b/pkg/types/versionmap.go
@@ -29,6 +29,7 @@ type VersionEntryFactoryMap interface {
 	GetEntryFactory(string) (EntryFactory, error) // return the entry factory for the specified version
 	SetEntryFactory(string, EntryFactory) error   // set the entry factory for the specified version
 	Count() int                                   // return the count of entry factories currently in the map
+	SupportedVersions() []string                  // return a list of versions currently stored in the map
 }
 
 // SemVerEntryFactoryMap implements a map that allows implementations to specify their supported versions using
@@ -86,3 +87,11 @@ func (s *SemVerEntryFactoryMap) SetEntryFactory(constraint string, ef EntryFacto
 	s.factoryMap[constraint] = ef
 	return nil
 }
+
+func (s *SemVerEntryFactoryMap) SupportedVersions() []string {
+	var versions []string
+	for k := range s.factoryMap {
+		versions = append(versions, k)
+	}
+	return versions
+}
-- 
GitLab