From 549b3d7903fdcca52f9cd79af69eb9b127ccfbec Mon Sep 17 00:00:00 2001
From: Bob Callaway <bobcallaway@users.noreply.github.com>
Date: Thu, 15 Apr 2021 17:04:17 -0400
Subject: [PATCH] Add new type for JAR archives (#272)

* Add new type for JAR archives

This adds support for a new pluggable type that can extract signatures
from signed JAR files. Per the JAR spec, a special manifest file is
created with the digest hashes of all included content in the JAR file.
It is this special manifest file that is then signed, and included in a
file within the archive in PKCS7 format. The PKCS7 file also includes
the X509 certificate that can be used to verify the signed manifest file
inside of the JAR.

Signed-off-by: Bob Callaway <bob.callaway@gmail.com>
---
 .gitignore                                  |   1 +
 cmd/rekor-cli/app/pflags.go                 |  69 ++-
 cmd/rekor-cli/app/upload.go                 |   5 +
 cmd/rekor-server/app/serve.go               |   3 +
 go.mod                                      |   6 +-
 go.sum                                      |  18 +-
 openapi.yaml                                |  17 +
 pkg/generated/models/jar.go                 | 210 ++++++++
 pkg/generated/models/jar_schema.go          |  29 ++
 pkg/generated/models/jar_v001_schema.go     | 545 ++++++++++++++++++++
 pkg/generated/models/proposed_entry.go      |   6 +
 pkg/generated/restapi/embedded_spec.go      | 271 ++++++++++
 pkg/pki/pkcs7/pkcs7.go                      | 186 +++++++
 pkg/pki/pkcs7/pkcs7_test.go                 | 247 +++++++++
 pkg/pki/pki.go                              |   5 +
 pkg/types/jar/jar.go                        |  66 +++
 pkg/types/jar/jar_schema.json               |  12 +
 pkg/types/jar/jar_test.go                   | 124 +++++
 pkg/types/jar/v0.0.1/entry.go               | 341 ++++++++++++
 pkg/types/jar/v0.0.1/entry_test.go          | 229 ++++++++
 pkg/types/jar/v0.0.1/jar_v0_0_1_schema.json |  79 +++
 tests/e2e_test.go                           |  14 +
 tests/jar.go                                |  86 +++
 tests/test.jar                              | Bin 0 -> 25668 bytes
 24 files changed, 2564 insertions(+), 5 deletions(-)
 create mode 100644 pkg/generated/models/jar.go
 create mode 100644 pkg/generated/models/jar_schema.go
 create mode 100644 pkg/generated/models/jar_v001_schema.go
 create mode 100644 pkg/pki/pkcs7/pkcs7.go
 create mode 100644 pkg/pki/pkcs7/pkcs7_test.go
 create mode 100644 pkg/types/jar/jar.go
 create mode 100644 pkg/types/jar/jar_schema.json
 create mode 100644 pkg/types/jar/jar_test.go
 create mode 100644 pkg/types/jar/v0.0.1/entry.go
 create mode 100644 pkg/types/jar/v0.0.1/entry_test.go
 create mode 100644 pkg/types/jar/v0.0.1/jar_v0_0_1_schema.json
 create mode 100644 tests/jar.go
 create mode 100644 tests/test.jar

diff --git a/.gitignore b/.gitignore
index d4d6dee..2350421 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+.DS_Store
 .idea/*
 .vscode/*
 /cli
diff --git a/cmd/rekor-cli/app/pflags.go b/cmd/rekor-cli/app/pflags.go
index cfd8aca..f701139 100644
--- a/cmd/rekor-cli/app/pflags.go
+++ b/cmd/rekor-cli/app/pflags.go
@@ -34,6 +34,7 @@ import (
 	"github.com/spf13/viper"
 
 	"github.com/sigstore/rekor/pkg/generated/models"
+	jar_v001 "github.com/sigstore/rekor/pkg/types/jar/v0.0.1"
 	rekord_v001 "github.com/sigstore/rekor/pkg/types/rekord/v0.0.1"
 	rpm_v001 "github.com/sigstore/rekor/pkg/types/rpm/v0.0.1"
 )
@@ -131,7 +132,7 @@ func validateArtifactPFlags(uuidValid, indexValid bool) error {
 		if signature == "" && typeStr == "rekord" {
 			return errors.New("--signature is required when --artifact is used")
 		}
-		if publicKey == "" {
+		if publicKey == "" && typeStr != "jar" {
 			return errors.New("--public-key is required when --artifact is used")
 		}
 	}
@@ -139,6 +140,71 @@ func validateArtifactPFlags(uuidValid, indexValid bool) error {
 	return 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)
+		} 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
+	}
+
+	return &returnVal, nil
+}
+
 func CreateRpmFromPFlags() (models.ProposedEntry, error) {
 	//TODO: how to select version of item to create
 	returnVal := models.Rpm{}
@@ -358,6 +424,7 @@ func (t *typeFlag) Set(s string) error {
 	set := map[string]struct{}{
 		"rekord": {},
 		"rpm":    {},
+		"jar":    {},
 	}
 	if _, ok := set[s]; ok {
 		t.value = s
diff --git a/cmd/rekor-cli/app/upload.go b/cmd/rekor-cli/app/upload.go
index 2c49d76..c9cfc6a 100644
--- a/cmd/rekor-cli/app/upload.go
+++ b/cmd/rekor-cli/app/upload.go
@@ -78,6 +78,11 @@ var uploadCmd = &cobra.Command{
 			if err != nil {
 				return nil, err
 			}
+		case "jar":
+			entry, err = CreateJarFromPFlags()
+			if err != nil {
+				return nil, err
+			}
 		default:
 			return nil, errors.New("unknown type specified")
 		}
diff --git a/cmd/rekor-server/app/serve.go b/cmd/rekor-server/app/serve.go
index d92b11d..2d68d96 100644
--- a/cmd/rekor-server/app/serve.go
+++ b/cmd/rekor-server/app/serve.go
@@ -28,6 +28,8 @@ import (
 	"github.com/sigstore/rekor/pkg/generated/restapi"
 	"github.com/sigstore/rekor/pkg/generated/restapi/operations"
 	"github.com/sigstore/rekor/pkg/log"
+	"github.com/sigstore/rekor/pkg/types/jar"
+	jar_v001 "github.com/sigstore/rekor/pkg/types/jar/v0.0.1"
 	"github.com/sigstore/rekor/pkg/types/rekord"
 	rekord_v001 "github.com/sigstore/rekor/pkg/types/rekord/v0.0.1"
 	"github.com/sigstore/rekor/pkg/types/rpm"
@@ -63,6 +65,7 @@ var serveCmd = &cobra.Command{
 		pluggableTypeMap := map[string]string{
 			rekord.KIND: rekord_v001.APIVERSION,
 			rpm.KIND:    rpm_v001.APIVERSION,
+			jar.KIND:    jar_v001.APIVERSION,
 		}
 
 		for k, v := range pluggableTypeMap {
diff --git a/go.mod b/go.mod
index c490a5b..daf05c4 100644
--- a/go.mod
+++ b/go.mod
@@ -17,6 +17,7 @@ require (
 	github.com/go-openapi/validate v0.20.2
 	github.com/google/rpmpack v0.0.0-20210107155803-d6befbf05148
 	github.com/google/trillian v1.3.13
+	github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c // indirect
 	github.com/jedisct1/go-minisign v0.0.0-20210106175330-e54e81d562c7
 	github.com/mediocregopher/radix/v4 v4.0.0-beta.1
 	github.com/mitchellh/go-homedir v1.1.0
@@ -24,11 +25,14 @@ require (
 	github.com/pkg/errors v0.9.1
 	github.com/prometheus/client_golang v1.10.0
 	github.com/rs/cors v1.7.0
-	github.com/sigstore/sigstore v0.0.0-20210414183523-383d6554c35c
+	github.com/sassoftware/relic v7.2.1+incompatible
+	github.com/sigstore/sigstore v0.0.0-20210415112811-cb2061113e4a
 	github.com/spf13/cobra v1.1.3
 	github.com/spf13/pflag v1.0.5
 	github.com/spf13/viper v1.7.1
+	github.com/ulikunitz/xz v0.5.9 // indirect
 	github.com/urfave/negroni v1.0.0
+	github.com/zalando/go-keyring v0.1.1 // indirect
 	go.uber.org/goleak v1.1.10
 	go.uber.org/zap v1.16.0
 	gocloud.dev v0.22.0
diff --git a/go.sum b/go.sum
index 4424062..aa71003 100644
--- a/go.sum
+++ b/go.sum
@@ -194,6 +194,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma
 github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/danieljoos/wincred v1.1.0 h1:3RNcEpBg4IhIChZdFRSdlQt1QjCp1sMAPIrOnm7Yf8g=
+github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
 github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -417,6 +419,8 @@ github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJA
 github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
 github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
 github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
+github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
+github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -585,6 +589,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
 github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
 github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
 github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
+github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c h1:aY2hhxLhjEAbfXOx2nRJxCXezC6CO2V/yN+OCr1srtk=
+github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
 github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
@@ -847,6 +853,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
 github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
 github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
+github.com/sassoftware/relic v7.2.1+incompatible h1:Pwyh1F3I0r4clFJXkSI8bOyJINGqpgjJU3DYAZeI05A=
+github.com/sassoftware/relic v7.2.1+incompatible/go.mod h1:CWfAxv73/iLZ17rbyhIEq3K9hs5w6FpNMdUT//qR+zk=
 github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/segmentio/ksuid v1.0.3/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
@@ -857,8 +865,8 @@ github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxr
 github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/sigstore/rekor v0.1.1/go.mod h1:b+T8TvGKWgaFbtPRQgF/gXjbj/R9HdJ5lA93cnGT3Sc=
-github.com/sigstore/sigstore v0.0.0-20210414183523-383d6554c35c h1:YPd6DUue2Vi6cavCGx7cgz3CM2CGS7LHm5FamwbUPic=
-github.com/sigstore/sigstore v0.0.0-20210414183523-383d6554c35c/go.mod h1:EoLIp5JbrCE2VZqdCCIemNEdNYiOcdwF0igIvorqo1o=
+github.com/sigstore/sigstore v0.0.0-20210415112811-cb2061113e4a h1:neuFPsd2BVJvqH5CdoPwKjZI3ZtD2q2PSf+7zrZc5gM=
+github.com/sigstore/sigstore v0.0.0-20210415112811-cb2061113e4a/go.mod h1:EoLIp5JbrCE2VZqdCCIemNEdNYiOcdwF0igIvorqo1o=
 github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
@@ -908,6 +916,7 @@ github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3
 github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
 github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
 github.com/stretchr/testify v0.0.0-20170130113145-4d4bfba8f1d1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
@@ -932,8 +941,9 @@ github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGr
 github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
 github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
-github.com/ulikunitz/xz v0.5.7 h1:YvTNdFzX6+W5m9msiYg/zpkSURPPtOlzbqYjrFn7Yt4=
 github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
+github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I=
+github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 github.com/ultraware/funlen v0.0.1/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA=
 github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
 github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
@@ -956,6 +966,8 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/zalando/go-keyring v0.1.1 h1:w2V9lcx/Uj4l+dzAf1m9s+DJ1O8ROkEHnynonHjTcYE=
+github.com/zalando/go-keyring v0.1.1/go.mod h1:OIC+OZ28XbmwFxU/Rp9V7eKzZjamBJwRzC8UFJH9+L8=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
diff --git a/openapi.yaml b/openapi.yaml
index fe705d8..0f9e240 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -269,6 +269,23 @@ definitions:
         - spec
       additionalProperties: false
 
+  jar:
+    type: object
+    description: Java Archive (JAR)
+    allOf:
+    - $ref: '#/definitions/ProposedEntry'
+    - properties:
+        apiVersion:
+          type: string
+          pattern: ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$
+        spec:
+          type: object
+          $ref: 'pkg/types/jar/jar_schema.json'
+      required:
+        - apiVersion
+        - spec
+      additionalProperties: false
+
   LogEntry:
     type: object
     additionalProperties:
diff --git a/pkg/generated/models/jar.go b/pkg/generated/models/jar.go
new file mode 100644
index 0000000..3df3d21
--- /dev/null
+++ b/pkg/generated/models/jar.go
@@ -0,0 +1,210 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package models
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+
+	"github.com/go-openapi/errors"
+	"github.com/go-openapi/strfmt"
+	"github.com/go-openapi/swag"
+	"github.com/go-openapi/validate"
+)
+
+// Jar Java Archive (JAR)
+//
+// swagger:model jar
+type Jar struct {
+
+	// api version
+	// Required: true
+	// Pattern: ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$
+	APIVersion *string `json:"apiVersion"`
+
+	// spec
+	// Required: true
+	Spec JarSchema `json:"spec"`
+}
+
+// Kind gets the kind of this subtype
+func (m *Jar) Kind() string {
+	return "jar"
+}
+
+// SetKind sets the kind of this subtype
+func (m *Jar) SetKind(val string) {
+}
+
+// UnmarshalJSON unmarshals this object with a polymorphic type from a JSON structure
+func (m *Jar) UnmarshalJSON(raw []byte) error {
+	var data struct {
+
+		// api version
+		// Required: true
+		// Pattern: ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$
+		APIVersion *string `json:"apiVersion"`
+
+		// spec
+		// Required: true
+		Spec JarSchema `json:"spec"`
+	}
+	buf := bytes.NewBuffer(raw)
+	dec := json.NewDecoder(buf)
+	dec.UseNumber()
+
+	if err := dec.Decode(&data); err != nil {
+		return err
+	}
+
+	var base struct {
+		/* Just the base type fields. Used for unmashalling polymorphic types.*/
+
+		Kind string `json:"kind"`
+	}
+	buf = bytes.NewBuffer(raw)
+	dec = json.NewDecoder(buf)
+	dec.UseNumber()
+
+	if err := dec.Decode(&base); err != nil {
+		return err
+	}
+
+	var result Jar
+
+	if base.Kind != result.Kind() {
+		/* Not the type we're looking for. */
+		return errors.New(422, "invalid kind value: %q", base.Kind)
+	}
+
+	result.APIVersion = data.APIVersion
+	result.Spec = data.Spec
+
+	*m = result
+
+	return nil
+}
+
+// MarshalJSON marshals this object with a polymorphic type to a JSON structure
+func (m Jar) MarshalJSON() ([]byte, error) {
+	var b1, b2, b3 []byte
+	var err error
+	b1, err = json.Marshal(struct {
+
+		// api version
+		// Required: true
+		// Pattern: ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$
+		APIVersion *string `json:"apiVersion"`
+
+		// spec
+		// Required: true
+		Spec JarSchema `json:"spec"`
+	}{
+
+		APIVersion: m.APIVersion,
+
+		Spec: m.Spec,
+	})
+	if err != nil {
+		return nil, err
+	}
+	b2, err = json.Marshal(struct {
+		Kind string `json:"kind"`
+	}{
+
+		Kind: m.Kind(),
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return swag.ConcatJSON(b1, b2, b3), nil
+}
+
+// Validate validates this jar
+func (m *Jar) Validate(formats strfmt.Registry) error {
+	var res []error
+
+	if err := m.validateAPIVersion(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if err := m.validateSpec(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+func (m *Jar) validateAPIVersion(formats strfmt.Registry) error {
+
+	if err := validate.Required("apiVersion", "body", m.APIVersion); err != nil {
+		return err
+	}
+
+	if err := validate.Pattern("apiVersion", "body", *m.APIVersion, `^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (m *Jar) validateSpec(formats strfmt.Registry) error {
+
+	if m.Spec == nil {
+		return errors.Required("spec", "body", nil)
+	}
+
+	return nil
+}
+
+// ContextValidate validate this jar based on the context it is used
+func (m *Jar) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
+	var res []error
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+// MarshalBinary interface implementation
+func (m *Jar) MarshalBinary() ([]byte, error) {
+	if m == nil {
+		return nil, nil
+	}
+	return swag.WriteJSON(m)
+}
+
+// UnmarshalBinary interface implementation
+func (m *Jar) UnmarshalBinary(b []byte) error {
+	var res Jar
+	if err := swag.ReadJSON(b, &res); err != nil {
+		return err
+	}
+	*m = res
+	return nil
+}
diff --git a/pkg/generated/models/jar_schema.go b/pkg/generated/models/jar_schema.go
new file mode 100644
index 0000000..d45c53d
--- /dev/null
+++ b/pkg/generated/models/jar_schema.go
@@ -0,0 +1,29 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package models
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+// JarSchema JAR Schema
+//
+// Schema for JAR objects
+//
+// swagger:model jarSchema
+type JarSchema interface{}
diff --git a/pkg/generated/models/jar_v001_schema.go b/pkg/generated/models/jar_v001_schema.go
new file mode 100644
index 0000000..f2fcab6
--- /dev/null
+++ b/pkg/generated/models/jar_v001_schema.go
@@ -0,0 +1,545 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package models
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"context"
+	"encoding/json"
+
+	"github.com/go-openapi/errors"
+	"github.com/go-openapi/strfmt"
+	"github.com/go-openapi/swag"
+	"github.com/go-openapi/validate"
+)
+
+// JarV001Schema JAR v0.0.1 Schema
+//
+// Schema for JAR entries
+//
+// swagger:model jarV001Schema
+type JarV001Schema struct {
+
+	// archive
+	// Required: true
+	Archive *JarV001SchemaArchive `json:"archive"`
+
+	// Arbitrary content to be included in the verifiable entry in the transparency log
+	ExtraData interface{} `json:"extraData,omitempty"`
+
+	// signature
+	Signature *JarV001SchemaSignature `json:"signature,omitempty"`
+}
+
+// Validate validates this jar v001 schema
+func (m *JarV001Schema) Validate(formats strfmt.Registry) error {
+	var res []error
+
+	if err := m.validateArchive(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if err := m.validateSignature(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+func (m *JarV001Schema) validateArchive(formats strfmt.Registry) error {
+
+	if err := validate.Required("archive", "body", m.Archive); err != nil {
+		return err
+	}
+
+	if m.Archive != nil {
+		if err := m.Archive.Validate(formats); err != nil {
+			if ve, ok := err.(*errors.Validation); ok {
+				return ve.ValidateName("archive")
+			}
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (m *JarV001Schema) validateSignature(formats strfmt.Registry) error {
+	if swag.IsZero(m.Signature) { // not required
+		return nil
+	}
+
+	if m.Signature != nil {
+		if err := m.Signature.Validate(formats); err != nil {
+			if ve, ok := err.(*errors.Validation); ok {
+				return ve.ValidateName("signature")
+			}
+			return err
+		}
+	}
+
+	return nil
+}
+
+// ContextValidate validate this jar v001 schema based on the context it is used
+func (m *JarV001Schema) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
+	var res []error
+
+	if err := m.contextValidateArchive(ctx, formats); err != nil {
+		res = append(res, err)
+	}
+
+	if err := m.contextValidateSignature(ctx, formats); err != nil {
+		res = append(res, err)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+func (m *JarV001Schema) contextValidateArchive(ctx context.Context, formats strfmt.Registry) error {
+
+	if m.Archive != nil {
+		if err := m.Archive.ContextValidate(ctx, formats); err != nil {
+			if ve, ok := err.(*errors.Validation); ok {
+				return ve.ValidateName("archive")
+			}
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (m *JarV001Schema) contextValidateSignature(ctx context.Context, formats strfmt.Registry) error {
+
+	if m.Signature != nil {
+		if err := m.Signature.ContextValidate(ctx, formats); err != nil {
+			if ve, ok := err.(*errors.Validation); ok {
+				return ve.ValidateName("signature")
+			}
+			return err
+		}
+	}
+
+	return nil
+}
+
+// MarshalBinary interface implementation
+func (m *JarV001Schema) MarshalBinary() ([]byte, error) {
+	if m == nil {
+		return nil, nil
+	}
+	return swag.WriteJSON(m)
+}
+
+// UnmarshalBinary interface implementation
+func (m *JarV001Schema) UnmarshalBinary(b []byte) error {
+	var res JarV001Schema
+	if err := swag.ReadJSON(b, &res); err != nil {
+		return err
+	}
+	*m = res
+	return nil
+}
+
+// JarV001SchemaArchive Information about the archive associated with the entry
+//
+// swagger:model JarV001SchemaArchive
+type JarV001SchemaArchive struct {
+
+	// Specifies the archive inline within the document
+	// Format: byte
+	Content strfmt.Base64 `json:"content,omitempty"`
+
+	// hash
+	Hash *JarV001SchemaArchiveHash `json:"hash,omitempty"`
+
+	// Specifies the location of the archive; if this is specified, a hash value must also be provided
+	// Format: uri
+	URL strfmt.URI `json:"url,omitempty"`
+}
+
+// Validate validates this jar v001 schema archive
+func (m *JarV001SchemaArchive) Validate(formats strfmt.Registry) error {
+	var res []error
+
+	if err := m.validateHash(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if err := m.validateURL(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+func (m *JarV001SchemaArchive) validateHash(formats strfmt.Registry) error {
+	if swag.IsZero(m.Hash) { // not required
+		return nil
+	}
+
+	if m.Hash != nil {
+		if err := m.Hash.Validate(formats); err != nil {
+			if ve, ok := err.(*errors.Validation); ok {
+				return ve.ValidateName("archive" + "." + "hash")
+			}
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (m *JarV001SchemaArchive) validateURL(formats strfmt.Registry) error {
+	if swag.IsZero(m.URL) { // not required
+		return nil
+	}
+
+	if err := validate.FormatOf("archive"+"."+"url", "body", "uri", m.URL.String(), formats); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// ContextValidate validate this jar v001 schema archive based on the context it is used
+func (m *JarV001SchemaArchive) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
+	var res []error
+
+	if err := m.contextValidateHash(ctx, formats); err != nil {
+		res = append(res, err)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+func (m *JarV001SchemaArchive) contextValidateHash(ctx context.Context, formats strfmt.Registry) error {
+
+	if m.Hash != nil {
+		if err := m.Hash.ContextValidate(ctx, formats); err != nil {
+			if ve, ok := err.(*errors.Validation); ok {
+				return ve.ValidateName("archive" + "." + "hash")
+			}
+			return err
+		}
+	}
+
+	return nil
+}
+
+// MarshalBinary interface implementation
+func (m *JarV001SchemaArchive) MarshalBinary() ([]byte, error) {
+	if m == nil {
+		return nil, nil
+	}
+	return swag.WriteJSON(m)
+}
+
+// UnmarshalBinary interface implementation
+func (m *JarV001SchemaArchive) UnmarshalBinary(b []byte) error {
+	var res JarV001SchemaArchive
+	if err := swag.ReadJSON(b, &res); err != nil {
+		return err
+	}
+	*m = res
+	return nil
+}
+
+// JarV001SchemaArchiveHash Specifies the hash algorithm and value encompassing the entire signed archive
+//
+// swagger:model JarV001SchemaArchiveHash
+type JarV001SchemaArchiveHash struct {
+
+	// The hashing function used to compute the hash value
+	// Required: true
+	// Enum: [sha256]
+	Algorithm *string `json:"algorithm"`
+
+	// The hash value for the archive
+	// Required: true
+	Value *string `json:"value"`
+}
+
+// Validate validates this jar v001 schema archive hash
+func (m *JarV001SchemaArchiveHash) Validate(formats strfmt.Registry) error {
+	var res []error
+
+	if err := m.validateAlgorithm(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if err := m.validateValue(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+var jarV001SchemaArchiveHashTypeAlgorithmPropEnum []interface{}
+
+func init() {
+	var res []string
+	if err := json.Unmarshal([]byte(`["sha256"]`), &res); err != nil {
+		panic(err)
+	}
+	for _, v := range res {
+		jarV001SchemaArchiveHashTypeAlgorithmPropEnum = append(jarV001SchemaArchiveHashTypeAlgorithmPropEnum, v)
+	}
+}
+
+const (
+
+	// JarV001SchemaArchiveHashAlgorithmSha256 captures enum value "sha256"
+	JarV001SchemaArchiveHashAlgorithmSha256 string = "sha256"
+)
+
+// prop value enum
+func (m *JarV001SchemaArchiveHash) validateAlgorithmEnum(path, location string, value string) error {
+	if err := validate.EnumCase(path, location, value, jarV001SchemaArchiveHashTypeAlgorithmPropEnum, true); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (m *JarV001SchemaArchiveHash) validateAlgorithm(formats strfmt.Registry) error {
+
+	if err := validate.Required("archive"+"."+"hash"+"."+"algorithm", "body", m.Algorithm); err != nil {
+		return err
+	}
+
+	// value enum
+	if err := m.validateAlgorithmEnum("archive"+"."+"hash"+"."+"algorithm", "body", *m.Algorithm); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (m *JarV001SchemaArchiveHash) validateValue(formats strfmt.Registry) error {
+
+	if err := validate.Required("archive"+"."+"hash"+"."+"value", "body", m.Value); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// ContextValidate validates this jar v001 schema archive hash based on context it is used
+func (m *JarV001SchemaArchiveHash) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
+	return nil
+}
+
+// MarshalBinary interface implementation
+func (m *JarV001SchemaArchiveHash) MarshalBinary() ([]byte, error) {
+	if m == nil {
+		return nil, nil
+	}
+	return swag.WriteJSON(m)
+}
+
+// UnmarshalBinary interface implementation
+func (m *JarV001SchemaArchiveHash) UnmarshalBinary(b []byte) error {
+	var res JarV001SchemaArchiveHash
+	if err := swag.ReadJSON(b, &res); err != nil {
+		return err
+	}
+	*m = res
+	return nil
+}
+
+// JarV001SchemaSignature Information about the included signature in the JAR file
+//
+// swagger:model JarV001SchemaSignature
+type JarV001SchemaSignature struct {
+
+	// Specifies the PKCS7 signature embedded within the JAR file
+	// Required: true
+	// Format: byte
+	Content *strfmt.Base64 `json:"content"`
+
+	// public key
+	// Required: true
+	PublicKey *JarV001SchemaSignaturePublicKey `json:"publicKey"`
+}
+
+// Validate validates this jar v001 schema signature
+func (m *JarV001SchemaSignature) Validate(formats strfmt.Registry) error {
+	var res []error
+
+	if err := m.validateContent(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if err := m.validatePublicKey(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+func (m *JarV001SchemaSignature) validateContent(formats strfmt.Registry) error {
+
+	if err := validate.Required("signature"+"."+"content", "body", m.Content); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (m *JarV001SchemaSignature) validatePublicKey(formats strfmt.Registry) error {
+
+	if err := validate.Required("signature"+"."+"publicKey", "body", m.PublicKey); err != nil {
+		return err
+	}
+
+	if m.PublicKey != nil {
+		if err := m.PublicKey.Validate(formats); err != nil {
+			if ve, ok := err.(*errors.Validation); ok {
+				return ve.ValidateName("signature" + "." + "publicKey")
+			}
+			return err
+		}
+	}
+
+	return nil
+}
+
+// ContextValidate validate this jar v001 schema signature based on the context it is used
+func (m *JarV001SchemaSignature) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
+	var res []error
+
+	if err := m.contextValidatePublicKey(ctx, formats); err != nil {
+		res = append(res, err)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+func (m *JarV001SchemaSignature) contextValidatePublicKey(ctx context.Context, formats strfmt.Registry) error {
+
+	if m.PublicKey != nil {
+		if err := m.PublicKey.ContextValidate(ctx, formats); err != nil {
+			if ve, ok := err.(*errors.Validation); ok {
+				return ve.ValidateName("signature" + "." + "publicKey")
+			}
+			return err
+		}
+	}
+
+	return nil
+}
+
+// MarshalBinary interface implementation
+func (m *JarV001SchemaSignature) MarshalBinary() ([]byte, error) {
+	if m == nil {
+		return nil, nil
+	}
+	return swag.WriteJSON(m)
+}
+
+// UnmarshalBinary interface implementation
+func (m *JarV001SchemaSignature) UnmarshalBinary(b []byte) error {
+	var res JarV001SchemaSignature
+	if err := swag.ReadJSON(b, &res); err != nil {
+		return err
+	}
+	*m = res
+	return nil
+}
+
+// JarV001SchemaSignaturePublicKey The X509 certificate containing the public key JAR which verifies the signature of the JAR
+//
+// swagger:model JarV001SchemaSignaturePublicKey
+type JarV001SchemaSignaturePublicKey struct {
+
+	// Specifies the content of the X509 certificate containing the public key used to verify the signature
+	// Required: true
+	// Format: byte
+	Content *strfmt.Base64 `json:"content"`
+}
+
+// Validate validates this jar v001 schema signature public key
+func (m *JarV001SchemaSignaturePublicKey) Validate(formats strfmt.Registry) error {
+	var res []error
+
+	if err := m.validateContent(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+func (m *JarV001SchemaSignaturePublicKey) validateContent(formats strfmt.Registry) error {
+
+	if err := validate.Required("signature"+"."+"publicKey"+"."+"content", "body", m.Content); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// ContextValidate validates this jar v001 schema signature public key based on context it is used
+func (m *JarV001SchemaSignaturePublicKey) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
+	return nil
+}
+
+// MarshalBinary interface implementation
+func (m *JarV001SchemaSignaturePublicKey) MarshalBinary() ([]byte, error) {
+	if m == nil {
+		return nil, nil
+	}
+	return swag.WriteJSON(m)
+}
+
+// UnmarshalBinary interface implementation
+func (m *JarV001SchemaSignaturePublicKey) UnmarshalBinary(b []byte) error {
+	var res JarV001SchemaSignaturePublicKey
+	if err := swag.ReadJSON(b, &res); err != nil {
+		return err
+	}
+	*m = res
+	return nil
+}
diff --git a/pkg/generated/models/proposed_entry.go b/pkg/generated/models/proposed_entry.go
index 511b3f4..4643b52 100644
--- a/pkg/generated/models/proposed_entry.go
+++ b/pkg/generated/models/proposed_entry.go
@@ -115,6 +115,12 @@ func unmarshalProposedEntry(data []byte, consumer runtime.Consumer) (ProposedEnt
 			return nil, err
 		}
 		return &result, nil
+	case "jar":
+		var result Jar
+		if err := consumer.Consume(buf2, &result); err != nil {
+			return nil, err
+		}
+		return &result, nil
 	case "rekord":
 		var result Rekord
 		if err := consumer.Consume(buf2, &result); err != nil {
diff --git a/pkg/generated/restapi/embedded_spec.go b/pkg/generated/restapi/embedded_spec.go
index b37e678..94f16d2 100644
--- a/pkg/generated/restapi/embedded_spec.go
+++ b/pkg/generated/restapi/embedded_spec.go
@@ -539,6 +539,32 @@ func init() {
         }
       }
     },
+    "jar": {
+      "description": "Java Archive (JAR)",
+      "type": "object",
+      "allOf": [
+        {
+          "$ref": "#/definitions/ProposedEntry"
+        },
+        {
+          "required": [
+            "apiVersion",
+            "spec"
+          ],
+          "properties": {
+            "apiVersion": {
+              "type": "string",
+              "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"
+            },
+            "spec": {
+              "type": "object",
+              "$ref": "pkg/types/jar/jar_schema.json"
+            }
+          },
+          "additionalProperties": false
+        }
+      ]
+    },
     "rekord": {
       "description": "Rekord object",
       "type": "object",
@@ -1031,6 +1057,119 @@ func init() {
         }
       }
     },
+    "JarV001SchemaArchive": {
+      "description": "Information about the archive associated with the entry",
+      "type": "object",
+      "oneOf": [
+        {
+          "required": [
+            "url"
+          ]
+        },
+        {
+          "required": [
+            "content"
+          ]
+        }
+      ],
+      "properties": {
+        "content": {
+          "description": "Specifies the archive inline within the document",
+          "type": "string",
+          "format": "byte"
+        },
+        "hash": {
+          "description": "Specifies the hash algorithm and value encompassing the entire signed archive",
+          "type": "object",
+          "required": [
+            "algorithm",
+            "value"
+          ],
+          "properties": {
+            "algorithm": {
+              "description": "The hashing function used to compute the hash value",
+              "type": "string",
+              "enum": [
+                "sha256"
+              ]
+            },
+            "value": {
+              "description": "The hash value for the archive",
+              "type": "string"
+            }
+          }
+        },
+        "url": {
+          "description": "Specifies the location of the archive; if this is specified, a hash value must also be provided",
+          "type": "string",
+          "format": "uri"
+        }
+      }
+    },
+    "JarV001SchemaArchiveHash": {
+      "description": "Specifies the hash algorithm and value encompassing the entire signed archive",
+      "type": "object",
+      "required": [
+        "algorithm",
+        "value"
+      ],
+      "properties": {
+        "algorithm": {
+          "description": "The hashing function used to compute the hash value",
+          "type": "string",
+          "enum": [
+            "sha256"
+          ]
+        },
+        "value": {
+          "description": "The hash value for the archive",
+          "type": "string"
+        }
+      }
+    },
+    "JarV001SchemaSignature": {
+      "description": "Information about the included signature in the JAR file",
+      "type": "object",
+      "required": [
+        "publicKey",
+        "content"
+      ],
+      "properties": {
+        "content": {
+          "description": "Specifies the PKCS7 signature embedded within the JAR file ",
+          "type": "string",
+          "format": "byte"
+        },
+        "publicKey": {
+          "description": "The X509 certificate containing the public key JAR which verifies the signature of the JAR",
+          "type": "object",
+          "required": [
+            "content"
+          ],
+          "properties": {
+            "content": {
+              "description": "Specifies the content of the X509 certificate containing the public key used to verify the signature",
+              "type": "string",
+              "format": "byte"
+            }
+          }
+        }
+      }
+    },
+    "JarV001SchemaSignaturePublicKey": {
+      "description": "The X509 certificate containing the public key JAR which verifies the signature of the JAR",
+      "type": "object",
+      "required": [
+        "content"
+      ],
+      "properties": {
+        "content": {
+          "description": "Specifies the content of the X509 certificate containing the public key used to verify the signature",
+          "type": "string",
+          "format": "byte"
+        }
+      }
+    },
     "LogEntry": {
       "type": "object",
       "additionalProperties": {
@@ -1502,6 +1641,138 @@ func init() {
         }
       }
     },
+    "jar": {
+      "description": "Java Archive (JAR)",
+      "type": "object",
+      "allOf": [
+        {
+          "$ref": "#/definitions/ProposedEntry"
+        },
+        {
+          "required": [
+            "apiVersion",
+            "spec"
+          ],
+          "properties": {
+            "apiVersion": {
+              "type": "string",
+              "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"
+            },
+            "spec": {
+              "$ref": "#/definitions/jarSchema"
+            }
+          },
+          "additionalProperties": false
+        }
+      ]
+    },
+    "jarSchema": {
+      "description": "Schema for JAR objects",
+      "type": "object",
+      "title": "JAR Schema",
+      "oneOf": [
+        {
+          "$ref": "#/definitions/jarV001Schema"
+        }
+      ],
+      "$schema": "http://json-schema.org/draft-07/schema",
+      "$id": "http://rekor.sigstore.dev/types/jar/jar_schema.json"
+    },
+    "jarV001Schema": {
+      "description": "Schema for JAR entries",
+      "type": "object",
+      "title": "JAR v0.0.1 Schema",
+      "required": [
+        "archive"
+      ],
+      "properties": {
+        "archive": {
+          "description": "Information about the archive associated with the entry",
+          "type": "object",
+          "oneOf": [
+            {
+              "required": [
+                "url"
+              ]
+            },
+            {
+              "required": [
+                "content"
+              ]
+            }
+          ],
+          "properties": {
+            "content": {
+              "description": "Specifies the archive inline within the document",
+              "type": "string",
+              "format": "byte"
+            },
+            "hash": {
+              "description": "Specifies the hash algorithm and value encompassing the entire signed archive",
+              "type": "object",
+              "required": [
+                "algorithm",
+                "value"
+              ],
+              "properties": {
+                "algorithm": {
+                  "description": "The hashing function used to compute the hash value",
+                  "type": "string",
+                  "enum": [
+                    "sha256"
+                  ]
+                },
+                "value": {
+                  "description": "The hash value for the archive",
+                  "type": "string"
+                }
+              }
+            },
+            "url": {
+              "description": "Specifies the location of the archive; if this is specified, a hash value must also be provided",
+              "type": "string",
+              "format": "uri"
+            }
+          }
+        },
+        "extraData": {
+          "description": "Arbitrary content to be included in the verifiable entry in the transparency log",
+          "type": "object",
+          "additionalProperties": true
+        },
+        "signature": {
+          "description": "Information about the included signature in the JAR file",
+          "type": "object",
+          "required": [
+            "publicKey",
+            "content"
+          ],
+          "properties": {
+            "content": {
+              "description": "Specifies the PKCS7 signature embedded within the JAR file ",
+              "type": "string",
+              "format": "byte"
+            },
+            "publicKey": {
+              "description": "The X509 certificate containing the public key JAR which verifies the signature of the JAR",
+              "type": "object",
+              "required": [
+                "content"
+              ],
+              "properties": {
+                "content": {
+                  "description": "Specifies the content of the X509 certificate containing the public key used to verify the signature",
+                  "type": "string",
+                  "format": "byte"
+                }
+              }
+            }
+          }
+        }
+      },
+      "$schema": "http://json-schema.org/draft-07/schema",
+      "$id": "http://rekor.sigstore.dev/types/jar/jar_v0_0_1_schema.json"
+    },
     "rekord": {
       "description": "Rekord object",
       "type": "object",
diff --git a/pkg/pki/pkcs7/pkcs7.go b/pkg/pki/pkcs7/pkcs7.go
new file mode 100644
index 0000000..98f90a0
--- /dev/null
+++ b/pkg/pki/pkcs7/pkcs7.go
@@ -0,0 +1,186 @@
+/*
+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 pkcs7
+
+import (
+	"bytes"
+	"crypto"
+	"crypto/x509"
+	"encoding/pem"
+	"errors"
+	"fmt"
+	"io"
+	"io/ioutil"
+
+	"github.com/sassoftware/relic/lib/pkcs7"
+)
+
+type Signature struct {
+	signedData pkcs7.SignedData
+	detached   bool
+	raw        *[]byte
+}
+
+// NewSignature creates and validates an PKCS7 signature object
+func NewSignature(r io.Reader) (*Signature, error) {
+	b, err := ioutil.ReadAll(r)
+	if err != nil {
+		return nil, err
+	}
+	// try PEM decoding first
+	var pkcsBytes *[]byte
+	block, _ := pem.Decode(b)
+	if block != nil {
+		if block.Type != "PKCS7" {
+			return nil, fmt.Errorf("unknown PEM block type %s found during PKCS7 parsing", block.Type)
+		}
+		pkcsBytes = &block.Bytes
+	} else {
+		// PEM decoding failed, it might just be raw ASN.1 data
+		pkcsBytes = &b
+	}
+
+	psd, err := pkcs7.Unmarshal(*pkcsBytes)
+	if err != nil {
+		return nil, err
+	}
+
+	// we store the detached signature as the raw, canonical format
+	if _, err := psd.Detach(); err != nil {
+		return nil, err
+	}
+
+	detached, err := psd.Marshal()
+	if err != nil {
+		return nil, err
+	}
+
+	cb, err := psd.Content.ContentInfo.Bytes()
+	if err != nil {
+		return nil, err
+	}
+
+	return &Signature{
+		signedData: psd.Content,
+		raw:        &detached,
+		detached:   cb == nil,
+	}, nil
+}
+
+// CanonicalValue implements the pki.Signature interface
+func (s Signature) CanonicalValue() ([]byte, error) {
+	if s.raw == nil {
+		return nil, fmt.Errorf("PKCS7 signature has not been initialized")
+	}
+
+	p := pem.Block{
+		Type:  "PKCS7",
+		Bytes: *s.raw,
+	}
+
+	var buf bytes.Buffer
+	if err := pem.Encode(&buf, &p); err != nil {
+		return nil, err
+	}
+	return buf.Bytes(), nil
+}
+
+// Verify implements the pki.Signature interface
+func (s Signature) Verify(r io.Reader, k interface{}) error {
+	if len(*s.raw) == 0 {
+		return fmt.Errorf("PKCS7 signature has not been initialized")
+	}
+
+	// if content was passed to this, verify signature as if it were detached
+	bb := bytes.Buffer{}
+	var extContent []byte
+	if r != nil {
+		n, err := io.Copy(&bb, r)
+		if err != nil {
+			return err
+		}
+		if n > 0 {
+			extContent = bb.Bytes()
+		} else if s.detached {
+			return errors.New("PKCS7 signature is detached and there is no external content to verify against")
+		}
+	}
+
+	if _, err := s.signedData.Verify(extContent, false); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// PublicKey Public Key contained in cert inside PKCS7 bundle
+type PublicKey struct {
+	key     crypto.PublicKey
+	certs   []*x509.Certificate
+	rawCert []byte
+}
+
+// NewPublicKey implements the pki.PublicKey interface
+func NewPublicKey(r io.Reader) (*PublicKey, error) {
+	rawPub, err := ioutil.ReadAll(r)
+	if err != nil {
+		return nil, err
+	}
+
+	// try PEM decoding first
+	var pkcsBytes *[]byte
+	block, _ := pem.Decode(rawPub)
+	if block != nil {
+		if block.Type != "PKCS7" {
+			return nil, fmt.Errorf("unknown PEM block type %s found during PKCS7 parsing", block.Type)
+		}
+		pkcsBytes = &block.Bytes
+	} else {
+		// PEM decoding failed, it might just be raw ASN.1 data
+		pkcsBytes = &rawPub
+	}
+	pkcs7, err := pkcs7.Unmarshal(*pkcsBytes)
+	if err != nil {
+		return nil, err
+	}
+	certs, err := pkcs7.Content.Certificates.Parse()
+	if err != nil {
+		return nil, err
+	}
+	for _, cert := range certs {
+		return &PublicKey{key: cert.PublicKey, certs: certs, rawCert: cert.Raw}, nil
+	}
+	return nil, errors.New("unable to extract public key from certificate inside PKCS7 bundle")
+}
+
+// CanonicalValue implements the pki.PublicKey interface
+func (k PublicKey) CanonicalValue() ([]byte, error) {
+	if k.rawCert == nil {
+		return nil, fmt.Errorf("PKCS7 public key has not been initialized")
+	}
+	//TODO: should we export the entire cert chain, not just the first one?
+	p := pem.Block{
+		Type:  "CERTIFICATE",
+		Bytes: k.rawCert,
+	}
+
+	var buf bytes.Buffer
+	if err := pem.Encode(&buf, &p); err != nil {
+		return nil, err
+	}
+	return buf.Bytes(), nil
+}
diff --git a/pkg/pki/pkcs7/pkcs7_test.go b/pkg/pki/pkcs7/pkcs7_test.go
new file mode 100644
index 0000000..21386d7
--- /dev/null
+++ b/pkg/pki/pkcs7/pkcs7_test.go
@@ -0,0 +1,247 @@
+/*
+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 pkcs7
+
+import (
+	"bytes"
+	"encoding/base64"
+	"strings"
+	"testing"
+)
+
+const pkcsECDSAPEM = `-----BEGIN PKCS7-----
+MIIW9QYJKoZIhvcNAQcCoIIW5jCCFuICAQExDzANBglghkgBZQMEAgEFADCCBAwG
+CSqGSIb3DQEHAaCCA/0EggP5U2lnbmF0dXJlLVZlcnNpb246IDEuMA0KQ3JlYXRl
+ZC1CeTogMTUgKEFkb3B0T3BlbkpESykNClNIQS0yNTYtRGlnZXN0LU1hbmlmZXN0
+OiB6QzV4S3JxM1pIZS90UnNMMTR6bittM1lReWVaZFltbmxuNWJNdlJaZW5JPQ0K
+U0hBLTI1Ni1EaWdlc3QtTWFuaWZlc3QtTWFpbi1BdHRyaWJ1dGVzOiBBZW00ckh4
+eTYycmx6QzJVU0NVbDcwSEFmYmV2NzhXDQogUkNhUWNKcXEwTE5nPQ0KDQpOYW1l
+OiBzaWdzdG9yZS9wbHVnaW4vU2lnbi5jbGFzcw0KU0hBLTI1Ni1EaWdlc3Q6IEZH
+UVZGbDlROEQ1ZTAzRE1RaGN2aTNtK0orZCtUc3A3TmFxKzBUUXpoSW89DQoNCk5h
+bWU6IE1FVEEtSU5GL21hdmVuL2Rldi5zaWdzdG9yZS9zaWdzdG9yZS1tYXZlbi1w
+bHVnaW4vcG9tLnhtbA0KU0hBLTI1Ni1EaWdlc3Q6IFlWRUFpeXZRMDZOVHRkRFRq
+cVJPYUZZbnQzcDY0QzFFa2NBbWlLNkpOcGM9DQoNCk5hbWU6IE1FVEEtSU5GL21h
+dmVuL2Rldi5zaWdzdG9yZS9zaWdzdG9yZS1tYXZlbi1wbHVnaW4vcG9tLnByb3Bl
+cnRpZXMNClNIQS0yNTYtRGlnZXN0OiA3aU1VWlpLeVI3cjdLelR1K2M2dVlsSWJ5
+c0VuZE1wMVBacUVXR2pHU2lNPQ0KDQpOYW1lOiBNRVRBLUlORi9tYXZlbi9kZXYu
+c2lnc3RvcmUvc2lnc3RvcmUtbWF2ZW4tcGx1Z2luL3BsdWdpbi1oZWxwLnhtbA0K
+U0hBLTI1Ni1EaWdlc3Q6IG4yM1N4ZmlDcU43WW9FSnd5S0k3NUE3N3crRHREUmIr
+dFI0bVl6SnZlWnc9DQoNCk5hbWU6IE1FVEEtSU5GL21hdmVuL3BsdWdpbi54bWwN
+ClNIQS0yNTYtRGlnZXN0OiBTRktBeGVwMlErSzJNVmZVeUV2U1FvMFRBNDhDSitu
+QXNxbmhzRWRJOUVFPQ0KDQpOYW1lOiBzaWdzdG9yZS9wbHVnaW4vU2lnbiQxLmNs
+YXNzDQpTSEEtMjU2LURpZ2VzdDogNlEvQVExZW9QNE9hQVJwbnVSRklRb0tZUC9S
+bmJ0TGxqOGJhUEg3TkdMZz0NCg0KTmFtZTogc2lnc3RvcmUvcGx1Z2luL0hlbHBN
+b2pvLmNsYXNzDQpTSEEtMjU2LURpZ2VzdDogU3ZPNkhibVlBSzBMVEhyVCtYbmRB
+OExJdUptZU5ub1dyYmVHS3dvTE9Pdz0NCg0KoIIEsjCCAfgwggF+oAMCAQICEzVZ
+A2aAoqHzw7Mu3X5uoHi27ocwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3Rv
+cmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTAzMDcwMzIwMjlaFw0zMTAy
+MjMwMzIwMjlaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2ln
+c3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAS0sgOyIuZPqTTvGRFmNMpXplg6
+MDpDWt5C/hmROWeRlnoS/fwPW0TQN0W67GeYtCXGrLWkS+0qeX6f4w+XcanP1HU1
+Z5b0temp/tmH7MHv0Li6JUVAq3DhNvtogOfrc3ejZjBkMA4GA1UdDwEB/wQEAwIB
+BjASBgNVHRMBAf8ECDAGAQH/AgEBMB0GA1UdDgQWBBTIxR0AQZokKTJRJOsNrkrt
+SgbT7DAfBgNVHSMEGDAWgBTIxR0AQZokKTJRJOsNrkrtSgbT7DAKBggqhkjOPQQD
+AwNoADBlAjB/JYliXzLour11wYYw4GODMLJZjf0ycVXv/N1qxaJsJjX9OestV+PB
+fXOJt2t6M1wCMQCo0Wsuf2o/47CihiJJkGrYyPLrqR6//gsRb2iVpqWKjZCwxkVP
+vaK84eYSNka3LmkwggKyMIICN6ADAgECAhQA0hq1XjiwESgeFBdACLc/8dxN/jAK
+BggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNp
+Z3N0b3JlMB4XDTIxMDQwNTE3MzA0MVoXDTIxMDQwNTE3NTAzM1owPDEcMBoGA1UE
+CgwTYmNhbGxhd2FAcmVkaGF0LmNvbTEcMBoGA1UEAwwTYmNhbGxhd2FAcmVkaGF0
+LmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOQ44IV3v9zK5zLUoPpqt4Wy
+snDT+OkgZQmPLq6PtNbqXOJnGtdi1crznvmlytJ1rsrNtobtG92Y3XtMSx+2fo6j
+ggEnMIIBIzAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwDAYD
+VR0TAQH/BAIwADAdBgNVHQ4EFgQU89SEXFUXE+hEwlqafHp6CayzjJcwHwYDVR0j
+BBgwFoAUyMUdAEGaJCkyUSTrDa5K7UoG0+wwgY0GCCsGAQUFBwEBBIGAMH4wfAYI
+KwYBBQUHMAKGcGh0dHA6Ly9wcml2YXRlY2EtY29udGVudC02MDNmZTdlNy0wMDAw
+LTIyMjctYmY3NS1mNGY1ZTgwZDI5NTQuc3RvcmFnZS5nb29nbGVhcGlzLmNvbS9j
+YTM2YTFlOTYyNDJiOWZjYjE0Ni9jYS5jcnQwHgYDVR0RBBcwFYETYmNhbGxhd2FA
+cmVkaGF0LmNvbTAKBggqhkjOPQQDAwNpADBmAjEAy3AOFlXTN7pMUyLyzsLk8tn8
+v782Bo5hGSGYJMZn8eRHGktDSlx4bj51Gu+V1c4kAjEA9ISrLl83ZU6j1yP0emR1
+FgAoHceF5dtx4KzSAi4B0Cghz7kBabfljWjCMy36Ce6rMYIOBDCCDgACAQEwQjAq
+MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlAhQA0hq1
+XjiwESgeFBdACLc/8dxN/jANBglghkgBZQMEAgEFADALBgcqhkjOPQIBBQAERzBF
+AiAttO+bYBcMnsMBQlkTdXII2f8CREQVkl9ehakvihSjBgIhAKYic4Ycq3qYLoV9
+4GZWm0NT0EFbzRG5BJaoEZgUL/lyoYINUDCCDUwGCyqGSIb3DQEJEAIOMYINOzCC
+DTcGCSqGSIb3DQEHAqCCDSgwgg0kAgEDMQ8wDQYJYIZIAWUDBAIBBQAwgYEGCyqG
+SIb3DQEJEAEEoHIEcDBuAgEBBglghkgBhv1sBwEwMTANBglghkgBZQMEAgEFAAQg
+DpZGPZm1vvrjhj/lQAoCYWm+9GCixa/ySbShy9tPdjQCEBOsApMET86YdBu0FUV2
+558YDzIwMjEwNDA1MTczMDQxWgIIMX79aENwnPqgggo3MIIE/jCCA+agAwIBAgIQ
+DUJK4L46iP9gQCHOFADw3TANBgkqhkiG9w0BAQsFADByMQswCQYDVQQGEwJVUzEV
+MBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29t
+MTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQgVGltZXN0YW1waW5n
+IENBMB4XDTIxMDEwMTAwMDAwMFoXDTMxMDEwNjAwMDAwMFowSDELMAkGA1UEBhMC
+VVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMSAwHgYDVQQDExdEaWdpQ2VydCBU
+aW1lc3RhbXAgMjAyMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMLm
+YYRnxYr1DQikRcpja1HXOhFCvQp1dU2UtAxQtSYQ/h3Ib5FrDJbnGlxI70Tlv5th
+zRWRYlq4/2cLnGP9NmqB+in43Stwhd4CGPN4bbx9+cdtCT2+anaH6Yq9+IRdHnbJ
+5MZ2djpT0dHTWjaPxqPhLxs6t2HWc+xObTOKfF1FLUuxUOZBOjdWhtyTI433UCXo
+ZObd048vV7WHIOsOjizVI9r0TXhG4wODMSlKXAwxikqMiMX3MFr5FK8VX2xDSQn9
+JiNT9o1j6BqrW7EdMMKbaYK02/xWVLwfoYervnpbCiAvSwnJlaeNsvrWY4tOpXIc
+7p96AXP4Gdb+DUmEvQECAwEAAaOCAbgwggG0MA4GA1UdDwEB/wQEAwIHgDAMBgNV
+HRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMEEGA1UdIAQ6MDgwNgYJ
+YIZIAYb9bAcBMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29t
+L0NQUzAfBgNVHSMEGDAWgBT0tuEgHf4prtLkYaWyoiWyyBc1bjAdBgNVHQ4EFgQU
+NkSGjqS6sGa+vCgtHUQ23eNqerwwcQYDVR0fBGowaDAyoDCgLoYsaHR0cDovL2Ny
+bDMuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC10cy5jcmwwMqAwoC6GLGh0dHA6
+Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9zaGEyLWFzc3VyZWQtdHMuY3JsMIGFBggrBgEF
+BQcBAQR5MHcwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBP
+BggrBgEFBQcwAoZDaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0
+U0hBMkFzc3VyZWRJRFRpbWVzdGFtcGluZ0NBLmNydDANBgkqhkiG9w0BAQsFAAOC
+AQEASBzctemaI7znGucgDo5nRv1CclF0CiNHo6uS0iXEcFm+FKDlJ4GlTRQVGQd5
+8NEEw4bZO73+RAJmTe1ppA/2uHDPYuj1UUp4eTZ6J7fz51Kfk6ftQ55757TdQSKJ
++4eiRgNO/PT+t2R3Y18jUmmDgvoaU+2QzI2hF3MN9PNlOXBL85zWenvaDLw9MtAb
+y/Vh/HUIAHa8gQ74wOFcz8QRcucbZEnYIpp1FUL1LTI4gdr0YKK6tFL7XOBhJCVP
+st/JKahzQ1HavWPWH1ub9y4bTxMd90oNcX6Xt/Q/hOvB46NJofrOp79Wz7pZdmGJ
+X36ntI5nePk2mOHLKNpbh6aKLzCCBTEwggQZoAMCAQICEAqhJdbWMht+QeQF2jaX
+whUwDQYJKoZIhvcNAQELBQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lD
+ZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGln
+aUNlcnQgQXNzdXJlZCBJRCBSb290IENBMB4XDTE2MDEwNzEyMDAwMFoXDTMxMDEw
+NzEyMDAwMFowcjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ
+MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hB
+MiBBc3N1cmVkIElEIFRpbWVzdGFtcGluZyBDQTCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBAL3QMu5LzY9/3am6gpnFOVQoV7YjSsQOB0UzURB90Pl9TWh+
+57ag9I2ziOSXv2MhkJi/E7xX08PhfgjWahQAOPcuHjvuzKb2Mln+X2U/4Jvr40ZH
+BhpVfgsnfsCi9aDg3iI/Dv9+lfvzo7oiPhisEeTwmQNtO4V8CdPuXciaC1TjqAlx
+a+DPIhAPdc9xck4Krd9AOly3UeGheRTGTSQjMF287DxgaqwvB8z98OpH2YhQXv1m
+blZhJymJhFHmgudGUP2UKiyn5HU+upgPhH+fMRTWrdXyZMt7HgXQhBlyF/EXBu89
+zdZN7wZC/aJTKk+FHcQdPK/P2qwQ9d2srOlW/5MCAwEAAaOCAc4wggHKMB0GA1Ud
+DgQWBBT0tuEgHf4prtLkYaWyoiWyyBc1bjAfBgNVHSMEGDAWgBRF66Kv9JLLgjEt
+UYunpyGd823IDzASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjAT
+BgNVHSUEDDAKBggrBgEFBQcDCDB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGG
+GGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2Nh
+Y2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDCB
+gQYDVR0fBHoweDA6oDigNoY0aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lD
+ZXJ0QXNzdXJlZElEUm9vdENBLmNybDA6oDigNoY0aHR0cDovL2NybDMuZGlnaWNl
+cnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDBQBgNVHSAESTBHMDgG
+CmCGSAGG/WwAAgQwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQu
+Y29tL0NQUzALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggEBAHGVEulRh1Zp
+ze/d2nyqY3qzeM8GN0CE70uEv8rPAwL9xafDDiBCLK938ysfDCFaKrcFNB1qrpn4
+J6JmvwmqYN92pDqTD/iy0dh8GWLoXoIlHsS6HHssIeLWWywUNUMEaLLbdQLgcseY
+1jxk5R9IEBhfiThhTWJGJIdjjJFSLK8pieV4H9YLFKWA1xJHcLN11ZOFk362kmf7
+U2GJqPVrlsD0WGkNfMgBsbkodbeZY4UijGHKeZR+WfyMD+NvtQEmtmyl7odRIeRY
+YJu6DC0rbaLEfrvEJStHAgh8Sa4TtuF8QkIoxhhWz0E0tmZdtnR79VYzIi8iNrJL
+okqV2PWmjlIxggJNMIICSQIBATCBhjByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMM
+RGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQD
+EyhEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQgVGltZXN0YW1waW5nIENBAhANQkrg
+vjqI/2BAIc4UAPDdMA0GCWCGSAFlAwQCAQUAoIGYMBoGCSqGSIb3DQEJAzENBgsq
+hkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjEwNDA1MTczMDQxWjArBgsqhkiG
+9w0BCRACDDEcMBowGDAWBBTh14Ko4ZG+72vKFpG1qrSUpiSb8zAvBgkqhkiG9w0B
+CQQxIgQggGa+Xld/PXUrauHVUANZD8tGkn6b2ioPrjbYztVzPikwDQYJKoZIhvcN
+AQEBBQAEggEAj7sr/Yqkwqrm21IhIHPLXDDDhxBPfcv0DJhFsAOR77wlDzV52yg6
+JrexTMuLWgPulVN0UyMoCISqMv22R9ELZGGxPjDYBu0jURFKZEryVEOoidA8U07x
+TBSkcGB6Vf4P6mNxzl2whkIg4bgob8ynD8O6eb7aF6sTXFN6GyZHtYhMlMuJw3Tt
+zNwtTy9bCZI4T4IlKscOhJ4hnVz0PO4mi/7C6Y/fLz/KoNXJR1q8LBTlHd5fNN5S
+NCy1JqXRQ/EFawlOicDB5IFL7TFpPTPEXsyTg1x5j1o0tAKErU3FJg30wiblro49
+oNLw5vSDnA3bG/vDsgshFr03RYcLPUVAtA==
+-----END PKCS7-----`
+
+const signedContent = `U2lnbmF0dXJlLVZlcnNpb246IDEuMA0KQ3JlYXRlZC1CeTogMTUgKEFkb3B0T3Bl
+bkpESykNClNIQS0yNTYtRGlnZXN0LU1hbmlmZXN0OiB6QzV4S3JxM1pIZS90UnNM
+MTR6bittM1lReWVaZFltbmxuNWJNdlJaZW5JPQ0KU0hBLTI1Ni1EaWdlc3QtTWFu
+aWZlc3QtTWFpbi1BdHRyaWJ1dGVzOiBBZW00ckh4eTYycmx6QzJVU0NVbDcwSEFm
+YmV2NzhXDQogUkNhUWNKcXEwTE5nPQ0KDQpOYW1lOiBzaWdzdG9yZS9wbHVnaW4v
+U2lnbi5jbGFzcw0KU0hBLTI1Ni1EaWdlc3Q6IEZHUVZGbDlROEQ1ZTAzRE1RaGN2
+aTNtK0orZCtUc3A3TmFxKzBUUXpoSW89DQoNCk5hbWU6IE1FVEEtSU5GL21hdmVu
+L2Rldi5zaWdzdG9yZS9zaWdzdG9yZS1tYXZlbi1wbHVnaW4vcG9tLnhtbA0KU0hB
+LTI1Ni1EaWdlc3Q6IFlWRUFpeXZRMDZOVHRkRFRqcVJPYUZZbnQzcDY0QzFFa2NB
+bWlLNkpOcGM9DQoNCk5hbWU6IE1FVEEtSU5GL21hdmVuL2Rldi5zaWdzdG9yZS9z
+aWdzdG9yZS1tYXZlbi1wbHVnaW4vcG9tLnByb3BlcnRpZXMNClNIQS0yNTYtRGln
+ZXN0OiA3aU1VWlpLeVI3cjdLelR1K2M2dVlsSWJ5c0VuZE1wMVBacUVXR2pHU2lN
+PQ0KDQpOYW1lOiBNRVRBLUlORi9tYXZlbi9kZXYuc2lnc3RvcmUvc2lnc3RvcmUt
+bWF2ZW4tcGx1Z2luL3BsdWdpbi1oZWxwLnhtbA0KU0hBLTI1Ni1EaWdlc3Q6IG4y
+M1N4ZmlDcU43WW9FSnd5S0k3NUE3N3crRHREUmIrdFI0bVl6SnZlWnc9DQoNCk5h
+bWU6IE1FVEEtSU5GL21hdmVuL3BsdWdpbi54bWwNClNIQS0yNTYtRGlnZXN0OiBT
+RktBeGVwMlErSzJNVmZVeUV2U1FvMFRBNDhDSituQXNxbmhzRWRJOUVFPQ0KDQpO
+YW1lOiBzaWdzdG9yZS9wbHVnaW4vU2lnbiQxLmNsYXNzDQpTSEEtMjU2LURpZ2Vz
+dDogNlEvQVExZW9QNE9hQVJwbnVSRklRb0tZUC9SbmJ0TGxqOGJhUEg3TkdMZz0N
+Cg0KTmFtZTogc2lnc3RvcmUvcGx1Z2luL0hlbHBNb2pvLmNsYXNzDQpTSEEtMjU2
+LURpZ2VzdDogU3ZPNkhibVlBSzBMVEhyVCtYbmRBOExJdUptZU5ub1dyYmVHS3dv
+TE9Pdz0NCg0K`
+
+func TestSignature_Verify(t *testing.T) {
+	tests := []struct {
+		name  string
+		pkcs7 string
+	}{
+		{
+			name:  "ec",
+			pkcs7: pkcsECDSAPEM,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			s, err := NewSignature(strings.NewReader(tt.pkcs7))
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			pub, err := NewPublicKey(strings.NewReader(tt.pkcs7))
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			data, _ := base64.StdEncoding.DecodeString(signedContent)
+			if err := s.Verify(bytes.NewReader(data), pub); err != nil {
+				t.Fatalf("Signature.Verify() error = %v", err)
+			}
+
+			// Now try with the canonical value (this is a detached signature)
+			cb, err := s.CanonicalValue()
+			if err != nil {
+				t.Fatal(err)
+			}
+			canonicalSig, err := NewSignature(bytes.NewReader(cb))
+			if err != nil {
+				t.Fatal(err)
+			}
+			if err := canonicalSig.Verify(bytes.NewReader(data), pub); err != nil {
+				t.Fatalf("CanonicalSignature.Verify() error = %v", err)
+			}
+		})
+	}
+}
+
+func TestSignature_VerifyFail(t *testing.T) {
+	tests := []struct {
+		name  string
+		pkcs7 string
+	}{
+		{
+			name:  "ec",
+			pkcs7: pkcsECDSAPEM,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// Make some fake data, and tamper with the signature
+			s, err := NewSignature(strings.NewReader(tt.pkcs7))
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			pub, err := NewPublicKey(strings.NewReader(tt.pkcs7))
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			data := []byte("something that shouldn't verify")
+			if err := s.Verify(bytes.NewReader(data), pub); err == nil {
+				t.Error("Signature.Verify() expected error!")
+			}
+		})
+	}
+}
diff --git a/pkg/pki/pki.go b/pkg/pki/pki.go
index c53116d..43fb9d7 100644
--- a/pkg/pki/pki.go
+++ b/pkg/pki/pki.go
@@ -22,6 +22,7 @@ import (
 
 	"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"
 )
@@ -57,6 +58,8 @@ func (a ArtifactFactory) NewPublicKey(r io.Reader) (PublicKey, error) {
 		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)
 }
@@ -71,6 +74,8 @@ func (a ArtifactFactory) NewSignature(r io.Reader) (Signature, error) {
 		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/types/jar/jar.go b/pkg/types/jar/jar.go
new file mode 100644
index 0000000..0151a3c
--- /dev/null
+++ b/pkg/types/jar/jar.go
@@ -0,0 +1,66 @@
+/*
+Copyright © 2021 The Rekor Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package jar
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/sigstore/rekor/pkg/types"
+	"github.com/sigstore/rekor/pkg/util"
+
+	"github.com/go-openapi/swag"
+	"github.com/sigstore/rekor/pkg/generated/models"
+)
+
+const (
+	KIND = "jar"
+)
+
+type BaseJARType struct{}
+
+func (jt BaseJARType) Kind() string {
+	return KIND
+}
+
+func init() {
+	types.TypeMap.Set(KIND, New)
+}
+
+func New() types.TypeImpl {
+	return &BaseJARType{}
+}
+
+var SemVerToFacFnMap = &util.VersionFactoryMap{VersionFactories: make(map[string]util.VersionFactory)}
+
+func (jt BaseJARType) UnmarshalEntry(pe models.ProposedEntry) (types.EntryImpl, error) {
+	jar, ok := pe.(*models.Jar)
+	if !ok {
+		return nil, errors.New("cannot unmarshal non-JAR types")
+	}
+
+	if genFn, found := SemVerToFacFnMap.Get(swag.StringValue(jar.APIVersion)); found {
+		entry := genFn()
+		if entry == nil {
+			return nil, fmt.Errorf("failure generating JAR object for version '%v'", jar.APIVersion)
+		}
+		if err := entry.Unmarshal(jar); err != nil {
+			return nil, err
+		}
+		return entry, nil
+	}
+	return nil, fmt.Errorf("JARType implementation for version '%v' not found", swag.StringValue(jar.APIVersion))
+}
diff --git a/pkg/types/jar/jar_schema.json b/pkg/types/jar/jar_schema.json
new file mode 100644
index 0000000..e8b237d
--- /dev/null
+++ b/pkg/types/jar/jar_schema.json
@@ -0,0 +1,12 @@
+{
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "$id": "http://rekor.sigstore.dev/types/jar/jar_schema.json",
+    "title": "JAR Schema",
+    "description": "Schema for JAR objects",
+    "type": "object",
+    "oneOf": [
+        {
+            "$ref": "v0.0.1/jar_v0_0_1_schema.json"
+        }
+    ]
+}
diff --git a/pkg/types/jar/jar_test.go b/pkg/types/jar/jar_test.go
new file mode 100644
index 0000000..fadd5f7
--- /dev/null
+++ b/pkg/types/jar/jar_test.go
@@ -0,0 +1,124 @@
+/*
+Copyright © 2021 The Rekor Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package jar
+
+import (
+	"context"
+	"errors"
+	"testing"
+
+	"github.com/go-openapi/swag"
+	"github.com/sigstore/rekor/pkg/generated/models"
+	"github.com/sigstore/rekor/pkg/types"
+)
+
+type UnmarshalTester struct {
+	models.Jar
+}
+
+func (u UnmarshalTester) NewEntry() types.EntryImpl {
+	return &UnmarshalTester{}
+}
+
+func (u UnmarshalTester) APIVersion() string {
+	return "2.0.1"
+}
+
+func (u UnmarshalTester) IndexKeys() []string {
+	return []string{}
+}
+
+func (u UnmarshalTester) Canonicalize(ctx context.Context) ([]byte, error) {
+	return nil, nil
+}
+
+func (u UnmarshalTester) HasExternalEntities() bool {
+	return false
+}
+
+func (u *UnmarshalTester) FetchExternalEntities(ctx context.Context) error {
+	return nil
+}
+
+func (u UnmarshalTester) Unmarshal(pe models.ProposedEntry) error {
+	return nil
+}
+
+func (u UnmarshalTester) Validate() error {
+	return nil
+}
+
+type UnmarshalFailsTester struct {
+	UnmarshalTester
+}
+
+func (u UnmarshalFailsTester) NewEntry() types.EntryImpl {
+	return &UnmarshalFailsTester{}
+}
+
+func (u UnmarshalFailsTester) Unmarshal(pe models.ProposedEntry) error {
+	return errors.New("error")
+}
+
+func TestJARType(t *testing.T) {
+	// empty to start
+	if len(SemVerToFacFnMap.VersionFactories) != 0 {
+		t.Error("semver range was not blank at start of test")
+	}
+
+	u := UnmarshalTester{}
+	// ensure semver range parser is working
+	invalidSemVerRange := "not a valid semver range"
+	SemVerToFacFnMap.Set(invalidSemVerRange, u.NewEntry)
+	if len(SemVerToFacFnMap.VersionFactories) > 0 {
+		t.Error("invalid semver range was incorrectly added to SemVerToFacFnMap")
+	}
+
+	// valid semver range can be parsed
+	SemVerToFacFnMap.Set(">= 1.2.3", u.NewEntry)
+	if len(SemVerToFacFnMap.VersionFactories) != 1 {
+		t.Error("valid semver range was not added to SemVerToFacFnMap")
+	}
+
+	u.Jar.APIVersion = swag.String("2.0.1")
+	brt := BaseJARType{}
+
+	// version requested matches implementation in map
+	if _, err := brt.UnmarshalEntry(&u.Jar); err != nil {
+		t.Errorf("unexpected error in Unmarshal: %v", err)
+	}
+
+	// version requested fails to match implementation in map
+	u.Jar.APIVersion = swag.String("1.2.2")
+	if _, err := brt.UnmarshalEntry(&u.Jar); err == nil {
+		t.Error("unexpected success in Unmarshal for non-matching version")
+	}
+
+	// error in Unmarshal call is raised appropriately
+	u.Jar.APIVersion = swag.String("2.2.0")
+	u2 := UnmarshalFailsTester{}
+	SemVerToFacFnMap.Set(">= 1.2.3", u2.NewEntry)
+	if _, err := brt.UnmarshalEntry(&u.Jar); err == nil {
+		t.Error("unexpected success in Unmarshal when error is thrown")
+	}
+
+	// version requested fails to match implementation in map
+	u.Jar.APIVersion = swag.String("not_a_version")
+	if _, err := brt.UnmarshalEntry(&u.Jar); err == nil {
+		t.Error("unexpected success in Unmarshal for invalid version")
+	}
+}
diff --git a/pkg/types/jar/v0.0.1/entry.go b/pkg/types/jar/v0.0.1/entry.go
new file mode 100644
index 0000000..3100846
--- /dev/null
+++ b/pkg/types/jar/v0.0.1/entry.go
@@ -0,0 +1,341 @@
+/*
+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 jar
+
+import (
+	"archive/zip"
+	"bytes"
+	"context"
+	"crypto/sha256"
+	"encoding/base64"
+	"encoding/hex"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"path"
+	"reflect"
+	"strings"
+
+	"github.com/sigstore/rekor/pkg/log"
+	"github.com/sigstore/rekor/pkg/pki"
+	"github.com/sigstore/rekor/pkg/types"
+	"github.com/sigstore/rekor/pkg/types/jar"
+	"github.com/sigstore/rekor/pkg/util"
+
+	"github.com/asaskevich/govalidator"
+
+	"github.com/go-openapi/strfmt"
+
+	"github.com/go-openapi/swag"
+	"github.com/mitchellh/mapstructure"
+	jarutils "github.com/sassoftware/relic/lib/signjar"
+	"github.com/sigstore/rekor/pkg/generated/models"
+)
+
+const (
+	APIVERSION = "0.0.1"
+)
+
+func init() {
+	jar.SemVerToFacFnMap.Set(APIVERSION, NewEntry)
+}
+
+type V001Entry struct {
+	JARModel                models.JarV001Schema
+	fetchedExternalEntities bool
+	jarObj                  *jarutils.JarSignature
+	keyObj                  pki.PublicKey
+	sigObj                  pki.Signature
+}
+
+func (v V001Entry) APIVersion() string {
+	return APIVERSION
+}
+
+func NewEntry() types.EntryImpl {
+	return &V001Entry{}
+}
+
+func Base64StringtoByteArray() mapstructure.DecodeHookFunc {
+	return func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
+		if f.Kind() != reflect.String || t.Kind() != reflect.Slice {
+			return data, nil
+		}
+
+		bytes, err := base64.StdEncoding.DecodeString(data.(string))
+		if err != nil {
+			return []byte{}, fmt.Errorf("failed parsing base64 data: %v", err)
+		}
+		return bytes, nil
+	}
+}
+
+func (v V001Entry) IndexKeys() []string {
+	var result []string
+
+	if v.HasExternalEntities() {
+		if err := v.FetchExternalEntities(context.Background()); err != nil {
+			log.Logger.Error(err)
+			return result
+		}
+	}
+
+	key, err := v.keyObj.CanonicalValue()
+	if err != nil {
+		log.Logger.Error(err)
+	} else {
+		hasher := sha256.New()
+		if _, err := hasher.Write(key); err != nil {
+			log.Logger.Error(err)
+		} else {
+			result = append(result, strings.ToLower(hex.EncodeToString(hasher.Sum(nil))))
+		}
+	}
+
+	if v.JARModel.Archive.Hash != nil {
+		result = append(result, strings.ToLower(swag.StringValue(v.JARModel.Archive.Hash.Value)))
+	}
+
+	return result
+}
+
+func (v *V001Entry) Unmarshal(pe models.ProposedEntry) error {
+	jar, ok := pe.(*models.Jar)
+	if !ok {
+		return errors.New("cannot unmarshal non JAR v0.0.1 type")
+	}
+
+	cfg := mapstructure.DecoderConfig{
+		DecodeHook: Base64StringtoByteArray(),
+		Result:     &v.JARModel,
+	}
+
+	dec, err := mapstructure.NewDecoder(&cfg)
+	if err != nil {
+		return fmt.Errorf("error initializing decoder: %w", err)
+	}
+
+	if err := dec.Decode(jar.Spec); err != nil {
+		return err
+	}
+	// field validation
+	if err := v.JARModel.Validate(strfmt.Default); err != nil {
+		return err
+	}
+	return nil
+
+}
+
+func (v V001Entry) HasExternalEntities() bool {
+	if v.fetchedExternalEntities {
+		return false
+	}
+
+	if v.JARModel.Archive != nil && v.JARModel.Archive.URL.String() != "" {
+		return true
+	}
+	return false
+}
+
+func (v *V001Entry) FetchExternalEntities(ctx context.Context) error {
+	if v.fetchedExternalEntities {
+		return nil
+	}
+
+	if err := v.Validate(); err != nil {
+		return err
+	}
+
+	oldSHA := ""
+	if v.JARModel.Archive.Hash != nil && v.JARModel.Archive.Hash.Value != nil {
+		oldSHA = swag.StringValue(v.JARModel.Archive.Hash.Value)
+	}
+
+	dataReadCloser, err := util.FileOrURLReadCloser(ctx, v.JARModel.Archive.URL.String(), v.JARModel.Archive.Content)
+	if err != nil {
+		return err
+	}
+	defer dataReadCloser.Close()
+
+	hasher := sha256.New()
+	b := &bytes.Buffer{}
+
+	n, err := io.Copy(io.MultiWriter(hasher, b), dataReadCloser)
+	if err != nil {
+		return err
+	}
+
+	computedSHA := hex.EncodeToString(hasher.Sum(nil))
+	if oldSHA != "" && computedSHA != oldSHA {
+		return fmt.Errorf("SHA mismatch: %s != %s", computedSHA, oldSHA)
+	}
+
+	zipReader, err := zip.NewReader(bytes.NewReader(b.Bytes()), n)
+	if err != nil {
+		return err
+	}
+
+	// this ensures that the JAR is signed and the signature verifies, as
+	// well as checks that the hashes in the signed manifest are all valid
+	jarObj, err := jarutils.Verify(zipReader, false)
+	if err != nil {
+		return err
+	}
+	switch len(jarObj) {
+	case 0:
+		return errors.New("no signatures detected in JAR archive")
+	case 1:
+	default:
+		return errors.New("multiple signatures detected in JAR; unable to process")
+	}
+	v.jarObj = jarObj[0]
+
+	af := pki.NewArtifactFactory("pkcs7")
+	// we need to find and extract the PKCS7 bundle from the JAR file manually
+	sigPKCS7, err := extractPKCS7SignatureFromJAR(zipReader)
+	if err != nil {
+		return err
+	}
+
+	v.keyObj, err = af.NewPublicKey(bytes.NewReader(sigPKCS7))
+	if err != nil {
+		return err
+	}
+
+	v.sigObj, err = af.NewSignature(bytes.NewReader(sigPKCS7))
+	if err != nil {
+		return err
+	}
+
+	// if we get here, all goroutines succeeded without error
+	if oldSHA == "" {
+		v.JARModel.Archive.Hash = &models.JarV001SchemaArchiveHash{}
+		v.JARModel.Archive.Hash.Algorithm = swag.String(models.JarV001SchemaArchiveHashAlgorithmSha256)
+		v.JARModel.Archive.Hash.Value = swag.String(computedSHA)
+	}
+
+	v.fetchedExternalEntities = true
+	return nil
+}
+
+func (v *V001Entry) Canonicalize(ctx context.Context) ([]byte, error) {
+	if err := v.FetchExternalEntities(ctx); err != nil {
+		return nil, err
+	}
+	if v.jarObj == nil {
+		return nil, errors.New("JAR object not initialized before canonicalization")
+	}
+	if v.keyObj == nil {
+		return nil, errors.New("public key not initialized before canonicalization")
+	}
+	if v.sigObj == nil {
+		return nil, errors.New("signature not initialized before canonicalization")
+	}
+
+	canonicalEntry := models.JarV001Schema{}
+	canonicalEntry.ExtraData = v.JARModel.ExtraData
+
+	var err error
+	// need to canonicalize key content
+	canonicalEntry.Signature = &models.JarV001SchemaSignature{}
+	canonicalEntry.Signature.PublicKey = &models.JarV001SchemaSignaturePublicKey{}
+	keyContent, err := v.keyObj.CanonicalValue()
+	if err != nil {
+		return nil, err
+	}
+	canonicalEntry.Signature.PublicKey.Content = (*strfmt.Base64)(&keyContent)
+	sigContent, err := v.sigObj.CanonicalValue()
+	if err != nil {
+		return nil, err
+	}
+	canonicalEntry.Signature.Content = (*strfmt.Base64)(&sigContent)
+
+	canonicalEntry.Archive = &models.JarV001SchemaArchive{}
+	canonicalEntry.Archive.Hash = &models.JarV001SchemaArchiveHash{}
+	canonicalEntry.Archive.Hash.Algorithm = v.JARModel.Archive.Hash.Algorithm
+	canonicalEntry.Archive.Hash.Value = v.JARModel.Archive.Hash.Value
+	// archive content is not set deliberately
+
+	// ExtraData is copied through unfiltered
+	canonicalEntry.ExtraData = v.JARModel.ExtraData
+
+	// wrap in valid object with kind and apiVersion set
+	jar := models.Jar{}
+	jar.APIVersion = swag.String(APIVERSION)
+	jar.Spec = &canonicalEntry
+
+	bytes, err := json.Marshal(&jar)
+	if err != nil {
+		return nil, err
+	}
+
+	return bytes, nil
+}
+
+// Validate performs cross-field validation for fields in object
+func (v V001Entry) Validate() error {
+	archive := v.JARModel.Archive
+	if archive == nil {
+		return errors.New("missing package")
+	}
+
+	if len(archive.Content) == 0 && archive.URL.String() == "" {
+		return errors.New("one of 'content' or 'url' must be specified for package")
+	}
+
+	hash := archive.Hash
+	if hash != nil {
+		if !govalidator.IsHash(swag.StringValue(hash.Value), swag.StringValue(hash.Algorithm)) {
+			return errors.New("invalid value for hash")
+		}
+	} else if archive.URL.String() != "" {
+		return errors.New("hash value must be provided if URL is specified")
+	}
+
+	return nil
+}
+
+// extractPKCS7SignatureFromJAR extracts the first signature file from the JAR and returns it
+func extractPKCS7SignatureFromJAR(inz *zip.Reader) ([]byte, error) {
+	for _, f := range inz.File {
+		dir, name := path.Split(strings.ToUpper(f.Name))
+		if dir != "META-INF/" || name == "" {
+			continue
+		}
+		i := strings.LastIndex(name, ".")
+		if i < 0 {
+			continue
+		}
+		fileExt := name[i:]
+		if fileExt == ".RSA" || fileExt == ".DSA" || fileExt == ".EC" || strings.HasPrefix(name, "SIG-") {
+			fileReader, err := f.Open()
+			if err != nil {
+				return nil, err
+			}
+			contents, err := ioutil.ReadAll(fileReader)
+			if err != nil {
+				return nil, err
+			}
+			if err = fileReader.Close(); err != nil {
+				return nil, err
+			}
+			return contents, nil
+		}
+	}
+	return nil, errors.New("unable to locate signature in JAR file")
+}
diff --git a/pkg/types/jar/v0.0.1/entry_test.go b/pkg/types/jar/v0.0.1/entry_test.go
new file mode 100644
index 0000000..029c849
--- /dev/null
+++ b/pkg/types/jar/v0.0.1/entry_test.go
@@ -0,0 +1,229 @@
+/*
+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 jar
+
+import (
+	"context"
+	"crypto/sha256"
+	"encoding/hex"
+	"errors"
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"reflect"
+	"testing"
+
+	"github.com/go-openapi/strfmt"
+	"github.com/go-openapi/swag"
+	"github.com/sigstore/rekor/pkg/generated/models"
+	"go.uber.org/goleak"
+)
+
+func TestMain(m *testing.M) {
+	goleak.VerifyTestMain(m)
+}
+
+func TestNewEntryReturnType(t *testing.T) {
+	entry := NewEntry()
+	if reflect.TypeOf(entry) != reflect.ValueOf(&V001Entry{}).Type() {
+		t.Errorf("invalid type returned from NewEntry: %T", entry)
+	}
+}
+
+func TestCrossFieldValidation(t *testing.T) {
+	type TestCase struct {
+		caseDesc                  string
+		entry                     V001Entry
+		hasExtEntities            bool
+		expectUnmarshalSuccess    bool
+		expectCanonicalizeSuccess bool
+	}
+
+	jarBytes, _ := ioutil.ReadFile("../../../../tests/test.jar")
+
+	h := sha256.New()
+	_, _ = h.Write(jarBytes)
+	dataSHA := hex.EncodeToString(h.Sum(nil))
+
+	testServer := httptest.NewServer(http.HandlerFunc(
+		func(w http.ResponseWriter, r *http.Request) {
+			var file *[]byte
+			var err error
+
+			switch r.URL.Path {
+			case "/data":
+				file = &jarBytes
+			default:
+				err = errors.New("unknown URL")
+			}
+			if err != nil {
+				w.WriteHeader(http.StatusNotFound)
+				return
+			}
+			w.WriteHeader(http.StatusOK)
+			_, _ = w.Write(*file)
+		}))
+	defer testServer.Close()
+
+	testCases := []TestCase{
+		{
+			caseDesc:               "empty obj",
+			entry:                  V001Entry{},
+			expectUnmarshalSuccess: false,
+		},
+		{
+			caseDesc: "empty archive",
+			entry: V001Entry{
+				JARModel: models.JarV001Schema{
+					Archive: &models.JarV001SchemaArchive{},
+				},
+			},
+			expectUnmarshalSuccess: false,
+		},
+		{
+			caseDesc: "archive with url but no hash",
+			entry: V001Entry{
+				JARModel: models.JarV001Schema{
+					Archive: &models.JarV001SchemaArchive{
+						URL: strfmt.URI(testServer.URL + "/data"),
+					},
+				},
+			},
+			hasExtEntities:         true,
+			expectUnmarshalSuccess: false,
+		},
+		{
+			caseDesc: "archive with url and empty hash",
+			entry: V001Entry{
+				JARModel: models.JarV001Schema{
+					Archive: &models.JarV001SchemaArchive{
+						Hash: &models.JarV001SchemaArchiveHash{},
+						URL:  strfmt.URI(testServer.URL + "/data"),
+					},
+				},
+			},
+			hasExtEntities:         true,
+			expectUnmarshalSuccess: false,
+		},
+		{
+			caseDesc: "archive with url and hash alg but missing value",
+			entry: V001Entry{
+				JARModel: models.JarV001Schema{
+					Archive: &models.JarV001SchemaArchive{
+						Hash: &models.JarV001SchemaArchiveHash{
+							Algorithm: swag.String(models.JarV001SchemaArchiveHashAlgorithmSha256),
+						},
+						URL: strfmt.URI(testServer.URL + "/data"),
+					},
+				},
+			},
+			hasExtEntities:         true,
+			expectUnmarshalSuccess: false,
+		},
+		{
+			caseDesc: "archive with valid url with matching hash",
+			entry: V001Entry{
+				JARModel: models.JarV001Schema{
+					Archive: &models.JarV001SchemaArchive{
+						Hash: &models.JarV001SchemaArchiveHash{
+							Algorithm: swag.String(models.JarV001SchemaArchiveHashAlgorithmSha256),
+							Value:     swag.String(dataSHA),
+						},
+						URL: strfmt.URI(testServer.URL + "/data"),
+					},
+				},
+			},
+			hasExtEntities:            true,
+			expectUnmarshalSuccess:    true,
+			expectCanonicalizeSuccess: true,
+		},
+		{
+			caseDesc: "archive with inline content",
+			entry: V001Entry{
+				JARModel: models.JarV001Schema{
+					Archive: &models.JarV001SchemaArchive{
+						Content: strfmt.Base64(jarBytes),
+					},
+				},
+			},
+			hasExtEntities:            false,
+			expectUnmarshalSuccess:    true,
+			expectCanonicalizeSuccess: true,
+		},
+		{
+			caseDesc: "archive with url and incorrect hash value",
+			entry: V001Entry{
+				JARModel: models.JarV001Schema{
+					Archive: &models.JarV001SchemaArchive{
+						Hash: &models.JarV001SchemaArchiveHash{
+							Algorithm: swag.String(models.JarV001SchemaArchiveHashAlgorithmSha256),
+							Value:     swag.String("3030303030303030303030303030303030303030303030303030303030303030"),
+						},
+						URL: strfmt.URI(testServer.URL + "/data"),
+					},
+				},
+			},
+			hasExtEntities:            true,
+			expectUnmarshalSuccess:    true,
+			expectCanonicalizeSuccess: false,
+		},
+		{
+			caseDesc: "valid obj with extradata",
+			entry: V001Entry{
+				JARModel: models.JarV001Schema{
+					Archive: &models.JarV001SchemaArchive{
+						Content: strfmt.Base64(jarBytes),
+					},
+					ExtraData: []byte("{\"something\": \"here\""),
+				},
+			},
+			hasExtEntities:            false,
+			expectUnmarshalSuccess:    true,
+			expectCanonicalizeSuccess: true,
+		},
+	}
+
+	for _, tc := range testCases {
+		if err := tc.entry.Validate(); (err == nil) != tc.expectUnmarshalSuccess {
+			t.Errorf("unexpected result in '%v': %v", tc.caseDesc, err)
+		}
+
+		v := &V001Entry{}
+		r := models.Jar{
+			APIVersion: swag.String(tc.entry.APIVersion()),
+			Spec:       tc.entry.JARModel,
+		}
+
+		unmarshalAndValidate := func() error {
+			if err := v.Unmarshal(&r); err != nil {
+				return err
+			}
+			return v.Validate()
+		}
+		if err := unmarshalAndValidate(); (err == nil) != tc.expectUnmarshalSuccess {
+			t.Errorf("unexpected result in '%v': %v", tc.caseDesc, err)
+		}
+
+		if tc.entry.HasExternalEntities() != tc.hasExtEntities {
+			t.Errorf("unexpected result from HasExternalEntities for '%v'", tc.caseDesc)
+		}
+
+		if _, err := tc.entry.Canonicalize(context.TODO()); (err == nil) != tc.expectCanonicalizeSuccess {
+			t.Errorf("unexpected result from Canonicalize for '%v': %v", tc.caseDesc, err)
+		}
+	}
+}
diff --git a/pkg/types/jar/v0.0.1/jar_v0_0_1_schema.json b/pkg/types/jar/v0.0.1/jar_v0_0_1_schema.json
new file mode 100644
index 0000000..f42376e
--- /dev/null
+++ b/pkg/types/jar/v0.0.1/jar_v0_0_1_schema.json
@@ -0,0 +1,79 @@
+{
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "$id": "http://rekor.sigstore.dev/types/jar/jar_v0_0_1_schema.json",
+    "title": "JAR v0.0.1 Schema",
+    "description": "Schema for JAR entries",
+    "type": "object",
+    "properties": {
+        "signature": {
+            "description": "Information about the included signature in the JAR file",
+            "type": "object",
+            "properties": {
+                "content": {
+                    "description": "Specifies the PKCS7 signature embedded within the JAR file ",
+                    "type": "string",
+                    "format": "byte"
+                },
+                "publicKey" : {
+                    "description": "The X509 certificate containing the public key JAR which verifies the signature of the JAR",
+                    "type": "object",
+                    "properties": {
+                        "content": {
+                            "description": "Specifies the content of the X509 certificate containing the public key used to verify the signature",
+                            "type": "string",
+                            "format": "byte"
+                        }
+                    },
+                    "required": [ "content" ]
+                }
+            },
+            "required": [ "publicKey", "content" ]
+        },
+        "archive": {
+            "description": "Information about the archive associated with the entry",
+            "type": "object",
+            "properties": {
+                "hash": {
+                    "description": "Specifies the hash algorithm and value encompassing the entire signed archive",
+                    "type": "object",
+                    "properties": {
+                        "algorithm": {
+                            "description": "The hashing function used to compute the hash value",
+                            "type": "string",
+                            "enum": [ "sha256" ]
+                        },
+                        "value": {
+                            "description": "The hash value for the archive",
+                            "type": "string"
+                        }
+                    },
+                    "required": [ "algorithm", "value" ]
+                },
+                "url": {
+                    "description": "Specifies the location of the archive; if this is specified, a hash value must also be provided",
+                    "type": "string",
+                    "format": "uri"
+                },
+                "content": {
+                    "description": "Specifies the archive inline within the document",
+                    "type": "string",
+                    "format": "byte"
+                }
+            },
+            "oneOf": [
+                {
+                    "required": [ "url" ]
+                },
+                {
+                    "required": [ "content" ]
+                }
+            ]
+        },
+        "extraData": {
+            "description": "Arbitrary content to be included in the verifiable entry in the transparency log", 
+            "type": "object",
+            "additionalProperties": true
+        }
+    },
+    "required": [ "archive" ]
+}
\ No newline at end of file
diff --git a/tests/e2e_test.go b/tests/e2e_test.go
index ee2c5f9..dbfe808 100644
--- a/tests/e2e_test.go
+++ b/tests/e2e_test.go
@@ -243,6 +243,20 @@ func TestSSH(t *testing.T) {
 	outputContains(t, out, uuid)
 }
 
+func TestJAR(t *testing.T) {
+	td := t.TempDir()
+	artifactPath := filepath.Join(td, "artifact.jar")
+
+	createSignedJar(t, artifactPath)
+
+	// If we do it twice, it should already exist
+	out := runCli(t, "upload", "--artifact", artifactPath, "--type", "jar")
+	outputContains(t, out, "Created entry at")
+	out = runCli(t, "upload", "--artifact", artifactPath, "--type", "jar")
+	outputContains(t, out, "Entry already exists")
+
+}
+
 func TestX509(t *testing.T) {
 	td := t.TempDir()
 	artifactPath := filepath.Join(td, "artifact")
diff --git a/tests/jar.go b/tests/jar.go
new file mode 100644
index 0000000..a1df776
--- /dev/null
+++ b/tests/jar.go
@@ -0,0 +1,86 @@
+// +build e2e
+
+/*
+Copyright © 2021 The Rekor Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package e2e
+
+import (
+	"archive/zip"
+	"bytes"
+	"context"
+	"crypto"
+	"os"
+	"testing"
+
+	"github.com/sassoftware/relic/lib/certloader"
+	"github.com/sassoftware/relic/lib/signjar"
+	"github.com/sassoftware/relic/lib/zipslicer"
+)
+
+//note: reuses PKI artifacts from x509 tests
+
+const manifest = `Manifest-Version: 1.0
+
+Name: src/some/java/HelloWorld.class
+SHA-256-Digest: cp40SgHlLIIr397GHijW7aAmWNLn0rgKm5Ap9B4hLd4=
+
+`
+
+func createSignedJar(t *testing.T, artifactPath string) {
+	t.Helper()
+
+	//create a ZIP file with a single file inside
+	f, err := os.Create(artifactPath)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	zw := zip.NewWriter(f)
+	jw, err := zw.Create("src/some/java/HelloWorld.class")
+	if err != nil {
+		t.Fatal(err)
+	}
+	jw.Write([]byte("HelloWorld!"))
+	mf, err := zw.Create("META-INF/MANIFEST.MF")
+	if err != nil {
+		t.Fatal(err)
+	}
+	mf.Write([]byte(manifest))
+	if err := zw.Close(); err != nil {
+		t.Fatal(err)
+	}
+	f.Sync()
+	buf := bytes.Buffer{}
+	zipslicer.ZipToTar(f, &buf)
+
+	jd, err := signjar.DigestJarStream(&buf, crypto.SHA256)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	c := certloader.Certificate{
+		PrivateKey: certPrivateKey,
+		Leaf:       cert,
+	}
+
+	patch, _, err := jd.Sign(context.Background(), &c, "rekor", false, true, false)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if err := patch.Apply(f, artifactPath); err != nil {
+		t.Fatal(err)
+	}
+}
diff --git a/tests/test.jar b/tests/test.jar
new file mode 100644
index 0000000000000000000000000000000000000000..83935ae040272be3ddfa604189b0cec301784165
GIT binary patch
literal 25668
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%0A<%=8rD*AfABB9a^Db9^h<B~ShHRbA
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
E7sy)Qb^rhX

literal 0
HcmV?d00001

-- 
GitLab