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 zt*UUu8+&yCsk5%D7B8@;7_sG}aUuR<S+z?|q|E2*aggIKrdLG~)9|@&!b$o2`=s}; z{-NDTFzX#003es<Klcxq{LcTK+lAwQ>>vKSY4?9Vv)bF){HwoMQQx-vXSVcv)hqDu zQ3n@~zBZNuRv8IpF%KwZNwxl~)i2&WEsVsH=xB}dz3W!YEA6A6ihEmNcXIDGwwrSk zE7o<p>R?<0fu2laH->y_ZJjV|1}HPwcG!1Qdp&i}Wyg%wL17j*oQAfCeDB(w%-ADH zK1!9y{*PTCkv%%h{O1@fhs$g-R#dkJv}S&8Zf^Eb`mof@4Ebqprc6sdx<Lk+(BUBI ze!P+YP}2OK?8K-d;+ki6BSbxl0DS}NwBPe;fWmef2<bb=>*kyXeZ}}s2o%TEVq5@v ze99kla^k~bkrHJ>`ZVVNAIK>r{ZsH#1+>Ls5imgtl!X_9zc1IPyC-e<y;~jS$&?`V zeuMc~o-RUK9Ve-K>B?rBY*3an(B(Tz<<VlfP_h1J$@<`14hb6z@yW=tKQra|i4Y_F zAz~Six;`_&n)RP2Ck@Ng3PLn(!-_yRs-}_64tR`Mpt1UxuM?j{1Kc@!ld|xm`iF1S zvKGSb0r#XyfpS)5N>YY+Nb4mkSLvF^ujw32r!p1#zQo2Kl^}o;5=i{d);)E?S=#!< zC`YClr5mm^70ip`*i5k2*^8#K7TyxI-!m{9SQOs+H-NvWu;P1#-Z@I5GP7i^gKMBq z+lzFdyA9Bkm-iMp{+(%E6I$R+ZjmTy(IN-_k>W_C&oYFb+nF4F<+Q(jJ}}|c$c$*B z_Ro9Lv^IQ�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 E7sy)Qb^rhX literal 0 HcmV?d00001 -- GitLab