diff --git a/.gitignore b/.gitignore index d4d6dee0fd1e80f6a2cfde6ee2ab1c82f47dc67a..2350421419f0ea7f2c9d18599a8ccde81e16df15 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 cfd8acaaaa4974b3606667da419fcf0e18188a77..f70113931613d12179577d8c0e5d5380ad5f6a7f 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 2c49d765e2d181be0394395e642ace4400c97d36..c9cfc6ad50d3af9bfa0fed684117faf253f3a589 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 d92b11d05a1f6f9570f3cff55729a0d8ea74dd9e..2d68d9601f8aea711dc112062088fb5c7a885671 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 c490a5b7371e5d91fc48725cb9b00aca590b77ef..daf05c4edbc4cbe61be5ba9903ddfd23b4e0f4dd 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 4424062d0a874e227dc1504f17456ec3831e6b50..aa71003b2f4bfa309ad8731738fa78de5bb44ab9 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 fe705d8ab5e1ffd4d44536beb31cb6bec43da957..0f9e240214c869626eb3bb39db7245eb6f778ab9 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 0000000000000000000000000000000000000000..3df3d21b8a4f4b16a2b288bb8097870dea46395c --- /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 0000000000000000000000000000000000000000..d45c53da7c513772cfbc2706d8c6bb33a91c4d46 --- /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 0000000000000000000000000000000000000000..f2fcab6ebb3990f5746c10bc64294aa09d0878b3 --- /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 511b3f414f88303441ac19a696ce1b3bfe2bdc67..4643b5259e6777e644b834784049d159c605ee8f 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 b37e67816f5d6fd3a83f8d2f60d41e562d78a4c3..94f16d21679fcd992d0785c680825cc72658f03a 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 0000000000000000000000000000000000000000..98f90a084b7d6496ec5494d4a2ac35b23525cfae --- /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 0000000000000000000000000000000000000000..21386d727ec4633d5a2f5897358f4531fd41ed8c --- /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 c53116daeb4ae6bd9abf7784650d82ceb5d3f891..43fb9d7b1086c8ddf58dcc00b8302d38b0953e39 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 0000000000000000000000000000000000000000..0151a3c9ef4408b114a0bb44e17ebac55fff94d9 --- /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 0000000000000000000000000000000000000000..e8b237d4573c98089b8abd38c6f11788f6d323f8 --- /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 0000000000000000000000000000000000000000..fadd5f762544c7d1420eddedbbd0c40d47bcbec3 --- /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 0000000000000000000000000000000000000000..3100846954fba5e51385e23d0aabb03a7c4f116d --- /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 0000000000000000000000000000000000000000..029c849e06563f06f728ca401c297480d031be3f --- /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 0000000000000000000000000000000000000000..f42376e7c4e886f3acaadc4a743eb4f4098be515 --- /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 ee2c5f96659cfc68da7708ceeaaefa1f27b895c6..dbfe808ef4655042b8e3acbf77c953f85dcfb3f3 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 0000000000000000000000000000000000000000..a1df776d263bd791e806f7ab2ab2886844ab58e7 --- /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 Binary files /dev/null and b/tests/test.jar differ