diff --git a/Makefile b/Makefile index e84913ead..4d5301f63 100644 --- a/Makefile +++ b/Makefile @@ -47,6 +47,7 @@ KO_PREFIX ?= gcr.io/projectsigstore export KO_DOCKER_REPO=$(KO_PREFIX) REKOR_YAML ?= rekor-$(GIT_TAG).yaml GHCR_PREFIX ?= ghcr.io/sigstore/rekor +GOBIN ?= $(shell go env GOPATH)/bin # Binaries SWAGGER := $(TOOLS_BIN_DIR)/swagger diff --git a/cmd/rekor-cli/app/pflag_groups.go b/cmd/rekor-cli/app/pflag_groups.go index c94c140e3..aeabc4aec 100644 --- a/cmd/rekor-cli/app/pflag_groups.go +++ b/cmd/rekor-cli/app/pflag_groups.go @@ -16,6 +16,7 @@ package app import ( + "encoding/base64" "errors" "fmt" "net/url" @@ -87,6 +88,11 @@ func addArtifactPFlags(cmd *cobra.Command) error { "path or URL to pre-formatted entry file", false, }, + "aad": { + base64Flag, + "base64 encoded additional authenticated data", + false, + }, } for flag, flagVal := range flags { @@ -152,6 +158,10 @@ func CreatePropsFromPflags() *types.ArtifactProperties { } props.PKIFormat = viper.GetString("pki-format") + b64aad := viper.GetString("aad") + if b64aad != "" { + props.AdditionalAuthenticatedData, _ = base64.StdEncoding.DecodeString(b64aad) + } return props } diff --git a/cmd/rekor-cli/app/pflags.go b/cmd/rekor-cli/app/pflags.go index c34c124a6..5a83883c3 100644 --- a/cmd/rekor-cli/app/pflags.go +++ b/cmd/rekor-cli/app/pflags.go @@ -16,6 +16,7 @@ package app import ( + "encoding/base64" "fmt" "log" "strconv" @@ -46,6 +47,7 @@ const ( oidFlag FlagType = "oid" formatFlag FlagType = "format" timeoutFlag FlagType = "timeout" + base64Flag FlagType = "base64" ) type newPFlagValueFunc func() pflag.Value @@ -105,6 +107,10 @@ func initializePFlagMap() { // this validates the timeout is >= 0 return valueFactory(formatFlag, validateTimeout, "") }, + base64Flag: func() pflag.Value { + // This validates the string is in base64 format + return valueFactory(base64Flag, validateBase64, "") + }, } } @@ -239,6 +245,13 @@ func validateTimeout(v string) error { return useValidator(timeoutFlag, d) } +// validateBase64 ensures that the supplied string is valid base64 encoded data +func validateBase64(v string) error { + _, err := base64.StdEncoding.DecodeString(v) + + return err +} + // validateTypeFlag ensures that the string is in the format type(\.version)? and // that one of the types requested is implemented func validateTypeFlag(v string) error { diff --git a/cmd/rekor-cli/app/pflags_test.go b/cmd/rekor-cli/app/pflags_test.go index 625c74928..6b1faa07b 100644 --- a/cmd/rekor-cli/app/pflags_test.go +++ b/cmd/rekor-cli/app/pflags_test.go @@ -38,6 +38,7 @@ func TestArtifactPFlags(t *testing.T) { signature string publicKey string uuid string + aad string uuidRequired bool logIndex string logIndexRequired bool @@ -346,6 +347,32 @@ func TestArtifactPFlags(t *testing.T) { expectParseSuccess: true, expectValidateSuccess: false, }, + { + caseDesc: "valid cose, with aad", + typeStr: "cose", + artifact: "../../../tests/test_cose.cbor", + publicKey: "../../../tests/test_cose.pub", + expectParseSuccess: true, + expectValidateSuccess: true, + aad: "dGVzdCBhYWQ=", + }, + { + caseDesc: "valid cose, malformed base64 aad", + typeStr: "cose", + artifact: "../../../tests/test_cose.cbor", + publicKey: "../../../tests/test_cose.pub", + expectParseSuccess: false, + expectValidateSuccess: true, + aad: "dGVzdCBhYWQ]", + }, + { + caseDesc: "valid cose, missing aad", + typeStr: "cose", + artifact: "../../../tests/test_cose.cbor", + publicKey: "../../../tests/test_cose.pub", + expectParseSuccess: true, + expectValidateSuccess: false, + }, } for _, tc := range tests { @@ -384,6 +411,9 @@ func TestArtifactPFlags(t *testing.T) { if tc.logIndex != "" { args = append(args, "--log-index", tc.logIndex) } + if tc.aad != "" { + args = append(args, "--aad", tc.aad) + } if err := blankCmd.ParseFlags(args); (err == nil) != tc.expectParseSuccess { t.Errorf("unexpected result parsing '%v': %v", tc.caseDesc, err) diff --git a/cmd/rekor-cli/app/root.go b/cmd/rekor-cli/app/root.go index 6363beee7..400b9acb4 100644 --- a/cmd/rekor-cli/app/root.go +++ b/cmd/rekor-cli/app/root.go @@ -28,6 +28,7 @@ import ( // these imports are to call the packages' init methods _ "github.com/sigstore/rekor/pkg/types/alpine/v0.0.1" + _ "github.com/sigstore/rekor/pkg/types/cose/v0.0.1" _ "github.com/sigstore/rekor/pkg/types/hashedrekord/v0.0.1" _ "github.com/sigstore/rekor/pkg/types/helm/v0.0.1" _ "github.com/sigstore/rekor/pkg/types/intoto/v0.0.1" diff --git a/cmd/rekor-server/app/serve.go b/cmd/rekor-server/app/serve.go index 8d2e25539..b3534ef5f 100644 --- a/cmd/rekor-server/app/serve.go +++ b/cmd/rekor-server/app/serve.go @@ -31,6 +31,8 @@ import ( "github.com/sigstore/rekor/pkg/log" "github.com/sigstore/rekor/pkg/types/alpine" alpine_v001 "github.com/sigstore/rekor/pkg/types/alpine/v0.0.1" + "github.com/sigstore/rekor/pkg/types/cose" + cose_v001 "github.com/sigstore/rekor/pkg/types/cose/v0.0.1" hashedrekord "github.com/sigstore/rekor/pkg/types/hashedrekord" hashedrekord_v001 "github.com/sigstore/rekor/pkg/types/hashedrekord/v0.0.1" "github.com/sigstore/rekor/pkg/types/helm" @@ -87,6 +89,7 @@ var serveCmd = &cobra.Command{ rpm.KIND: rpm_v001.APIVERSION, jar.KIND: jar_v001.APIVERSION, intoto.KIND: intoto_v001.APIVERSION, + cose.KIND: cose_v001.APIVERSION, rfc3161.KIND: rfc3161_v001.APIVERSION, alpine.KIND: alpine_v001.APIVERSION, helm.KIND: helm_v001.APIVERSION, diff --git a/go.mod b/go.mod index 4b3229c62..ef5bea61d 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/mediocregopher/radix/v4 v4.1.0 github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/mapstructure v1.5.0 + github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.12.2 github.com/rs/cors v1.8.2 github.com/sassoftware/relic v0.0.0-20210427151427-dfb082b79b74 @@ -37,6 +38,8 @@ require ( github.com/theupdateframework/go-tuf v0.3.0 github.com/transparency-dev/merkle v0.0.1 github.com/urfave/negroni v1.0.0 + github.com/veraison/go-cose v1.0.0-alpha.1 + github.com/zalando/go-keyring v0.1.1 // indirect go.uber.org/goleak v1.1.12 go.uber.org/zap v1.21.0 gocloud.dev v0.24.1-0.20211119014450-028788aaaa4c @@ -66,6 +69,7 @@ require ( github.com/danieljoos/wincred v1.1.1 // indirect github.com/docker/go-units v0.4.0 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/fxamacker/cbor/v2 v2.4.0 // indirect github.com/go-openapi/analysis v0.21.2 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect @@ -95,7 +99,6 @@ require ( github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.1 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.34.0 // indirect github.com/prometheus/procfs v0.7.3 // indirect @@ -108,7 +111,7 @@ require ( github.com/tilinna/clock v1.1.0 // indirect github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect github.com/ulikunitz/xz v0.5.10 // indirect - github.com/zalando/go-keyring v0.1.1 // indirect + github.com/x448/float16 v0.8.4 // indirect go.mongodb.org/mongo-driver v1.8.3 // indirect go.opencensus.io v0.23.0 // indirect go.uber.org/atomic v1.9.0 // indirect diff --git a/go.sum b/go.sum index 9fbfbeb16..26caa8130 100644 --- a/go.sum +++ b/go.sum @@ -475,6 +475,8 @@ github.com/fullstorydev/grpcurl v1.6.0/go.mod h1:ZQ+ayqbKMJNhzLmbpCiurTVlaK2M/3n github.com/fullstorydev/grpcurl v1.8.0/go.mod h1:Mn2jWbdMrQGJQ8UD62uNyMumT2acsZUCkZIqFxsQf1o= github.com/fullstorydev/grpcurl v1.8.1/go.mod h1:3BWhvHZwNO7iLXaQlojdg5NA6SxUDePli4ecpK1N7gw= github.com/fullstorydev/grpcurl v1.8.6/go.mod h1:WhP7fRQdhxz2TkL97u+TCb505sxfH78W1usyoB3tepw= +github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= +github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= @@ -1581,6 +1583,8 @@ github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+ github.com/valyala/quicktemplate v1.7.0/go.mod h1:sqKJnoaOF88V07vkO+9FL8fb9uZg/VPSJnLYn+LmLk8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI= +github.com/veraison/go-cose v1.0.0-alpha.1 h1:W5AhenQOS3ZDsJH2rdDMffLuuFOIoZw6VfIAkPatsRs= +github.com/veraison/go-cose v1.0.0-alpha.1/go.mod h1:7ziE85vSq4ScFTg6wyoMXjucIGOf4JkFEZi/an96Ct4= github.com/viki-org/dnscache v0.0.0-20130720023526-c70c1f23c5d8/go.mod h1:dniwbG03GafCjFohMDmz6Zc6oCuiqgH6tGNyXTkHzXE= github.com/vmihailenco/msgpack/v4 v4.3.12 h1:07s4sz9IReOgdikxLTKNbBdqDMLsjPKXwvCazn8G65U= github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= @@ -1588,6 +1592,8 @@ github.com/vmihailenco/tagparser v0.1.1 h1:quXMXlA39OCbd2wAdTsGDlK9RkOk6Wuw+x37w github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/weppos/publicsuffix-go v0.15.1-0.20210807195340-dc689ff0bb59/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE= github.com/weppos/publicsuffix-go v0.15.1-0.20220329081811-9a40b608a236/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/go-gitlab v0.31.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= diff --git a/openapi.yaml b/openapi.yaml index 4878ae2f7..fa53c02ab 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -378,6 +378,23 @@ definitions: - spec additionalProperties: false + cose: + type: object + description: COSE object + 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/cose/cose_schema.json' + required: + - apiVersion + - spec + additionalProperties: false + jar: type: object description: Java Archive (JAR) diff --git a/pkg/generated/models/cose.go b/pkg/generated/models/cose.go new file mode 100644 index 000000000..8de4083ba --- /dev/null +++ b/pkg/generated/models/cose.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" +) + +// Cose COSE object +// +// swagger:model cose +type Cose 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 CoseSchema `json:"spec"` +} + +// Kind gets the kind of this subtype +func (m *Cose) Kind() string { + return "cose" +} + +// SetKind sets the kind of this subtype +func (m *Cose) SetKind(val string) { +} + +// UnmarshalJSON unmarshals this object with a polymorphic type from a JSON structure +func (m *Cose) 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 CoseSchema `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 Cose + + 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 Cose) 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 CoseSchema `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 cose +func (m *Cose) 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 *Cose) 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 *Cose) validateSpec(formats strfmt.Registry) error { + + if m.Spec == nil { + return errors.Required("spec", "body", nil) + } + + return nil +} + +// ContextValidate validate this cose based on the context it is used +func (m *Cose) 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 *Cose) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *Cose) UnmarshalBinary(b []byte) error { + var res Cose + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/pkg/generated/models/cose_schema.go b/pkg/generated/models/cose_schema.go new file mode 100644 index 000000000..1d4f0dca1 --- /dev/null +++ b/pkg/generated/models/cose_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 + +// CoseSchema COSE Schema +// +// COSE for Rekord objects +// +// swagger:model coseSchema +type CoseSchema interface{} diff --git a/pkg/generated/models/cose_v001_schema.go b/pkg/generated/models/cose_v001_schema.go new file mode 100644 index 000000000..caadb44d4 --- /dev/null +++ b/pkg/generated/models/cose_v001_schema.go @@ -0,0 +1,508 @@ +// 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" +) + +// CoseV001Schema cose v0.0.1 Schema +// +// Schema for cose object +// +// swagger:model coseV001Schema +type CoseV001Schema struct { + + // data + // Required: true + Data *CoseV001SchemaData `json:"data"` + + // The COSE Sign1 Message + // Format: byte + Message strfmt.Base64 `json:"message,omitempty"` + + // The public key that can verify the signature + // Required: true + // Format: byte + PublicKey *strfmt.Base64 `json:"publicKey"` +} + +// Validate validates this cose v001 schema +func (m *CoseV001Schema) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateData(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 *CoseV001Schema) validateData(formats strfmt.Registry) error { + + if err := validate.Required("data", "body", m.Data); err != nil { + return err + } + + if m.Data != nil { + if err := m.Data.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("data") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("data") + } + return err + } + } + + return nil +} + +func (m *CoseV001Schema) validatePublicKey(formats strfmt.Registry) error { + + if err := validate.Required("publicKey", "body", m.PublicKey); err != nil { + return err + } + + return nil +} + +// ContextValidate validate this cose v001 schema based on the context it is used +func (m *CoseV001Schema) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateData(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *CoseV001Schema) contextValidateData(ctx context.Context, formats strfmt.Registry) error { + + if m.Data != nil { + if err := m.Data.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("data") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("data") + } + return err + } + } + + return nil +} + +// MarshalBinary interface implementation +func (m *CoseV001Schema) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *CoseV001Schema) UnmarshalBinary(b []byte) error { + var res CoseV001Schema + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} + +// CoseV001SchemaData Information about the content associated with the entry +// +// swagger:model CoseV001SchemaData +type CoseV001SchemaData struct { + + // Specifies the additional authenticated data required to verify the signature + // Format: byte + Aad strfmt.Base64 `json:"aad,omitempty"` + + // envelope hash + EnvelopeHash *CoseV001SchemaDataEnvelopeHash `json:"envelopeHash,omitempty"` + + // payload hash + PayloadHash *CoseV001SchemaDataPayloadHash `json:"payloadHash,omitempty"` +} + +// Validate validates this cose v001 schema data +func (m *CoseV001SchemaData) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateEnvelopeHash(formats); err != nil { + res = append(res, err) + } + + if err := m.validatePayloadHash(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *CoseV001SchemaData) validateEnvelopeHash(formats strfmt.Registry) error { + if swag.IsZero(m.EnvelopeHash) { // not required + return nil + } + + if m.EnvelopeHash != nil { + if err := m.EnvelopeHash.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("data" + "." + "envelopeHash") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("data" + "." + "envelopeHash") + } + return err + } + } + + return nil +} + +func (m *CoseV001SchemaData) validatePayloadHash(formats strfmt.Registry) error { + if swag.IsZero(m.PayloadHash) { // not required + return nil + } + + if m.PayloadHash != nil { + if err := m.PayloadHash.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("data" + "." + "payloadHash") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("data" + "." + "payloadHash") + } + return err + } + } + + return nil +} + +// ContextValidate validate this cose v001 schema data based on the context it is used +func (m *CoseV001SchemaData) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateEnvelopeHash(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidatePayloadHash(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *CoseV001SchemaData) contextValidateEnvelopeHash(ctx context.Context, formats strfmt.Registry) error { + + if m.EnvelopeHash != nil { + if err := m.EnvelopeHash.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("data" + "." + "envelopeHash") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("data" + "." + "envelopeHash") + } + return err + } + } + + return nil +} + +func (m *CoseV001SchemaData) contextValidatePayloadHash(ctx context.Context, formats strfmt.Registry) error { + + if m.PayloadHash != nil { + if err := m.PayloadHash.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("data" + "." + "payloadHash") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("data" + "." + "payloadHash") + } + return err + } + } + + return nil +} + +// MarshalBinary interface implementation +func (m *CoseV001SchemaData) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *CoseV001SchemaData) UnmarshalBinary(b []byte) error { + var res CoseV001SchemaData + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} + +// CoseV001SchemaDataEnvelopeHash Specifies the hash algorithm and value for the COSE envelope +// +// swagger:model CoseV001SchemaDataEnvelopeHash +type CoseV001SchemaDataEnvelopeHash struct { + + // The hashing function used to compute the hash value + // Required: true + // Enum: [sha256] + Algorithm *string `json:"algorithm"` + + // The hash value for the envelope + // Required: true + Value *string `json:"value"` +} + +// Validate validates this cose v001 schema data envelope hash +func (m *CoseV001SchemaDataEnvelopeHash) 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 coseV001SchemaDataEnvelopeHashTypeAlgorithmPropEnum []interface{} + +func init() { + var res []string + if err := json.Unmarshal([]byte(`["sha256"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + coseV001SchemaDataEnvelopeHashTypeAlgorithmPropEnum = append(coseV001SchemaDataEnvelopeHashTypeAlgorithmPropEnum, v) + } +} + +const ( + + // CoseV001SchemaDataEnvelopeHashAlgorithmSha256 captures enum value "sha256" + CoseV001SchemaDataEnvelopeHashAlgorithmSha256 string = "sha256" +) + +// prop value enum +func (m *CoseV001SchemaDataEnvelopeHash) validateAlgorithmEnum(path, location string, value string) error { + if err := validate.EnumCase(path, location, value, coseV001SchemaDataEnvelopeHashTypeAlgorithmPropEnum, true); err != nil { + return err + } + return nil +} + +func (m *CoseV001SchemaDataEnvelopeHash) validateAlgorithm(formats strfmt.Registry) error { + + if err := validate.Required("data"+"."+"envelopeHash"+"."+"algorithm", "body", m.Algorithm); err != nil { + return err + } + + // value enum + if err := m.validateAlgorithmEnum("data"+"."+"envelopeHash"+"."+"algorithm", "body", *m.Algorithm); err != nil { + return err + } + + return nil +} + +func (m *CoseV001SchemaDataEnvelopeHash) validateValue(formats strfmt.Registry) error { + + if err := validate.Required("data"+"."+"envelopeHash"+"."+"value", "body", m.Value); err != nil { + return err + } + + return nil +} + +// ContextValidate validate this cose v001 schema data envelope hash based on the context it is used +func (m *CoseV001SchemaDataEnvelopeHash) 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 *CoseV001SchemaDataEnvelopeHash) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *CoseV001SchemaDataEnvelopeHash) UnmarshalBinary(b []byte) error { + var res CoseV001SchemaDataEnvelopeHash + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} + +// CoseV001SchemaDataPayloadHash Specifies the hash algorithm and value for the content +// +// swagger:model CoseV001SchemaDataPayloadHash +type CoseV001SchemaDataPayloadHash struct { + + // The hashing function used to compute the hash value + // Required: true + // Enum: [sha256] + Algorithm *string `json:"algorithm"` + + // The hash value for the content + // Required: true + Value *string `json:"value"` +} + +// Validate validates this cose v001 schema data payload hash +func (m *CoseV001SchemaDataPayloadHash) 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 coseV001SchemaDataPayloadHashTypeAlgorithmPropEnum []interface{} + +func init() { + var res []string + if err := json.Unmarshal([]byte(`["sha256"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + coseV001SchemaDataPayloadHashTypeAlgorithmPropEnum = append(coseV001SchemaDataPayloadHashTypeAlgorithmPropEnum, v) + } +} + +const ( + + // CoseV001SchemaDataPayloadHashAlgorithmSha256 captures enum value "sha256" + CoseV001SchemaDataPayloadHashAlgorithmSha256 string = "sha256" +) + +// prop value enum +func (m *CoseV001SchemaDataPayloadHash) validateAlgorithmEnum(path, location string, value string) error { + if err := validate.EnumCase(path, location, value, coseV001SchemaDataPayloadHashTypeAlgorithmPropEnum, true); err != nil { + return err + } + return nil +} + +func (m *CoseV001SchemaDataPayloadHash) validateAlgorithm(formats strfmt.Registry) error { + + if err := validate.Required("data"+"."+"payloadHash"+"."+"algorithm", "body", m.Algorithm); err != nil { + return err + } + + // value enum + if err := m.validateAlgorithmEnum("data"+"."+"payloadHash"+"."+"algorithm", "body", *m.Algorithm); err != nil { + return err + } + + return nil +} + +func (m *CoseV001SchemaDataPayloadHash) validateValue(formats strfmt.Registry) error { + + if err := validate.Required("data"+"."+"payloadHash"+"."+"value", "body", m.Value); err != nil { + return err + } + + return nil +} + +// ContextValidate validate this cose v001 schema data payload hash based on the context it is used +func (m *CoseV001SchemaDataPayloadHash) 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 *CoseV001SchemaDataPayloadHash) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *CoseV001SchemaDataPayloadHash) UnmarshalBinary(b []byte) error { + var res CoseV001SchemaDataPayloadHash + 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 a9c360586..6ebbf1016 100644 --- a/pkg/generated/models/proposed_entry.go +++ b/pkg/generated/models/proposed_entry.go @@ -121,6 +121,12 @@ func unmarshalProposedEntry(data []byte, consumer runtime.Consumer) (ProposedEnt return nil, err } return &result, nil + case "cose": + var result Cose + if err := consumer.Consume(buf2, &result); err != nil { + return nil, err + } + return &result, nil case "hashedrekord": var result Hashedrekord if err := consumer.Consume(buf2, &result); err != nil { diff --git a/pkg/types/README.md b/pkg/types/README.md index 510ab4a9e..f77f2408c 100644 --- a/pkg/types/README.md +++ b/pkg/types/README.md @@ -20,5 +20,7 @@ Rekor supports pluggable types (aka different schemas) for entries stored in the - Versions: 0.0.1 - RPM Packages [schema](rpm/rpm_schema.json) - Versions: 0.0.1 +- COSE Envelopes [schema](cose/cose_schema.json) + - Versions: 0.0.1 Refer to [Rekor docs](https://docs.sigstore.dev/rekor/pluggable-types) for adding support for new types. diff --git a/pkg/types/cose/README.md b/pkg/types/cose/README.md new file mode 100644 index 000000000..64550686a --- /dev/null +++ b/pkg/types/cose/README.md @@ -0,0 +1,36 @@ +**COSE Type Data Documentation** + +This document provides a definition for each field that is not +otherwise described in the [cose +schema](https://github.com/sigstore/rekor/blob/main/pkg/types/cose/v0.0.1/cose_v0_0_1_schema.json). This +document also notes any additional information about the values +associated with each field such as the format in which the data is +stored and any necessary transformations. + +**AAD** Additional Authenticated Data. + +If the COSE envelope is signed using AAD, the same data must be +provided during upload, otherwise the signature verification will +fail. This data is not stored in Rekor. + +**How do you identify an object as an cose object?** + +The "Body" field will include an "coseObj" field. + +**Recognized content types** + +- [in-toto + statements](https://github.com/in-toto/attestation/tree/main/spec#statement) + are recognized and parsed. The found subject hashes are indexed so + they can be searched for. + +**What data about the envelope is stored in Rekor** + +Only the hash of the payload, the hash of the COSE envelope and the +public key is stored. + +If Rekor is configured to use attestation storage, the entire +envelope is also stored. If attestation storage is enabled, the COSE +envelope is stored as an attestation, which means that during +retrieval of the record, the complete envelope is returned in the +`attestation` field, not within the `body`. diff --git a/pkg/types/cose/cose.go b/pkg/types/cose/cose.go new file mode 100644 index 000000000..dcdc072a3 --- /dev/null +++ b/pkg/types/cose/cose.go @@ -0,0 +1,74 @@ +// +// Copyright 2022 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 cose + +import ( + "context" + "errors" + "fmt" + + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/rekor/pkg/types" +) + +const ( + KIND = "cose" +) + +type BaseCOSEType struct { + types.RekorType +} + +func init() { + types.TypeMap.Store(KIND, New) +} + +func New() types.TypeImpl { + bit := BaseCOSEType{} + bit.Kind = KIND + bit.VersionMap = VersionMap + return &bit +} + +var VersionMap = types.NewSemVerEntryFactoryMap() + +func (it BaseCOSEType) UnmarshalEntry(pe models.ProposedEntry) (types.EntryImpl, error) { + if pe == nil { + return nil, errors.New("proposed entry cannot be nil") + } + + in, ok := pe.(*models.Cose) + if !ok { + return nil, errors.New("cannot unmarshal non-COSE types") + } + + return it.VersionedUnmarshal(in, *in.APIVersion) +} + +func (it *BaseCOSEType) CreateProposedEntry(ctx context.Context, version string, props types.ArtifactProperties) (models.ProposedEntry, error) { + if version == "" { + version = it.DefaultVersion() + } + ei, err := it.VersionedUnmarshal(nil, version) + if err != nil { + return nil, fmt.Errorf("fetching COSE version implementation: %w", err) + } + return ei.CreateFromArtifactProperties(ctx, props) +} + +func (it BaseCOSEType) DefaultVersion() string { + return "0.0.1" +} diff --git a/pkg/types/cose/cose_schema.json b/pkg/types/cose/cose_schema.json new file mode 100644 index 000000000..68cf653c8 --- /dev/null +++ b/pkg/types/cose/cose_schema.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://rekor.sigstore.dev/types/cose/cose_schema.json", + "title": "COSE Schema", + "description": "COSE for Rekord objects", + "type": "object", + "oneOf": [ + { + "$ref": "v0.0.1/cose_v0_0_1_schema.json" + } + ] +} diff --git a/pkg/types/cose/cose_test.go b/pkg/types/cose/cose_test.go new file mode 100644 index 000000000..04290ec46 --- /dev/null +++ b/pkg/types/cose/cose_test.go @@ -0,0 +1,168 @@ +// +// Copyright 2022 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 cose + +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.Cose + types.BaseUnmarshalTester +} + +type UnmarshalFailsTester struct { + types.BaseUnmarshalTester +} + +func (u UnmarshalFailsTester) NewEntry() types.EntryImpl { + return &UnmarshalFailsTester{} +} + +func (u UnmarshalFailsTester) Unmarshal(pe models.ProposedEntry) error { + return errors.New("error") +} + +func TestCOSEType(t *testing.T) { + // empty to start + if VersionMap.Count() != 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" + err := VersionMap.SetEntryFactory(invalidSemVerRange, u.NewEntry) + if err == nil || VersionMap.Count() > 0 { + t.Error("invalid semver range was incorrectly added to SemVerToFacFnMap") + } + + // valid semver range can be parsed + err = VersionMap.SetEntryFactory(">= 1.2.3", u.NewEntry) + if err != nil || VersionMap.Count() != 1 { + t.Error("valid semver range was not added to SemVerToFacFnMap") + } + + u.Cose.APIVersion = swag.String("2.0.1") + brt := New() + + // version requested matches implementation in map + if _, err := brt.UnmarshalEntry(&u.Cose); err != nil { + t.Errorf("unexpected error in Unmarshal: %v", err) + } + + // version requested fails to match implementation in map + u.Cose.APIVersion = swag.String("1.2.2") + if _, err := brt.UnmarshalEntry(&u.Cose); err == nil { + t.Error("unexpected success in Unmarshal for non-matching version") + } + + // error in Unmarshal call is raised appropriately + u.Cose.APIVersion = swag.String("2.2.0") + u2 := UnmarshalFailsTester{} + _ = VersionMap.SetEntryFactory(">= 1.2.3", u2.NewEntry) + if _, err := brt.UnmarshalEntry(&u.Cose); err == nil { + t.Error("unexpected success in Unmarshal when error is thrown") + } + + // version requested fails to match implementation in map + u.Cose.APIVersion = swag.String("not_a_version") + if _, err := brt.UnmarshalEntry(&u.Cose); err == nil { + t.Error("unexpected success in Unmarshal for invalid version") + } + + ti, err := brt.UnmarshalEntry(nil) + if ti != nil { + t.Error("unexpected success in unmarshal for nil") + } + if err == nil { + t.Error("expected error") + } + + ti, err = brt.UnmarshalEntry(types.BaseProposedEntryTester{}) + if ti != nil { + t.Error("unexpected success in unmarshal for nil") + } + if err == nil { + t.Error("expected error") + } + +} + +func TestCOSEDefaultVersion(t *testing.T) { + brt := New() + ver := brt.DefaultVersion() + if ver != "0.0.1" { + t.Errorf("unexpected default version %s", ver) + } +} + +func TestCOSECreateProposedEntry(t *testing.T) { + // Reset semver map + VersionMap = types.NewSemVerEntryFactoryMap() + u := UnmarshalTester{} + VersionMap.SetEntryFactory("0.0.3", u.NewEntry) + VersionMap.SetEntryFactory(New().DefaultVersion(), u.NewEntry) + + t.Run("unknown version", func(t *testing.T) { + ctx := context.Background() + brt := New() + props := types.ArtifactProperties{} + pe, err := brt.CreateProposedEntry(ctx, "1.2.3", props) + + if pe != nil { + t.Error("unexpected propsed entry") + } + if err == nil { + t.Error("expected error") + } + }) + t.Run("valid version", func(t *testing.T) { + ctx := context.Background() + brt := New() + props := types.ArtifactProperties{} + pe, err := brt.CreateProposedEntry(ctx, "0.0.3", props) + + // BaseUnmarshalTester returns nil for the proposed entry + if pe != nil { + t.Error("unexpected proposed entry") + } + if err != nil { + t.Error("unexpected error") + } + }) + t.Run("default version", func(t *testing.T) { + ctx := context.Background() + brt := New() + props := types.ArtifactProperties{} + pe, err := brt.CreateProposedEntry(ctx, "", props) + + // BaseUnmarshalTester returns nil for the proposed entry + if pe != nil { + t.Error("unexpected proposed entry") + } + if err != nil { + t.Error("unexpected error") + } + }) +} diff --git a/pkg/types/cose/v0.0.1/cose_v0_0_1_schema.json b/pkg/types/cose/v0.0.1/cose_v0_0_1_schema.json new file mode 100644 index 000000000..cf6cef62b --- /dev/null +++ b/pkg/types/cose/v0.0.1/cose_v0_0_1_schema.json @@ -0,0 +1,81 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://rekor.sigstore.dev/types/intoto/intoto_v0_0_1_schema.json", + "title": "cose v0.0.1 Schema", + "description": "Schema for cose object", + "type": "object", + "properties": { + "message": { + "description": "The COSE Sign1 Message", + "type": "string", + "format": "byte", + "writeOnly": true + }, + "publicKey": { + "description": "The public key that can verify the signature", + "type": "string", + "format": "byte" + }, + "data": { + "description": "Information about the content associated with the entry", + "type": "object", + "properties": { + "payloadHash": { + "description": "Specifies the hash algorithm and value for the content", + "type": "object", + "readOnly": true, + "properties": { + "algorithm": { + "description": "The hashing function used to compute the hash value", + "type": "string", + "enum": [ + "sha256" + ] + }, + "value": { + "description": "The hash value for the content", + "type": "string" + } + }, + "required": [ + "algorithm", + "value" + ] + }, + "envelopeHash": { + "description": "Specifies the hash algorithm and value for the COSE envelope", + "type": "object", + "readOnly": true, + "properties": { + "algorithm": { + "description": "The hashing function used to compute the hash value", + "type": "string", + "enum": [ + "sha256" + ] + }, + "value": { + "description": "The hash value for the envelope", + "type": "string" + } + }, + "required": [ + "algorithm", + "value" + ] + }, + "aad": { + "description": "Specifies the additional authenticated data required to verify the signature", + "type": "string", + "format": "byte", + "writeOnly": true + } + }, + "required": [] + } + }, + "required": [ + "publicKey", + "data" + ] +} diff --git a/pkg/types/cose/v0.0.1/entry.go b/pkg/types/cose/v0.0.1/entry.go new file mode 100644 index 000000000..55dcf3008 --- /dev/null +++ b/pkg/types/cose/v0.0.1/entry.go @@ -0,0 +1,348 @@ +// +// Copyright 2022 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 cose + +import ( + "bytes" + "context" + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "path/filepath" + "strings" + + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/in-toto/in-toto-golang/in_toto" + "github.com/spf13/viper" + gocose "github.com/veraison/go-cose" + + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/rekor/pkg/log" + "github.com/sigstore/rekor/pkg/pki" + "github.com/sigstore/rekor/pkg/pki/x509" + "github.com/sigstore/rekor/pkg/types" + "github.com/sigstore/rekor/pkg/types/cose" +) + +const ( + APIVERSION = "0.0.1" +) + +const ( + CurveP256 = "P-256" +) + +func init() { + if err := cose.VersionMap.SetEntryFactory(APIVERSION, NewEntry); err != nil { + log.Logger.Panic(err) + } +} + +type V001Entry struct { + CoseObj models.CoseV001Schema + keyObj pki.PublicKey + sign1Msg *gocose.Sign1Message + envelopeHash []byte +} + +func (v V001Entry) APIVersion() string { + return APIVERSION +} + +func NewEntry() types.EntryImpl { + return &V001Entry{} +} + +func (v V001Entry) IndexKeys() ([]string, error) { + var result []string + + // We add the key, the hash of the overall cose envelope, and the hash of the payload itself as keys. + keyObj, err := x509.NewPublicKey(bytes.NewReader(*v.CoseObj.PublicKey)) + if err != nil { + return nil, err + } + + // 1. Key + key, err := keyObj.CanonicalValue() + if err != nil { + log.Logger.Error(err) + } else { + keyHash := sha256.Sum256(key) + result = append(result, strings.ToLower(hex.EncodeToString(keyHash[:]))) + } + result = append(result, keyObj.EmailAddresses()...) + + // 2. Overall envelope + result = append(result, formatKey(v.CoseObj.Message)) + + // 3. Payload + if v.sign1Msg != nil { + result = append(result, formatKey(v.sign1Msg.Payload)) + } else { + // If no payload exists (it's unpacked in validate() method) + // return now, as we will not be able to extract any headers + return result, nil + } + + // If payload is an in-toto statement, let's grab the subjects. + if rawContentType, ok := v.sign1Msg.Headers.Protected[gocose.HeaderLabelContentType]; ok { + contentType, ok := rawContentType.(string) + // Integers as defined by CoAP content format are valid too, + // but in-intoto payload type is not defined there, so only + // proceed if content type is a string. + // See list of CoAP content formats here: + // https://www.iana.org/assignments/core-parameters/core-parameters.xhtml#content-formats + if ok && contentType == in_toto.PayloadType { + stmt, err := getIntotoStatement(v.sign1Msg.Payload) + if err != nil { + // ContentType header says intoto statement, but + // parsing failed, continue with a warning. + log.Logger.Warnf("Failed to parse intoto statement") + } else { + for _, sub := range stmt.Subject { + for alg, digest := range sub.Digest { + index := alg + ":" + digest + result = append(result, index) + } + } + } + } + } + + return result, nil +} + +func getIntotoStatement(b []byte) (*in_toto.Statement, error) { + var stmt in_toto.Statement + if err := json.Unmarshal(b, &stmt); err != nil { + return nil, err + } + + return &stmt, nil +} + +func formatKey(b []byte) string { + h := sha256.Sum256(b) + hash := hex.EncodeToString(h[:]) + return strings.ToLower(fmt.Sprintf("%s:%s", models.CoseV001SchemaDataPayloadHashAlgorithmSha256, hash)) +} + +func (v *V001Entry) Unmarshal(pe models.ProposedEntry) error { + it, ok := pe.(*models.Cose) + if !ok { + return errors.New("cannot unmarshal non Cose v0.0.1 type") + } + + var err error + if err := types.DecodeEntry(it.Spec, &v.CoseObj); err != nil { + return err + } + + // field validation + if err := v.CoseObj.Validate(strfmt.Default); err != nil { + return err + } + + v.keyObj, err = x509.NewPublicKey(bytes.NewReader(*v.CoseObj.PublicKey)) + if err != nil { + return err + } + + // Store the envelope hash. + // The CoseObj.Message is only populated during entry creation. + // When marshalling from the database (retrieval) the envelope + // hash must be decoded fromt he stored hex string. + // The envelope hash is used to create the attestation key during + // retrieval of a record. + if len(v.CoseObj.Message) == 0 { + b, err := hex.DecodeString(*v.CoseObj.Data.EnvelopeHash.Value) + if err != nil { + return err + } + v.envelopeHash = b + } else { + h := sha256.Sum256(v.CoseObj.Message) + v.envelopeHash = h[:] + } + + return v.validate() +} + +func (v *V001Entry) Canonicalize(ctx context.Context) ([]byte, error) { + if v.keyObj == nil { + return nil, errors.New("cannot canonicalze empty key") + } + pk, err := v.keyObj.CanonicalValue() + if err != nil { + return nil, err + } + pkb := strfmt.Base64(pk) + + h := sha256.Sum256([]byte(v.sign1Msg.Payload)) + + canonicalEntry := models.CoseV001Schema{ + PublicKey: &pkb, + Data: &models.CoseV001SchemaData{ + PayloadHash: &models.CoseV001SchemaDataPayloadHash{ + Algorithm: swag.String(models.CoseV001SchemaDataPayloadHashAlgorithmSha256), + Value: swag.String(hex.EncodeToString(h[:])), + }, + EnvelopeHash: &models.CoseV001SchemaDataEnvelopeHash{ + Algorithm: swag.String(models.CoseV001SchemaDataEnvelopeHashAlgorithmSha256), + Value: swag.String(hex.EncodeToString(v.envelopeHash)), + }, + }, + } + + itObj := models.Cose{} + itObj.APIVersion = swag.String(APIVERSION) + itObj.Spec = &canonicalEntry + + return json.Marshal(&itObj) +} + +// validate performs cross-field validation for fields in object +func (v *V001Entry) validate() error { + // This also gets called in the CLI, where we won't have this data + // or during record retrieval (message is the raw COSE object) which + // is only stored as an attestation. + if len(v.CoseObj.Message) == 0 { + return nil + } + + alg, pk, err := getPublicKey(v.keyObj) + if err != nil { + return err + } + + bv, err := gocose.NewVerifier(alg, pk) + if err != nil { + return err + } + sign1Msg := gocose.NewSign1Message() + if err := sign1Msg.UnmarshalCBOR(v.CoseObj.Message); err != nil { + return err + } + + if err := sign1Msg.Verify(v.CoseObj.Data.Aad, bv); err != nil { + return err + } + + v.sign1Msg = sign1Msg + return nil +} + +func getPublicKey(pk pki.PublicKey) (gocose.Algorithm, crypto.PublicKey, error) { + invAlg := gocose.Algorithm(0) + x5pk, ok := pk.(*x509.PublicKey) + + if !ok { + return invAlg, nil, errors.New("invalid public key type") + } + + cryptoPub := x5pk.CryptoPubKey() + + var alg gocose.Algorithm + switch t := cryptoPub.(type) { + case *rsa.PublicKey: + alg = gocose.AlgorithmPS256 + case *ecdsa.PublicKey: + alg = gocose.AlgorithmES256 + if t.Params().Name != CurveP256 { + return invAlg, nil, fmt.Errorf("unsupported elliptic curve %s", + t.Params().Name) + } + default: + return invAlg, nil, fmt.Errorf("unsupported algorithm type %T", t) + } + + return alg, cryptoPub, nil +} + +// AttestationKey returns the digest of the COSE envelope that was uploaded, +// to be used to lookup the attestation from storage. +func (v *V001Entry) AttestationKey() string { + return fmt.Sprintf("%s:%s", + models.CoseV001SchemaDataEnvelopeHashAlgorithmSha256, + hex.EncodeToString(v.envelopeHash)) +} + +// AttestationKeyValue returns both the key and value to be persisted +// into attestation storage +func (v *V001Entry) AttestationKeyValue() (string, []byte) { + storageSize := len(v.CoseObj.Message) + if storageSize > viper.GetInt("max_attestation_size") { + log.Logger.Infof("Skipping attestation storage, size %d is greater than max %d", storageSize, viper.GetInt("max_attestation_size")) + return "", nil + } + + return v.AttestationKey(), v.CoseObj.Message +} + +func (v V001Entry) CreateFromArtifactProperties(_ context.Context, props types.ArtifactProperties) (models.ProposedEntry, error) { + returnVal := models.Cose{} + var err error + messageBytes := props.ArtifactBytes + if messageBytes == nil { + if props.ArtifactPath == nil { + return nil, errors.New("path to artifact file must be specified") + } + if props.ArtifactPath.IsAbs() { + return nil, errors.New("cose envelopes cannot be fetched over HTTP(S)") + } + messageBytes, err = ioutil.ReadFile(filepath.Clean(props.ArtifactPath.Path)) + if err != nil { + return nil, err + } + } + publicKeyBytes := props.PublicKeyBytes + if publicKeyBytes == nil { + if props.PublicKeyPath == nil { + return nil, errors.New("public key must be provided to verify signature") + } + publicKeyBytes, err = ioutil.ReadFile(filepath.Clean(props.PublicKeyPath.Path)) + if err != nil { + return nil, fmt.Errorf("error reading public key file: %w", err) + } + } + if err != nil { + return nil, err + } + kb := strfmt.Base64(publicKeyBytes) + mb := strfmt.Base64(messageBytes) + + re := V001Entry{ + CoseObj: models.CoseV001Schema{ + Data: &models.CoseV001SchemaData{ + Aad: props.AdditionalAuthenticatedData, + }, + PublicKey: &kb, + Message: mb, + }, + } + + returnVal.Spec = re.CoseObj + returnVal.APIVersion = swag.String(re.APIVersion()) + + return &returnVal, nil +} diff --git a/pkg/types/cose/v0.0.1/entry_test.go b/pkg/types/cose/v0.0.1/entry_test.go new file mode 100644 index 000000000..77a030d2a --- /dev/null +++ b/pkg/types/cose/v0.0.1/entry_test.go @@ -0,0 +1,659 @@ +// +// Copyright 2022 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 cose + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "encoding/pem" + "fmt" + "math/big" + "os" + "reflect" + "testing" + + "github.com/go-openapi/strfmt" + "github.com/sigstore/rekor/pkg/generated/models" + sigx509 "github.com/sigstore/rekor/pkg/pki/x509" + "github.com/sigstore/rekor/pkg/types" + "github.com/spf13/viper" + gocose "github.com/veraison/go-cose" + "go.uber.org/goleak" +) + +const ( + pubKeyP256 = `-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5P3tzcNDA11znnCFF3DHLwiHNCl3 +OXbUFakqff3cSRd4OTH1hiJgi15VIGSKZALlqjdWpf+fs87uRpiI6Yp59A== +-----END PUBLIC KEY----- +` + pubKeyP384 = `-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEPx88tPXP1ggkZHXnvg0vQAQ3vBlpKhF0 +hVt3kEn4ug3o72Wa1JnJALuOALGn4tY5Xuv9jx4BG+DzbAcyMbC3ueuw6ppQcNEu +YJtZ/ty5vUBCekso165mLmAK+l5UXWTq +-----END PUBLIC KEY----- +` + pubKeyP521 = `-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBRuRK30vNm09kt7AqbEtyZ4csZ943 +5zgNvcYlqO9GOPA5rUu8lvjbwiELR4WPr9lzofDJY/I7gq8Hzdnl6snlyycBabpQ +Ndanm2XueC84SStD3ElF6JzjsD9QGljaVYWek6to/8luw5+1niH3hNDEw5jsqa2W +/r+0gL0QOCKvVsThqp4= +-----END PUBLIC KEY----- +` + pubKeyRSA2048 = `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuqO4gwscYmCE3P8eM9eg +yIiElLNAzjWapPn99/uFAFKqkinGr/DAejP2zxgdXk+ESd8bO0Rqob1WZL8/HqQN +8kRkf2KfR7d6jFe06V7N/Fmh+3YCcNNS6K9eW86u31sjnszgdtmWDrXhsH+M0W8g +Q7rmo+7BUJAcU39iApN2GNsji6vrRLRiEnMP/fpnsLa8qYpPToSE0YVfWrKOvY2q +Qhg/LceADsJzdYP0Yp+Q2jdC1J5OvUC4Mq08YdD7EawWJ5JI2qEkcPgPn5SqPomS +ihKHDVzm+FqHEbgx0P57ZdKnk8kALNz5FFdwq46mbY8FRqGD56r4sB5rRcxy0cbB +EQIDAQAB +-----END PUBLIC KEY----- +` + pubKeyEd25519 = `-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEABhjHE6AOa33q2JGlVk9OjICRp2S6d9nUJh0Xr6PUego= +-----END PUBLIC KEY----- +` +) + +type testPublicKey int + +func (t testPublicKey) CanonicalValue() ([]byte, error) { + return nil, nil +} + +func (t testPublicKey) EmailAddresses() []string { + return nil +} + +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 p(b []byte) *strfmt.Base64 { + b64 := strfmt.Base64(b) + return &b64 +} + +func makeSignedCose(t *testing.T, priv *ecdsa.PrivateKey, payload, aad []byte, contentType interface{}) []byte { + m := gocose.NewSign1Message() + m.Payload = payload + m.Headers.Protected[gocose.HeaderLabelAlgorithm] = gocose.AlgorithmES256 + + if contentType != "" { + m.Headers.Protected[gocose.HeaderLabelContentType] = contentType + } + + signer, err := gocose.NewSigner(gocose.AlgorithmES256, priv) + if err != nil { + t.Fatal(err) + } + + if err := m.Sign(rand.Reader, aad, signer); err != nil { + t.Fatal(err) + } + + msg, err := m.MarshalCBOR() + if err != nil { + t.Fatal(err) + } + return msg +} + +func TestV001Entry_Unmarshal(t *testing.T) { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + der, err := x509.MarshalPKIXPublicKey(&priv.PublicKey) + if err != nil { + t.Fatal(err) + } + pub := pem.EncodeToMemory(&pem.Block{ + Bytes: der, + Type: "PUBLIC KEY", + }) + + ca := &x509.Certificate{ + SerialNumber: big.NewInt(1), + } + caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &priv.PublicKey, priv) + if err != nil { + t.Fatal(err) + } + pemBytes := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: caBytes, + }) + + msg := makeSignedCose(t, priv, []byte("hello"), nil, "") + msgWithAAD := makeSignedCose(t, priv, []byte("hello"), []byte("external aad"), "") + + tests := []struct { + name string + want models.CoseV001Schema + it *models.CoseV001Schema + wantErr bool + }{ + { + name: "empty", + it: &models.CoseV001Schema{}, + wantErr: true, + }, + { + name: "missing data", + it: &models.CoseV001Schema{ + PublicKey: p(pub), + }, + wantErr: true, + }, + { + name: "missing envelope", + it: &models.CoseV001Schema{ + Data: &models.CoseV001SchemaData{}, + PublicKey: p([]byte("hello")), + }, + wantErr: true, + }, + { + name: "valid", + it: &models.CoseV001Schema{ + Data: &models.CoseV001SchemaData{}, + PublicKey: p(pub), + Message: msg, + }, + wantErr: false, + }, + { + name: "valid with aad", + it: &models.CoseV001Schema{ + Data: &models.CoseV001SchemaData{ + Aad: strfmt.Base64("external aad"), + }, + PublicKey: p(pub), + Message: msgWithAAD, + }, + wantErr: false, + }, + { + name: "extra aad", + it: &models.CoseV001Schema{ + Data: &models.CoseV001SchemaData{ + Aad: strfmt.Base64("aad"), + }, + PublicKey: p(pub), + Message: msg, + }, + wantErr: true, + }, + { + name: "invalid envelope", + it: &models.CoseV001Schema{ + Data: &models.CoseV001SchemaData{}, + PublicKey: p([]byte(pemBytes)), + Message: []byte("hello"), + }, + wantErr: true, + }, + { + name: "cert", + it: &models.CoseV001Schema{ + Data: &models.CoseV001SchemaData{}, + PublicKey: p([]byte(pemBytes)), + Message: msg, + }, + wantErr: false, + }, + { + name: "invalid key", + it: &models.CoseV001Schema{ + Data: &models.CoseV001SchemaData{}, + PublicKey: p([]byte("notavalidkey")), + Message: []byte("hello"), + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := &V001Entry{ + CoseObj: models.CoseV001Schema{}, + } + it := &models.Cose{ + Spec: tt.it, + } + if err := v.Unmarshal(it); (err != nil) != tt.wantErr { + t.Errorf("V001Entry.Unmarshal() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } + + t.Run("invalid type", func(t *testing.T) { + want := "cannot unmarshal non Cose v0.0.1 type" + v := V001Entry{} + if err := v.Unmarshal(&types.BaseProposedEntryTester{}); err == nil { + t.Error("expected error") + } else if err.Error() != want { + t.Errorf("wrong error: %s", err.Error()) + } + }) +} + +func TestV001Entry_IndexKeys(t *testing.T) { + payloadType := "application/vnd.in-toto+json" + attestation := ` +{ + "_type": "https://in-toto.io/Statement/v0.1", + "predicateType": "https://slsa.dev/provenance/v0.2", + "subject": [ + { + "name": "foo", + "digest": { + "sha256": "ad92c12d7947cc04000948248ccf305682f395af3e109ed044081dbb40182e6c" + } + } + ], + "predicate": { + "builder": { + "id": "https://example.com/test-builder" + }, + "buildType": "test" + } +} +` + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + + der, err := x509.MarshalPKIXPublicKey(&priv.PublicKey) + if err != nil { + t.Fatal(err) + } + pub := pem.EncodeToMemory(&pem.Block{ + Bytes: der, + Type: "PUBLIC KEY", + }) + + rawMsg := []byte(attestation) + msg := makeSignedCose(t, priv, rawMsg, nil, payloadType) + pk, err := sigx509.NewPublicKey(bytes.NewReader(pub)) + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + v := V001Entry{ + CoseObj: models.CoseV001Schema{ + Message: msg, + Data: &models.CoseV001SchemaData{}, + PublicKey: p(pub), + }, + keyObj: pk, + } + err = v.validate() + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + got, err := v.IndexKeys() + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + // Envelope digest + sha := sha256.Sum256(msg) + envDigest := "sha256:" + hex.EncodeToString(sha[:]) + mustContain(t, envDigest, got) + + // Message digest in envelope + sha = sha256.Sum256(rawMsg) + rawDigest := "sha256:" + hex.EncodeToString(sha[:]) + mustContain(t, rawDigest, got) + + // Subject from in-toto statement + mustContain(t, "sha256:ad92c12d7947cc04000948248ccf305682f395af3e109ed044081dbb40182e6c", got) +} + +func TestV001Entry_IndexKeysWrongContentType(t *testing.T) { + payloadType := "application/vnd.in-toto+json" + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + + der, err := x509.MarshalPKIXPublicKey(&priv.PublicKey) + if err != nil { + t.Fatal(err) + } + pub := pem.EncodeToMemory(&pem.Block{ + Bytes: der, + Type: "PUBLIC KEY", + }) + + rawMsg := []byte("this is not an intoto statement") + msg := makeSignedCose(t, priv, rawMsg, nil, payloadType) + pk, err := sigx509.NewPublicKey(bytes.NewReader(pub)) + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + v := V001Entry{ + CoseObj: models.CoseV001Schema{ + Message: msg, + Data: &models.CoseV001SchemaData{}, + PublicKey: p(pub), + }, + keyObj: pk, + } + err = v.validate() + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + got, err := v.IndexKeys() + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + // Envelope digest + sha := sha256.Sum256(msg) + envDigest := "sha256:" + hex.EncodeToString(sha[:]) + mustContain(t, envDigest, got) + + // Message digest in envelope + sha = sha256.Sum256(rawMsg) + rawDigest := "sha256:" + hex.EncodeToString(sha[:]) + mustContain(t, rawDigest, got) +} + +func TestV001Entry_IndexKeysIntegerContentType(t *testing.T) { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + + der, err := x509.MarshalPKIXPublicKey(&priv.PublicKey) + if err != nil { + t.Fatal(err) + } + pub := pem.EncodeToMemory(&pem.Block{ + Bytes: der, + Type: "PUBLIC KEY", + }) + + rawMsg := []byte("hello") + msg := makeSignedCose(t, priv, rawMsg, nil, 12345) + pk, err := sigx509.NewPublicKey(bytes.NewReader(pub)) + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + v := V001Entry{ + CoseObj: models.CoseV001Schema{ + Message: msg, + Data: &models.CoseV001SchemaData{}, + PublicKey: p(pub), + }, + keyObj: pk, + } + err = v.validate() + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + got, err := v.IndexKeys() + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + // Envelope digest + sha := sha256.Sum256(msg) + envDigest := "sha256:" + hex.EncodeToString(sha[:]) + mustContain(t, envDigest, got) + + // Message digest in envelope + sha = sha256.Sum256(rawMsg) + rawDigest := "sha256:" + hex.EncodeToString(sha[:]) + mustContain(t, rawDigest, got) +} + +func TestV001Entry_Attestation(t *testing.T) { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + der, err := x509.MarshalPKIXPublicKey(&priv.PublicKey) + if err != nil { + t.Fatal(err) + } + pub := pem.EncodeToMemory(&pem.Block{ + Bytes: der, + Type: "PUBLIC KEY", + }) + + msg := makeSignedCose(t, priv, []byte("hello"), nil, "") + + it := &models.Cose{ + Spec: &models.CoseV001Schema{ + Data: &models.CoseV001SchemaData{}, + PublicKey: p(pub), + Message: msg, + }, + } + + t.Run("no storage", func(t *testing.T) { + v := &V001Entry{ + CoseObj: models.CoseV001Schema{}, + } + if err := v.Unmarshal(it); err != nil { + t.Errorf("V001Entry.Unmarshal() error = %v", err) + } + key, att := v.AttestationKeyValue() + if key != "" { + t.Errorf("Unexpected key returned") + } + if len(att) != 0 { + t.Errorf("Attestation returned") + } + }) + + t.Run("with storage", func(t *testing.T) { + // Need to trick viper to update config so we can return + // an attestation + os.Setenv("MAX_ATTESTATION_SIZE", "1048576") + viper.AutomaticEnv() + + msgHash := sha256.Sum256(msg) + wantKey := fmt.Sprintf("sha256:%s", + hex.EncodeToString(msgHash[:])) + + v := &V001Entry{ + CoseObj: models.CoseV001Schema{}, + } + if err := v.Unmarshal(it); err != nil { + t.Errorf("V001Entry.Unmarshal() error = %v", err) + } + key, att := v.AttestationKeyValue() + if key != wantKey { + t.Errorf("unexpected attestation key: %s want: %s", + key, wantKey) + } + + if len(att) != len(msg) { + t.Error("Wrong attestation returned") + } + for i := range att { + if att[i] != msg[i] { + t.Error("Wrong attestation returned") + return + } + } + }) +} + +func TestGetPublicKey(t *testing.T) { + t.Run("P256", func(t *testing.T) { + pk, err := sigx509.NewPublicKey(bytes.NewBufferString(pubKeyP256)) + if err != nil { + t.Error("failed to load public key") + } + alg, cpk, err := getPublicKey(pk) + if alg != gocose.AlgorithmES256 { + t.Error("wrong algorithm") + } + if cpk == nil { + t.Error("no public key returned") + } + if err != nil { + t.Errorf("Unexpected error %s", err.Error()) + } + }) + + t.Run("P384", func(t *testing.T) { + pk, err := sigx509.NewPublicKey(bytes.NewBufferString(pubKeyP384)) + if err != nil { + t.Error("failed to load public key") + } + alg, cpk, err := getPublicKey(pk) + if alg != gocose.Algorithm(0) { + t.Error("unexpected algorithm returned") + } + if cpk != nil { + t.Error("unexpected key returned") + } + if err == nil { + t.Error("expected error") + } + }) + + t.Run("P521", func(t *testing.T) { + pk, err := sigx509.NewPublicKey(bytes.NewBufferString(pubKeyP521)) + if err != nil { + t.Error("failed to load public key") + } + alg, cpk, err := getPublicKey(pk) + if alg != gocose.Algorithm(0) { + t.Error("unexpected algorithm returned") + } + if cpk != nil { + t.Error("unexpected key returned") + } + if err == nil { + t.Error("expected error") + } + }) + + t.Run("RSA2048", func(t *testing.T) { + pk, err := sigx509.NewPublicKey(bytes.NewBufferString(pubKeyRSA2048)) + if err != nil { + t.Error("failed to load public key") + } + alg, cpk, err := getPublicKey(pk) + if alg != gocose.AlgorithmPS256 { + t.Error("unexpected algorithm returned") + } + if cpk == nil { + t.Error("no public key returned") + } + if err != nil { + t.Error("unexpected error") + } + }) + + t.Run("Invalid key", func(t *testing.T) { + alg, cpk, err := getPublicKey(testPublicKey(0)) + if alg != gocose.Algorithm(0) { + t.Error("unexpected algorithm returned") + } + if cpk != nil { + t.Error("unexpected key returned") + } + if err == nil { + t.Error("expected error") + } + }) + + t.Run("Ed25519", func(t *testing.T) { + pk, err := sigx509.NewPublicKey(bytes.NewBufferString(pubKeyEd25519)) + if err != nil { + t.Error("failed to load public key") + } + alg, cpk, err := getPublicKey(pk) + if alg != gocose.Algorithm(0) { + t.Error("unexpected algorithm returned") + } + if cpk != nil { + t.Error("unexpected key returned") + } + if err == nil { + t.Error("expected error") + } + if err.Error() != "unsupported algorithm type ed25519.PublicKey" { + t.Error("expected error") + } + }) + +} + +func TestV001Entry_Validate(t *testing.T) { + t.Run("missing message", func(t *testing.T) { + v := V001Entry{} + err := v.validate() + if err != nil { + t.Error("unexpected error") + } + }) + + t.Run("invalid public key", func(t *testing.T) { + v := V001Entry{} + v.CoseObj.Message = []byte("string") + v.keyObj, _ = sigx509.NewPublicKey(bytes.NewBufferString(pubKeyEd25519)) + err := v.validate() + if err == nil { + t.Error("expected error") + return + } + if err.Error() != "unsupported algorithm type ed25519.PublicKey" { + t.Error("wrong error returned") + } + }) + +} + +func mustContain(t *testing.T, want string, l []string) { + for _, s := range l { + if s == want { + return + } + } + t.Fatalf("list %v does not contain %s", l, want) +} diff --git a/pkg/types/entries.go b/pkg/types/entries.go index 8f71bc720..65e7ff6c9 100644 --- a/pkg/types/entries.go +++ b/pkg/types/entries.go @@ -121,12 +121,13 @@ func CanonicalizeEntry(ctx context.Context, entry EntryImpl) ([]byte, error) { // ArtifactProperties provide a consistent struct for passing values from // CLI flags to the type+version specific CreateProposeEntry() methods type ArtifactProperties struct { - ArtifactPath *url.URL - ArtifactHash string - ArtifactBytes []byte - SignaturePath *url.URL - SignatureBytes []byte - PublicKeyPath *url.URL - PublicKeyBytes []byte - PKIFormat string + AdditionalAuthenticatedData []byte + ArtifactPath *url.URL + ArtifactHash string + ArtifactBytes []byte + SignaturePath *url.URL + SignatureBytes []byte + PublicKeyPath *url.URL + PublicKeyBytes []byte + PKIFormat string } diff --git a/pkg/types/test_util.go b/pkg/types/test_util.go index e057bd19c..423c3a230 100644 --- a/pkg/types/test_util.go +++ b/pkg/types/test_util.go @@ -19,6 +19,8 @@ package types import ( "context" + "github.com/go-openapi/strfmt" + "github.com/sigstore/rekor/pkg/generated/models" ) @@ -59,3 +61,21 @@ func (u BaseUnmarshalTester) AttestationKeyValue() (string, []byte) { func (u BaseUnmarshalTester) CreateFromArtifactProperties(_ context.Context, _ ArtifactProperties) (models.ProposedEntry, error) { return nil, nil } + +type BaseProposedEntryTester struct{} + +func (b BaseProposedEntryTester) Kind() string { + return "nil" +} + +func (b BaseProposedEntryTester) SetKind(v string) { + +} + +func (b BaseProposedEntryTester) Validate(r strfmt.Registry) error { + return nil +} + +func (b BaseProposedEntryTester) ContextValidate(ctx context.Context, r strfmt.Registry) error { + return nil +} diff --git a/tests/test_cose.cbor b/tests/test_cose.cbor new file mode 100644 index 000000000..6463ce5ef --- /dev/null +++ b/tests/test_cose.cbor @@ -0,0 +1 @@ +҄C�&�Khello worldX@U�=��'��g`5��d�gG�{��;Q�|R�+��s�� ��S�r������ÁA��Z�c \ No newline at end of file diff --git a/tests/test_cose.pub b/tests/test_cose.pub new file mode 100644 index 000000000..c22caa868 --- /dev/null +++ b/tests/test_cose.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7IEW48UTgci4iVRMGQZA0CsDABAB +1ijWhU182cy9blVGfEkCNGay1wSGjiYmnrZedXh49YI7u8l9xZ5bu0rK7w== +-----END PUBLIC KEY-----