From a3b49849d9e415fb2cc5aecba2176caaa2d1c166 Mon Sep 17 00:00:00 2001 From: Dan Lorenc Date: Sun, 13 Mar 2022 08:51:47 -0500 Subject: [PATCH 01/14] WIP: Add COSE support to Rekor This commit adds COSE Sign1 support to rekor via a new data type. COSE is defined in RFC8152, and provides a signing message envelope. This is supported in rekor using the veraison/go-cose library. The new API type requires the signed content, the signature envelope, and the public key. The public key is in the standard rekor PKI format, at the moment only ECDSA P256 with SHA256 is supported. The signed message only supports in-line bodies (no URL fetching), and there is no support for pre-hashed entries. Signed-off-by: Dan Lorenc --- cmd/rekor-cli/app/root.go | 1 + cmd/rekor-server/app/serve.go | 3 + go.mod | 2 + go.sum | 16 + openapi.yaml | 17 + pkg/generated/models/cose.go | 210 ++++++++++ pkg/generated/models/cose_schema.go | 29 ++ pkg/generated/models/cose_v001_schema.go | 375 ++++++++++++++++++ pkg/generated/models/proposed_entry.go | 6 + pkg/types/cose/README.md | 7 + pkg/types/cose/cose.go | 73 ++++ pkg/types/cose/cose_schema.json | 12 + pkg/types/cose/cose_test.go | 92 +++++ pkg/types/cose/v0.0.1/cose_v0_0_1_schema.json | 60 +++ pkg/types/cose/v0.0.1/entry.go | 251 ++++++++++++ pkg/types/cose/v0.0.1/entry_test.go | 240 +++++++++++ 16 files changed, 1394 insertions(+) create mode 100644 pkg/generated/models/cose.go create mode 100644 pkg/generated/models/cose_schema.go create mode 100644 pkg/generated/models/cose_v001_schema.go create mode 100644 pkg/types/cose/README.md create mode 100644 pkg/types/cose/cose.go create mode 100644 pkg/types/cose/cose_schema.json create mode 100644 pkg/types/cose/cose_test.go create mode 100644 pkg/types/cose/v0.0.1/cose_v0_0_1_schema.json create mode 100644 pkg/types/cose/v0.0.1/entry.go create mode 100644 pkg/types/cose/v0.0.1/entry_test.go 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..842af9b84 100644 --- a/go.mod +++ b/go.mod @@ -37,6 +37,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 v0.0.0-20211126173600-dee3b3e54910 + 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 diff --git a/go.sum b/go.sum index 9fbfbeb16..b366c5700 100644 --- a/go.sum +++ b/go.sum @@ -474,8 +474,12 @@ github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmV github.com/fullstorydev/grpcurl v1.6.0/go.mod h1:ZQ+ayqbKMJNhzLmbpCiurTVlaK2M/3nqZCxaQ2Ze/sM= 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.2/go.mod h1:YvWNT3xRp2KIRuvCphFodG0fKkMXwaxA9CJgKCcyzUQ= github.com/fullstorydev/grpcurl v1.8.6/go.mod h1:WhP7fRQdhxz2TkL97u+TCb505sxfH78W1usyoB3tepw= github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E= +github.com/fxamacker/cbor/v2 v2.2.1-0.20200429214022-fc263b46c618 h1:RIQZGQ00xy1acO7H7mjL8N5ZDyI0soZG7X8akiXwSTo= +github.com/fxamacker/cbor/v2 v2.2.1-0.20200429214022-fc263b46c618/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= +github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -1582,12 +1586,24 @@ github.com/valyala/quicktemplate v1.7.0/go.mod h1:sqKJnoaOF88V07vkO+9FL8fb9uZg/V 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/viki-org/dnscache v0.0.0-20130720023526-c70c1f23c5d8/go.mod h1:dniwbG03GafCjFohMDmz6Zc6oCuiqgH6tGNyXTkHzXE= +github.com/veraison/go-cose v0.0.0-20211126173600-dee3b3e54910 h1:dtZjTJ/89XAZjDygdVe5X5/wnxo9gYtmKpfxGqYGbws= +github.com/veraison/go-cose v0.0.0-20211126173600-dee3b3e54910/go.mod h1:sjLU/8dYHRJj3RWtKLJUbPLoByKdV7nnegaTBgQ+9XA= +github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/vmihailenco/msgpack/v4 v4.3.12 h1:07s4sz9IReOgdikxLTKNbBdqDMLsjPKXwvCazn8G65U= github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= github.com/vmihailenco/tagparser v0.1.1 h1:quXMXlA39OCbd2wAdTsGDlK9RkOk6Wuw+x37wVyIuWY= 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/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= +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..61a8af5fe --- /dev/null +++ b/pkg/generated/models/cose_v001_schema.go @@ -0,0 +1,375 @@ +// 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 + // Required: true + // Format: byte + Message *strfmt.Base64 `json:"message"` + + // 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.validateMessage(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) validateMessage(formats strfmt.Registry) error { + + if err := validate.Required("message", "body", m.Message); err != nil { + 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 content inline within the document + // Required: true + // Format: byte + Content *strfmt.Base64 `json:"content"` + + // hash + Hash *CoseV001SchemaDataHash `json:"hash,omitempty"` +} + +// Validate validates this cose v001 schema data +func (m *CoseV001SchemaData) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateContent(formats); err != nil { + res = append(res, err) + } + + if err := m.validateHash(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *CoseV001SchemaData) validateContent(formats strfmt.Registry) error { + + if err := validate.Required("data"+"."+"content", "body", m.Content); err != nil { + return err + } + + return nil +} + +func (m *CoseV001SchemaData) 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("data" + "." + "hash") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("data" + "." + "hash") + } + 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.contextValidateHash(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *CoseV001SchemaData) 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("data" + "." + "hash") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("data" + "." + "hash") + } + 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 +} + +// CoseV001SchemaDataHash Specifies the hash algorithm and value for the content +// +// swagger:model CoseV001SchemaDataHash +type CoseV001SchemaDataHash 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 hash +func (m *CoseV001SchemaDataHash) 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 coseV001SchemaDataHashTypeAlgorithmPropEnum []interface{} + +func init() { + var res []string + if err := json.Unmarshal([]byte(`["sha256"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + coseV001SchemaDataHashTypeAlgorithmPropEnum = append(coseV001SchemaDataHashTypeAlgorithmPropEnum, v) + } +} + +const ( + + // CoseV001SchemaDataHashAlgorithmSha256 captures enum value "sha256" + CoseV001SchemaDataHashAlgorithmSha256 string = "sha256" +) + +// prop value enum +func (m *CoseV001SchemaDataHash) validateAlgorithmEnum(path, location string, value string) error { + if err := validate.EnumCase(path, location, value, coseV001SchemaDataHashTypeAlgorithmPropEnum, true); err != nil { + return err + } + return nil +} + +func (m *CoseV001SchemaDataHash) validateAlgorithm(formats strfmt.Registry) error { + + if err := validate.Required("data"+"."+"hash"+"."+"algorithm", "body", m.Algorithm); err != nil { + return err + } + + // value enum + if err := m.validateAlgorithmEnum("data"+"."+"hash"+"."+"algorithm", "body", *m.Algorithm); err != nil { + return err + } + + return nil +} + +func (m *CoseV001SchemaDataHash) validateValue(formats strfmt.Registry) error { + + if err := validate.Required("data"+"."+"hash"+"."+"value", "body", m.Value); err != nil { + return err + } + + return nil +} + +// ContextValidate validates this cose v001 schema data hash based on context it is used +func (m *CoseV001SchemaDataHash) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *CoseV001SchemaDataHash) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *CoseV001SchemaDataHash) UnmarshalBinary(b []byte) error { + var res CoseV001SchemaDataHash + 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/cose/README.md b/pkg/types/cose/README.md new file mode 100644 index 000000000..d3c5d17d0 --- /dev/null +++ b/pkg/types/cose/README.md @@ -0,0 +1,7 @@ +**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. + +**How do you identify an object as an cose object?** + +The "Body" field will include an "coseObj" field. diff --git a/pkg/types/cose/cose.go b/pkg/types/cose/cose.go new file mode 100644 index 000000000..0bcf8623c --- /dev/null +++ b/pkg/types/cose/cose.go @@ -0,0 +1,73 @@ +// +// 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 cose + +import ( + "context" + + "github.com/pkg/errors" + "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, errors.Wrap(err, "fetching COSE version implementation") + } + 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..c4c66713b --- /dev/null +++ b/pkg/types/cose/cose_test.go @@ -0,0 +1,92 @@ +// +// 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 cose + +import ( + "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") + } +} 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..f9f77da13 --- /dev/null +++ b/pkg/types/cose/v0.0.1/cose_v0_0_1_schema.json @@ -0,0 +1,60 @@ +{ + "$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": { + "type": "string", + "format": "byte", + "description": "The COSE Sign1 Message" + }, + "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": { + "hash": { + "description": "Specifies the hash algorithm and value for the content", + "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 content", + "type": "string" + } + }, + "required": [ + "algorithm", + "value" + ] + }, + "content": { + "description": "Specifies the content inline within the document", + "type": "string", + "format": "byte", + "writeOnly": true + } + }, + "required": [ + "content" + ] + } + }, + "required": [ + "publicKey", + "data", + "message" + ] +} \ No newline at end of file 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..c385e262e --- /dev/null +++ b/pkg/types/cose/v0.0.1/entry.go @@ -0,0 +1,251 @@ +// +// 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 cose + +import ( + "bytes" + "context" + "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/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" + gocose "github.com/veraison/go-cose" +) + +const ( + APIVERSION = "0.0.1" +) + +func init() { + if err := cose.VersionMap.SetEntryFactory(APIVERSION, NewEntry); err != nil { + log.Logger.Panic(err) + } +} + +type V001Entry struct { + CoseObj models.CoseV001Schema + keyObj pki.PublicKey +} + +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.CoseObj.Data.Content != nil { + result = append(result, formatKey(*v.CoseObj.Data.Content)) + } + + return result, nil +} + +func formatKey(b []byte) string { + h := sha256.Sum256(b) + hash := hex.EncodeToString(h[:]) + return strings.ToLower(fmt.Sprintf("%s:%s", models.CoseV001SchemaDataHashAlgorithmSha256, 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 + } + + // Check and make sure the hash value is correct. + h := sha256.Sum256(*v.CoseObj.Message) + computedSha := hex.EncodeToString(h[:]) + + if v.CoseObj.Data.Hash != nil { + if computedSha != *v.CoseObj.Data.Hash.Value { + return errors.New("hash mismatch") + } + } else { + v.CoseObj.Data.Hash = &models.CoseV001SchemaDataHash{ + Algorithm: swag.String(models.CoseV001SchemaDataHashAlgorithmSha256), + Value: &computedSha, + } + } + 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.CoseObj.Data.Content.String())) + + canonicalEntry := models.CoseV001Schema{ + PublicKey: &pkb, + Data: &models.CoseV001SchemaData{ + Hash: &models.CoseV001SchemaDataHash{ + Algorithm: swag.String(models.CoseV001SchemaDataHashAlgorithmSha256), + Value: swag.String(hex.EncodeToString(h[:])), + }, + }, + } + + 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 + if v.CoseObj.Message == nil { + return nil + } + + pk := v.keyObj.(*x509.PublicKey) + cryptoPub := pk.CryptoPubKey() + + var alg *gocose.Algorithm + switch t := cryptoPub.(type) { + case *rsa.PublicKey: + alg = gocose.PS256 + case *ecdsa.PublicKey: + alg = gocose.ES256 + default: + return fmt.Errorf("unsupported algorithm type %T", t) + } + + bv := gocose.Verifier{ + PublicKey: cryptoPub, + Alg: alg, + } + + msg := gocose.NewSign1Message() + if err := msg.UnmarshalCBOR(*v.CoseObj.Message); err != nil { + return err + } + + return msg.Verify(*v.CoseObj.Data.Content, bv) +} + +func (v *V001Entry) Attestation() []byte { + return nil +} + +func (v V001Entry) CreateFromArtifactProperties(_ context.Context, props types.ArtifactProperties) (models.ProposedEntry, error) { + returnVal := models.Cose{} + + var err error + artifactBytes := props.ArtifactBytes + if artifactBytes == nil { + if props.ArtifactPath == nil { + return nil, errors.New("path to artifact file must be specified") + } + if props.ArtifactPath.IsAbs() { + return nil, errors.New("cose envelopes cannot be fetched over HTTP(S)") + } + artifactBytes, err = ioutil.ReadFile(filepath.Clean(props.ArtifactPath.Path)) + if err != nil { + return nil, err + } + } + publicKeyBytes := props.PublicKeyBytes + if publicKeyBytes == nil { + if props.PublicKeyPath == nil { + return nil, errors.New("public key must be provided to verify signature") + } + publicKeyBytes, err = ioutil.ReadFile(filepath.Clean(props.PublicKeyPath.Path)) + if err != nil { + return nil, fmt.Errorf("error reading public key file: %w", err) + } + } + kb := strfmt.Base64(publicKeyBytes) + ab := strfmt.Base64(artifactBytes) + + re := V001Entry{ + CoseObj: models.CoseV001Schema{ + Data: &models.CoseV001SchemaData{ + Content: &ab, + }, + PublicKey: &kb, + }, + } + + 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..72aa1f7bd --- /dev/null +++ b/pkg/types/cose/v0.0.1/entry_test.go @@ -0,0 +1,240 @@ +// +// 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 cose + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "encoding/pem" + "math/big" + "reflect" + "testing" + + "github.com/go-openapi/strfmt" + "github.com/sigstore/rekor/pkg/generated/models" + gocose "github.com/veraison/go-cose" + "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 p(b []byte) *strfmt.Base64 { + b64 := strfmt.Base64(b) + return &b64 +} + +func makeSignedCose(t *testing.T, priv crypto.PrivateKey, payload []byte) []byte { + m := gocose.NewSign1Message() + m.Headers.Protected[1] = -7 + + signer, err := gocose.NewSignerFromKey(gocose.ES256, priv) + if err != nil { + t.Fatal(err) + } + + if err := m.Sign(rand.Reader, payload, *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")) + + tests := []struct { + name string + want models.CoseV001Schema + it *models.CoseV001Schema + wantErr bool + }{ + { + name: "empty", + it: &models.CoseV001Schema{}, + wantErr: true, + }, + { + name: "missing envelope", + it: &models.CoseV001Schema{ + PublicKey: p(pub), + }, + wantErr: true, + }, + { + name: "missing envelope", + it: &models.CoseV001Schema{ + PublicKey: p([]byte("hello")), + }, + wantErr: true, + }, + { + name: "valid", + it: &models.CoseV001Schema{ + PublicKey: p(pub), + Data: &models.CoseV001SchemaData{ + Content: p([]byte("hello")), + }, + }, + wantErr: false, + }, + { + name: "cert", + it: &models.CoseV001Schema{ + PublicKey: p([]byte(pemBytes)), + Data: &models.CoseV001SchemaData{ + Content: p([]byte("hello")), + }, + }, + wantErr: false, + }, + { + name: "invalid key", + it: &models.CoseV001Schema{ + PublicKey: p([]byte("notavalidkey")), + Data: &models.CoseV001SchemaData{ + Content: p([]byte("hello")), + }, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := &V001Entry{ + CoseObj: models.CoseV001Schema{ + Message: (*strfmt.Base64)(&msg), + Data: &models.CoseV001SchemaData{ + Content: p([]byte("hello")), + }, + }, + } + tt.it.Message = (*strfmt.Base64)(&msg) + it := &models.Cose{ + Spec: tt.it, + } + var uv = func() error { + if err := v.Unmarshal(it); err != nil { + return err + } + if err := v.validate(); err != nil { + return err + } + return nil + } + if err := uv(); (err != nil) != tt.wantErr { + t.Errorf("V001Entry.Unmarshal() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestV001Entry_IndexKeys(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) + + v := V001Entry{ + CoseObj: models.CoseV001Schema{ + Message: p(msg), + Data: &models.CoseV001SchemaData{ + Content: p(rawMsg), + }, + PublicKey: p(pub), + }, + } + + 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 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) +} From 227c7784064d292efc1e23372f1609d0f1c3e6c4 Mon Sep 17 00:00:00 2001 From: Fredrik Skogman Date: Thu, 9 Jun 2022 09:02:04 +0200 Subject: [PATCH 02/14] Completed basic support for COSE records. This adds some more functionality related to COSE enveleopes. Features added are: - Support for specifying Additional Authentincated Data (AAD) - The entire CBOR envelope is stored as an attestation - If the payload type is an in-toto statement, subject is indexed What's not optimal is that the COSE envelope is using the regular `Attestion()` functionality, which means that rekor cli tries to print it during `rekor-cli get` and the response record from Rekor looks a bit awkward. Signed-off-by: Fredrik Skogman --- Makefile | 1 + cmd/rekor-cli/app/pflag_groups.go | 6 + cmd/rekor-cli/app/pflags.go | 13 + go.mod | 2 +- go.sum | 4 + pkg/generated/models/cose_v001_schema.go | 249 +++++++++++---- pkg/types/cose/cose_test.go | 76 +++++ pkg/types/cose/v0.0.1/cose_v0_0_1_schema.json | 41 ++- pkg/types/cose/v0.0.1/entry.go | 129 +++++--- pkg/types/cose/v0.0.1/entry_test.go | 294 +++++++++++++++--- pkg/types/entries.go | 17 +- pkg/types/test_util.go | 20 ++ 12 files changed, 697 insertions(+), 155 deletions(-) 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..149b6c2f0 100644 --- a/cmd/rekor-cli/app/pflag_groups.go +++ b/cmd/rekor-cli/app/pflag_groups.go @@ -87,6 +87,11 @@ func addArtifactPFlags(cmd *cobra.Command) error { "path or URL to pre-formatted entry file", false, }, + "aad": { + base64Flag, + "base64 encoded aad", + false, + }, } for flag, flagVal := range flags { @@ -152,6 +157,7 @@ func CreatePropsFromPflags() *types.ArtifactProperties { } props.PKIFormat = viper.GetString("pki-format") + props.AdditionalAuthenticatedData = viper.GetString("aad") 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/go.mod b/go.mod index 842af9b84..7e82f8e51 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,7 @@ 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 v0.0.0-20211126173600-dee3b3e54910 + 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 diff --git a/go.sum b/go.sum index b366c5700..ff1fb9cae 100644 --- a/go.sum +++ b/go.sum @@ -479,6 +479,8 @@ github.com/fullstorydev/grpcurl v1.8.6/go.mod h1:WhP7fRQdhxz2TkL97u+TCb505sxfH78 github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E= github.com/fxamacker/cbor/v2 v2.2.1-0.20200429214022-fc263b46c618 h1:RIQZGQ00xy1acO7H7mjL8N5ZDyI0soZG7X8akiXwSTo= github.com/fxamacker/cbor/v2 v2.2.1-0.20200429214022-fc263b46c618/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= +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/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= @@ -1588,6 +1590,8 @@ github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaW github.com/viki-org/dnscache v0.0.0-20130720023526-c70c1f23c5d8/go.mod h1:dniwbG03GafCjFohMDmz6Zc6oCuiqgH6tGNyXTkHzXE= github.com/veraison/go-cose v0.0.0-20211126173600-dee3b3e54910 h1:dtZjTJ/89XAZjDygdVe5X5/wnxo9gYtmKpfxGqYGbws= github.com/veraison/go-cose v0.0.0-20211126173600-dee3b3e54910/go.mod h1:sjLU/8dYHRJj3RWtKLJUbPLoByKdV7nnegaTBgQ+9XA= +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/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= diff --git a/pkg/generated/models/cose_v001_schema.go b/pkg/generated/models/cose_v001_schema.go index 61a8af5fe..caadb44d4 100644 --- a/pkg/generated/models/cose_v001_schema.go +++ b/pkg/generated/models/cose_v001_schema.go @@ -43,9 +43,8 @@ type CoseV001Schema struct { Data *CoseV001SchemaData `json:"data"` // The COSE Sign1 Message - // Required: true // Format: byte - Message *strfmt.Base64 `json:"message"` + Message strfmt.Base64 `json:"message,omitempty"` // The public key that can verify the signature // Required: true @@ -61,10 +60,6 @@ func (m *CoseV001Schema) Validate(formats strfmt.Registry) error { res = append(res, err) } - if err := m.validateMessage(formats); err != nil { - res = append(res, err) - } - if err := m.validatePublicKey(formats); err != nil { res = append(res, err) } @@ -95,15 +90,6 @@ func (m *CoseV001Schema) validateData(formats strfmt.Registry) error { return nil } -func (m *CoseV001Schema) validateMessage(formats strfmt.Registry) error { - - if err := validate.Required("message", "body", m.Message); err != nil { - return err - } - - return nil -} - func (m *CoseV001Schema) validatePublicKey(formats strfmt.Registry) error { if err := validate.Required("publicKey", "body", m.PublicKey); err != nil { @@ -166,24 +152,26 @@ func (m *CoseV001Schema) UnmarshalBinary(b []byte) error { // swagger:model CoseV001SchemaData type CoseV001SchemaData struct { - // Specifies the content inline within the document - // Required: true + // Specifies the additional authenticated data required to verify the signature // Format: byte - Content *strfmt.Base64 `json:"content"` + Aad strfmt.Base64 `json:"aad,omitempty"` - // hash - Hash *CoseV001SchemaDataHash `json:"hash,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.validateContent(formats); err != nil { + if err := m.validateEnvelopeHash(formats); err != nil { res = append(res, err) } - if err := m.validateHash(formats); err != nil { + if err := m.validatePayloadHash(formats); err != nil { res = append(res, err) } @@ -193,26 +181,36 @@ func (m *CoseV001SchemaData) Validate(formats strfmt.Registry) error { return nil } -func (m *CoseV001SchemaData) validateContent(formats strfmt.Registry) error { +func (m *CoseV001SchemaData) validateEnvelopeHash(formats strfmt.Registry) error { + if swag.IsZero(m.EnvelopeHash) { // not required + return nil + } - if err := validate.Required("data"+"."+"content", "body", m.Content); err != nil { - return err + 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) validateHash(formats strfmt.Registry) error { - if swag.IsZero(m.Hash) { // not required +func (m *CoseV001SchemaData) validatePayloadHash(formats strfmt.Registry) error { + if swag.IsZero(m.PayloadHash) { // not required return nil } - if m.Hash != nil { - if err := m.Hash.Validate(formats); err != nil { + if m.PayloadHash != nil { + if err := m.PayloadHash.Validate(formats); err != nil { if ve, ok := err.(*errors.Validation); ok { - return ve.ValidateName("data" + "." + "hash") + return ve.ValidateName("data" + "." + "payloadHash") } else if ce, ok := err.(*errors.CompositeError); ok { - return ce.ValidateName("data" + "." + "hash") + return ce.ValidateName("data" + "." + "payloadHash") } return err } @@ -225,7 +223,11 @@ func (m *CoseV001SchemaData) validateHash(formats strfmt.Registry) error { func (m *CoseV001SchemaData) ContextValidate(ctx context.Context, formats strfmt.Registry) error { var res []error - if err := m.contextValidateHash(ctx, formats); err != nil { + if err := m.contextValidateEnvelopeHash(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidatePayloadHash(ctx, formats); err != nil { res = append(res, err) } @@ -235,14 +237,30 @@ func (m *CoseV001SchemaData) ContextValidate(ctx context.Context, formats strfmt return nil } -func (m *CoseV001SchemaData) contextValidateHash(ctx context.Context, formats strfmt.Registry) error { +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.Hash != nil { - if err := m.Hash.ContextValidate(ctx, formats); err != nil { + if m.PayloadHash != nil { + if err := m.PayloadHash.ContextValidate(ctx, formats); err != nil { if ve, ok := err.(*errors.Validation); ok { - return ve.ValidateName("data" + "." + "hash") + return ve.ValidateName("data" + "." + "payloadHash") } else if ce, ok := err.(*errors.CompositeError); ok { - return ce.ValidateName("data" + "." + "hash") + return ce.ValidateName("data" + "." + "payloadHash") } return err } @@ -269,10 +287,120 @@ func (m *CoseV001SchemaData) UnmarshalBinary(b []byte) error { return nil } -// CoseV001SchemaDataHash Specifies the hash algorithm and value for the content +// 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 CoseV001SchemaDataHash -type CoseV001SchemaDataHash struct { +// swagger:model CoseV001SchemaDataPayloadHash +type CoseV001SchemaDataPayloadHash struct { // The hashing function used to compute the hash value // Required: true @@ -284,8 +412,8 @@ type CoseV001SchemaDataHash struct { Value *string `json:"value"` } -// Validate validates this cose v001 schema data hash -func (m *CoseV001SchemaDataHash) Validate(formats strfmt.Registry) error { +// 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 { @@ -302,7 +430,7 @@ func (m *CoseV001SchemaDataHash) Validate(formats strfmt.Registry) error { return nil } -var coseV001SchemaDataHashTypeAlgorithmPropEnum []interface{} +var coseV001SchemaDataPayloadHashTypeAlgorithmPropEnum []interface{} func init() { var res []string @@ -310,54 +438,59 @@ func init() { panic(err) } for _, v := range res { - coseV001SchemaDataHashTypeAlgorithmPropEnum = append(coseV001SchemaDataHashTypeAlgorithmPropEnum, v) + coseV001SchemaDataPayloadHashTypeAlgorithmPropEnum = append(coseV001SchemaDataPayloadHashTypeAlgorithmPropEnum, v) } } const ( - // CoseV001SchemaDataHashAlgorithmSha256 captures enum value "sha256" - CoseV001SchemaDataHashAlgorithmSha256 string = "sha256" + // CoseV001SchemaDataPayloadHashAlgorithmSha256 captures enum value "sha256" + CoseV001SchemaDataPayloadHashAlgorithmSha256 string = "sha256" ) // prop value enum -func (m *CoseV001SchemaDataHash) validateAlgorithmEnum(path, location string, value string) error { - if err := validate.EnumCase(path, location, value, coseV001SchemaDataHashTypeAlgorithmPropEnum, true); err != nil { +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 *CoseV001SchemaDataHash) validateAlgorithm(formats strfmt.Registry) error { +func (m *CoseV001SchemaDataPayloadHash) validateAlgorithm(formats strfmt.Registry) error { - if err := validate.Required("data"+"."+"hash"+"."+"algorithm", "body", m.Algorithm); err != nil { + if err := validate.Required("data"+"."+"payloadHash"+"."+"algorithm", "body", m.Algorithm); err != nil { return err } // value enum - if err := m.validateAlgorithmEnum("data"+"."+"hash"+"."+"algorithm", "body", *m.Algorithm); err != nil { + if err := m.validateAlgorithmEnum("data"+"."+"payloadHash"+"."+"algorithm", "body", *m.Algorithm); err != nil { return err } return nil } -func (m *CoseV001SchemaDataHash) validateValue(formats strfmt.Registry) error { +func (m *CoseV001SchemaDataPayloadHash) validateValue(formats strfmt.Registry) error { - if err := validate.Required("data"+"."+"hash"+"."+"value", "body", m.Value); err != nil { + if err := validate.Required("data"+"."+"payloadHash"+"."+"value", "body", m.Value); err != nil { return err } return nil } -// ContextValidate validates this cose v001 schema data hash based on context it is used -func (m *CoseV001SchemaDataHash) ContextValidate(ctx context.Context, formats strfmt.Registry) error { +// 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 *CoseV001SchemaDataHash) MarshalBinary() ([]byte, error) { +func (m *CoseV001SchemaDataPayloadHash) MarshalBinary() ([]byte, error) { if m == nil { return nil, nil } @@ -365,8 +498,8 @@ func (m *CoseV001SchemaDataHash) MarshalBinary() ([]byte, error) { } // UnmarshalBinary interface implementation -func (m *CoseV001SchemaDataHash) UnmarshalBinary(b []byte) error { - var res CoseV001SchemaDataHash +func (m *CoseV001SchemaDataPayloadHash) UnmarshalBinary(b []byte) error { + var res CoseV001SchemaDataPayloadHash if err := swag.ReadJSON(b, &res); err != nil { return err } diff --git a/pkg/types/cose/cose_test.go b/pkg/types/cose/cose_test.go index c4c66713b..c1af3b8ac 100644 --- a/pkg/types/cose/cose_test.go +++ b/pkg/types/cose/cose_test.go @@ -16,6 +16,7 @@ package cose import ( + "context" "errors" "testing" @@ -89,4 +90,79 @@ func TestCOSEType(t *testing.T) { 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 index f9f77da13..cf6cef62b 100644 --- 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 @@ -6,9 +6,10 @@ "type": "object", "properties": { "message": { + "description": "The COSE Sign1 Message", "type": "string", "format": "byte", - "description": "The COSE Sign1 Message" + "writeOnly": true }, "publicKey": { "description": "The public key that can verify the signature", @@ -19,9 +20,10 @@ "description": "Information about the content associated with the entry", "type": "object", "properties": { - "hash": { + "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", @@ -40,21 +42,40 @@ "value" ] }, - "content": { - "description": "Specifies the content inline within the document", + "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": [ - "content" - ] + "required": [] } }, "required": [ "publicKey", - "data", - "message" + "data" ] -} \ No newline at end of file +} diff --git a/pkg/types/cose/v0.0.1/entry.go b/pkg/types/cose/v0.0.1/entry.go index c385e262e..28a373c7c 100644 --- a/pkg/types/cose/v0.0.1/entry.go +++ b/pkg/types/cose/v0.0.1/entry.go @@ -21,6 +21,7 @@ import ( "crypto/ecdsa" "crypto/rsa" "crypto/sha256" + "encoding/base64" "encoding/hex" "encoding/json" "errors" @@ -31,6 +32,9 @@ import ( "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" @@ -38,7 +42,6 @@ import ( "github.com/sigstore/rekor/pkg/pki/x509" "github.com/sigstore/rekor/pkg/types" "github.com/sigstore/rekor/pkg/types/cose" - gocose "github.com/veraison/go-cose" ) const ( @@ -52,8 +55,10 @@ func init() { } type V001Entry struct { - CoseObj models.CoseV001Schema - keyObj pki.PublicKey + CoseObj models.CoseV001Schema + keyObj pki.PublicKey + sign1Msg *gocose.Sign1Message + envelopeHash []byte } func (v V001Entry) APIVersion() string { @@ -84,20 +89,54 @@ func (v V001Entry) IndexKeys() ([]string, error) { result = append(result, keyObj.EmailAddresses()...) // 2. Overall envelope - result = append(result, formatKey(*v.CoseObj.Message)) + result = append(result, formatKey(v.CoseObj.Message)) // 3. Payload - if v.CoseObj.Data.Content != nil { - result = append(result, formatKey(*v.CoseObj.Data.Content)) + if v.sign1Msg != nil { + result = append(result, formatKey(v.sign1Msg.Payload)) + } + + // 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.CoseV001SchemaDataHashAlgorithmSha256, hash)) + return strings.ToLower(fmt.Sprintf("%s:%s", models.CoseV001SchemaDataPayloadHashAlgorithmSha256, hash)) } func (v *V001Entry) Unmarshal(pe models.ProposedEntry) error { @@ -121,20 +160,6 @@ func (v *V001Entry) Unmarshal(pe models.ProposedEntry) error { return err } - // Check and make sure the hash value is correct. - h := sha256.Sum256(*v.CoseObj.Message) - computedSha := hex.EncodeToString(h[:]) - - if v.CoseObj.Data.Hash != nil { - if computedSha != *v.CoseObj.Data.Hash.Value { - return errors.New("hash mismatch") - } - } else { - v.CoseObj.Data.Hash = &models.CoseV001SchemaDataHash{ - Algorithm: swag.String(models.CoseV001SchemaDataHashAlgorithmSha256), - Value: &computedSha, - } - } return v.validate() } @@ -148,15 +173,19 @@ func (v *V001Entry) Canonicalize(ctx context.Context) ([]byte, error) { } pkb := strfmt.Base64(pk) - h := sha256.Sum256([]byte(v.CoseObj.Data.Content.String())) + h := sha256.Sum256([]byte(v.sign1Msg.Payload)) canonicalEntry := models.CoseV001Schema{ PublicKey: &pkb, Data: &models.CoseV001SchemaData{ - Hash: &models.CoseV001SchemaDataHash{ - Algorithm: swag.String(models.CoseV001SchemaDataHashAlgorithmSha256), + 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)), + }, }, } @@ -171,53 +200,65 @@ func (v *V001Entry) Canonicalize(ctx context.Context) ([]byte, error) { func (v *V001Entry) validate() error { // This also gets called in the CLI, where we won't have this data - if v.CoseObj.Message == nil { + if len(v.CoseObj.Message) == 0 { return nil } pk := v.keyObj.(*x509.PublicKey) cryptoPub := pk.CryptoPubKey() - var alg *gocose.Algorithm + var alg gocose.Algorithm switch t := cryptoPub.(type) { case *rsa.PublicKey: - alg = gocose.PS256 + alg = gocose.AlgorithmPS256 case *ecdsa.PublicKey: - alg = gocose.ES256 + alg = gocose.AlgorithmES256 default: return fmt.Errorf("unsupported algorithm type %T", t) } - bv := gocose.Verifier{ - PublicKey: cryptoPub, - Alg: alg, + bv, err := gocose.NewVerifier(alg, cryptoPub) + if err != nil { + return err + } + v.sign1Msg = gocose.NewSign1Message() + if err := v.sign1Msg.UnmarshalCBOR(v.CoseObj.Message); err != nil { + return err } - msg := gocose.NewSign1Message() - if err := msg.UnmarshalCBOR(*v.CoseObj.Message); err != nil { + if err := v.sign1Msg.Verify(v.CoseObj.Data.Aad, bv); err != nil { return err } - return msg.Verify(*v.CoseObj.Data.Content, bv) + // Store the envelope hash + h := sha256.Sum256(v.CoseObj.Message) + v.envelopeHash = h[:] + + return nil } func (v *V001Entry) Attestation() []byte { - return nil + 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.CoseObj.Message } func (v V001Entry) CreateFromArtifactProperties(_ context.Context, props types.ArtifactProperties) (models.ProposedEntry, error) { returnVal := models.Cose{} - var err error - artifactBytes := props.ArtifactBytes - if artifactBytes == nil { + 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)") } - artifactBytes, err = ioutil.ReadFile(filepath.Clean(props.ArtifactPath.Path)) + messageBytes, err = ioutil.ReadFile(filepath.Clean(props.ArtifactPath.Path)) if err != nil { return nil, err } @@ -232,15 +273,21 @@ func (v V001Entry) CreateFromArtifactProperties(_ context.Context, props types.A return nil, fmt.Errorf("error reading public key file: %w", err) } } + aadBytes, err := base64.StdEncoding.DecodeString(props.AdditionalAuthenticatedData) + if err != nil { + return nil, err + } kb := strfmt.Base64(publicKeyBytes) - ab := strfmt.Base64(artifactBytes) + mb := strfmt.Base64(messageBytes) + ab := strfmt.Base64(aadBytes) re := V001Entry{ CoseObj: models.CoseV001Schema{ Data: &models.CoseV001SchemaData{ - Content: &ab, + Aad: ab, }, PublicKey: &kb, + Message: mb, }, } diff --git a/pkg/types/cose/v0.0.1/entry_test.go b/pkg/types/cose/v0.0.1/entry_test.go index 72aa1f7bd..6bdb17a4d 100644 --- a/pkg/types/cose/v0.0.1/entry_test.go +++ b/pkg/types/cose/v0.0.1/entry_test.go @@ -16,7 +16,7 @@ package cose import ( - "crypto" + "bytes" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" @@ -25,11 +25,14 @@ import ( "encoding/hex" "encoding/pem" "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/spf13/viper" gocose "github.com/veraison/go-cose" "go.uber.org/goleak" ) @@ -50,16 +53,21 @@ func p(b []byte) *strfmt.Base64 { return &b64 } -func makeSignedCose(t *testing.T, priv crypto.PrivateKey, payload []byte) []byte { +func makeSignedCose(t *testing.T, priv *ecdsa.PrivateKey, payload, aad []byte, contentType interface{}) []byte { m := gocose.NewSign1Message() - m.Headers.Protected[1] = -7 + m.Payload = payload + m.Headers.Protected[gocose.HeaderLabelAlgorithm] = gocose.AlgorithmES256 - signer, err := gocose.NewSignerFromKey(gocose.ES256, priv) + 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, payload, *signer); err != nil { + if err := m.Sign(rand.Reader, aad, signer); err != nil { t.Fatal(err) } @@ -96,7 +104,8 @@ func TestV001Entry_Unmarshal(t *testing.T) { Bytes: caBytes, }) - msg := makeSignedCose(t, priv, []byte("hello")) + msg := makeSignedCose(t, priv, []byte("hello"), nil, "") + msgWithAAD := makeSignedCose(t, priv, []byte("hello"), []byte("external aad"), "") tests := []struct { name string @@ -110,7 +119,7 @@ func TestV001Entry_Unmarshal(t *testing.T) { wantErr: true, }, { - name: "missing envelope", + name: "missing data", it: &models.CoseV001Schema{ PublicKey: p(pub), }, @@ -119,6 +128,7 @@ func TestV001Entry_Unmarshal(t *testing.T) { { name: "missing envelope", it: &models.CoseV001Schema{ + Data: &models.CoseV001SchemaData{}, PublicKey: p([]byte("hello")), }, wantErr: true, @@ -126,30 +136,58 @@ func TestV001Entry_Unmarshal(t *testing.T) { { 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{ - Content: p([]byte("hello")), + Aad: strfmt.Base64("external aad"), }, + PublicKey: p(pub), + Message: msgWithAAD, }, wantErr: false, }, { - name: "cert", + name: "extra aad", it: &models.CoseV001Schema{ - PublicKey: p([]byte(pemBytes)), Data: &models.CoseV001SchemaData{ - Content: p([]byte("hello")), + 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")), - Data: &models.CoseV001SchemaData{ - Content: p([]byte("hello")), - }, + Message: []byte("hello"), }, wantErr: true, }, @@ -158,27 +196,12 @@ func TestV001Entry_Unmarshal(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { v := &V001Entry{ - CoseObj: models.CoseV001Schema{ - Message: (*strfmt.Base64)(&msg), - Data: &models.CoseV001SchemaData{ - Content: p([]byte("hello")), - }, - }, + CoseObj: models.CoseV001Schema{}, } - tt.it.Message = (*strfmt.Base64)(&msg) it := &models.Cose{ Spec: tt.it, } - var uv = func() error { - if err := v.Unmarshal(it); err != nil { - return err - } - if err := v.validate(); err != nil { - return err - } - return nil - } - if err := uv(); (err != nil) != tt.wantErr { + if err := v.Unmarshal(it); (err != nil) != tt.wantErr { t.Errorf("V001Entry.Unmarshal() error = %v, wantErr %v", err, tt.wantErr) } }) @@ -186,7 +209,83 @@ func TestV001Entry_Unmarshal(t *testing.T) { } 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) @@ -201,19 +300,79 @@ func TestV001Entry_IndexKeys(t *testing.T) { Type: "PUBLIC KEY", }) - rawMsg := []byte("hello") - msg := makeSignedCose(t, priv, rawMsg) + 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: p(msg), - Data: &models.CoseV001SchemaData{ - Content: p(rawMsg), - }, + 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) @@ -230,6 +389,67 @@ func TestV001Entry_IndexKeys(t *testing.T) { 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) + } + att := v.Attestation() + 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() + v := &V001Entry{ + CoseObj: models.CoseV001Schema{}, + } + if err := v.Unmarshal(it); err != nil { + t.Errorf("V001Entry.Unmarshal() error = %v", err) + } + att := v.Attestation() + if len(att) != len(msg) { + t.Errorf("Wrong attestation returned") + } + for i := range att { + if att[i] != msg[i] { + t.Errorf("Wrong attestation returned") + return + } + } + }) +} + func mustContain(t *testing.T, want string, l []string) { for _, s := range l { if s == want { diff --git a/pkg/types/entries.go b/pkg/types/entries.go index 8f71bc720..11a8f6f8b 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 string + 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 +} From 496d5e7a5c90374b89e9084c3cd42298a7d834f6 Mon Sep 17 00:00:00 2001 From: Fredrik Skogman Date: Thu, 9 Jun 2022 10:13:41 +0200 Subject: [PATCH 03/14] Updated the documentation for COSE envelopes. Signed-off-by: Fredrik Skogman --- pkg/types/README.md | 2 ++ pkg/types/cose/README.md | 31 ++++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) 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 index d3c5d17d0..64550686a 100644 --- a/pkg/types/cose/README.md +++ b/pkg/types/cose/README.md @@ -1,7 +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. +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`. From fe8f53b0f49547a3302d1fd4ce64311312c9ec2f Mon Sep 17 00:00:00 2001 From: Fredrik Skogman Date: Fri, 10 Jun 2022 09:44:24 +0200 Subject: [PATCH 04/14] Resolved merge conflicts with main. The biggest change is adapting the new interface where attestation func is split to two, one to get the key and a nother to get key/val. Signed-off-by: Fredrik Skogman --- go.mod | 1 + go.sum | 1 - pkg/types/cose/v0.0.1/entry.go | 44 +++++++++++++++++++++++------ pkg/types/cose/v0.0.1/entry_test.go | 22 ++++++++++++--- 4 files changed, 55 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index 7e82f8e51..69c5e880a 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 github.com/prometheus/client_golang v1.12.2 github.com/rs/cors v1.8.2 github.com/sassoftware/relic v0.0.0-20210427151427-dfb082b79b74 diff --git a/go.sum b/go.sum index ff1fb9cae..f2e38ca5f 100644 --- a/go.sum +++ b/go.sum @@ -474,7 +474,6 @@ github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmV github.com/fullstorydev/grpcurl v1.6.0/go.mod h1:ZQ+ayqbKMJNhzLmbpCiurTVlaK2M/3nqZCxaQ2Ze/sM= 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.2/go.mod h1:YvWNT3xRp2KIRuvCphFodG0fKkMXwaxA9CJgKCcyzUQ= github.com/fullstorydev/grpcurl v1.8.6/go.mod h1:WhP7fRQdhxz2TkL97u+TCb505sxfH78W1usyoB3tepw= github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E= github.com/fxamacker/cbor/v2 v2.2.1-0.20200429214022-fc263b46c618 h1:RIQZGQ00xy1acO7H7mjL8N5ZDyI0soZG7X8akiXwSTo= diff --git a/pkg/types/cose/v0.0.1/entry.go b/pkg/types/cose/v0.0.1/entry.go index 28a373c7c..9b009ba83 100644 --- a/pkg/types/cose/v0.0.1/entry.go +++ b/pkg/types/cose/v0.0.1/entry.go @@ -94,6 +94,10 @@ func (v V001Entry) IndexKeys() ([]string, error) { // 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. @@ -160,6 +164,23 @@ func (v *V001Entry) Unmarshal(pe models.ProposedEntry) error { 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() } @@ -198,8 +219,9 @@ func (v *V001Entry) Canonicalize(ctx context.Context) ([]byte, error) { // 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 } @@ -230,21 +252,27 @@ func (v *V001Entry) validate() error { return err } - // Store the envelope hash - h := sha256.Sum256(v.CoseObj.Message) - v.envelopeHash = h[:] - return nil } -func (v *V001Entry) Attestation() []byte { +// 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 "", nil } - return v.CoseObj.Message + return v.AttestationKey(), v.CoseObj.Message } func (v V001Entry) CreateFromArtifactProperties(_ context.Context, props types.ArtifactProperties) (models.ProposedEntry, error) { diff --git a/pkg/types/cose/v0.0.1/entry_test.go b/pkg/types/cose/v0.0.1/entry_test.go index 6bdb17a4d..8f437d17f 100644 --- a/pkg/types/cose/v0.0.1/entry_test.go +++ b/pkg/types/cose/v0.0.1/entry_test.go @@ -24,6 +24,7 @@ import ( "crypto/x509" "encoding/hex" "encoding/pem" + "fmt" "math/big" "os" "reflect" @@ -420,7 +421,10 @@ func TestV001Entry_Attestation(t *testing.T) { if err := v.Unmarshal(it); err != nil { t.Errorf("V001Entry.Unmarshal() error = %v", err) } - att := v.Attestation() + key, att := v.AttestationKeyValue() + if key != "" { + t.Errorf("Unexpected key returned") + } if len(att) != 0 { t.Errorf("Attestation returned") } @@ -431,19 +435,29 @@ func TestV001Entry_Attestation(t *testing.T) { // 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) } - att := v.Attestation() + key, att := v.AttestationKeyValue() + if key != wantKey { + t.Errorf("unexpected attestation key: %s want: %s", + key, wantKey) + } + if len(att) != len(msg) { - t.Errorf("Wrong attestation returned") + t.Error("Wrong attestation returned") } for i := range att { if att[i] != msg[i] { - t.Errorf("Wrong attestation returned") + t.Error("Wrong attestation returned") return } } From 8ed2c9896bfe7bea0a58250ff36fe19163065004 Mon Sep 17 00:00:00 2001 From: Fredrik Skogman Date: Fri, 10 Jun 2022 09:58:48 +0200 Subject: [PATCH 05/14] Ran go mod tidy after resolving merge committs. Signed-off-by: Fredrik Skogman --- go.mod | 4 ++-- go.sum | 17 ++--------------- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index 69c5e880a..b35c4737c 100644 --- a/go.mod +++ b/go.mod @@ -69,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 @@ -98,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 @@ -111,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 f2e38ca5f..26caa8130 100644 --- a/go.sum +++ b/go.sum @@ -475,12 +475,9 @@ 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/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E= -github.com/fxamacker/cbor/v2 v2.2.1-0.20200429214022-fc263b46c618 h1:RIQZGQ00xy1acO7H7mjL8N5ZDyI0soZG7X8akiXwSTo= -github.com/fxamacker/cbor/v2 v2.2.1-0.20200429214022-fc263b46c618/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= 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/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= +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= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -1586,25 +1583,15 @@ 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/viki-org/dnscache v0.0.0-20130720023526-c70c1f23c5d8/go.mod h1:dniwbG03GafCjFohMDmz6Zc6oCuiqgH6tGNyXTkHzXE= -github.com/veraison/go-cose v0.0.0-20211126173600-dee3b3e54910 h1:dtZjTJ/89XAZjDygdVe5X5/wnxo9gYtmKpfxGqYGbws= -github.com/veraison/go-cose v0.0.0-20211126173600-dee3b3e54910/go.mod h1:sjLU/8dYHRJj3RWtKLJUbPLoByKdV7nnegaTBgQ+9XA= 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/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= -github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= -github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= -github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= -github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= -github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +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= github.com/vmihailenco/tagparser v0.1.1 h1:quXMXlA39OCbd2wAdTsGDlK9RkOk6Wuw+x37wVyIuWY= 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/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= -github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= 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= From a6ec0b7619aecba2f802fb4c9a9e3a251401b420 Mon Sep 17 00:00:00 2001 From: Fredrik Skogman Date: Mon, 13 Jun 2022 12:39:29 +0200 Subject: [PATCH 06/14] Added check to see that provided EC key uses the P256 curve. Signed-off-by: Fredrik Skogman --- pkg/types/cose/v0.0.1/entry.go | 48 ++++++--- pkg/types/cose/v0.0.1/entry_test.go | 149 ++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+), 12 deletions(-) diff --git a/pkg/types/cose/v0.0.1/entry.go b/pkg/types/cose/v0.0.1/entry.go index 9b009ba83..f4afe2084 100644 --- a/pkg/types/cose/v0.0.1/entry.go +++ b/pkg/types/cose/v0.0.1/entry.go @@ -18,6 +18,7 @@ package cose import ( "bytes" "context" + "crypto" "crypto/ecdsa" "crypto/rsa" "crypto/sha256" @@ -48,6 +49,10 @@ const ( APIVERSION = "0.0.1" ) +const ( + CurveP256 = "P-256" +) + func init() { if err := cose.VersionMap.SetEntryFactory(APIVERSION, NewEntry); err != nil { log.Logger.Panic(err) @@ -226,20 +231,12 @@ func (v *V001Entry) validate() error { return nil } - pk := v.keyObj.(*x509.PublicKey) - cryptoPub := pk.CryptoPubKey() - - var alg gocose.Algorithm - switch t := cryptoPub.(type) { - case *rsa.PublicKey: - alg = gocose.AlgorithmPS256 - case *ecdsa.PublicKey: - alg = gocose.AlgorithmES256 - default: - return fmt.Errorf("unsupported algorithm type %T", t) + alg, pk, err := getPublicKey(v.keyObj) + if err != nil { + return nil } - bv, err := gocose.NewVerifier(alg, cryptoPub) + bv, err := gocose.NewVerifier(alg, pk) if err != nil { return err } @@ -255,6 +252,33 @@ func (v *V001Entry) validate() error { 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 { diff --git a/pkg/types/cose/v0.0.1/entry_test.go b/pkg/types/cose/v0.0.1/entry_test.go index 8f437d17f..6bc19731c 100644 --- a/pkg/types/cose/v0.0.1/entry_test.go +++ b/pkg/types/cose/v0.0.1/entry_test.go @@ -38,6 +38,51 @@ import ( "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) } @@ -464,6 +509,110 @@ func TestV001Entry_Attestation(t *testing.T) { }) } +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 mustContain(t *testing.T, want string, l []string) { for _, s := range l { if s == want { From 4ef4d7822043c34c3c5081e837ea66b5e2018899 Mon Sep 17 00:00:00 2001 From: Fredrik Skogman Date: Tue, 14 Jun 2022 09:43:33 +0200 Subject: [PATCH 07/14] Spelled out aad when printing the help message. Signed-off-by: Fredrik Skogman --- cmd/rekor-cli/app/pflag_groups.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/rekor-cli/app/pflag_groups.go b/cmd/rekor-cli/app/pflag_groups.go index 149b6c2f0..553ddd7d1 100644 --- a/cmd/rekor-cli/app/pflag_groups.go +++ b/cmd/rekor-cli/app/pflag_groups.go @@ -89,7 +89,7 @@ func addArtifactPFlags(cmd *cobra.Command) error { }, "aad": { base64Flag, - "base64 encoded aad", + "base64 encoded additional authenticated data", false, }, } From c6c2de730172c7ca364ac0d236f7c5ee1900a0a9 Mon Sep 17 00:00:00 2001 From: Fredrik Skogman Date: Tue, 14 Jun 2022 09:45:59 +0200 Subject: [PATCH 08/14] Updated copyright notice to have current (2022) year. Signed-off-by: Fredrik Skogman --- pkg/types/cose/cose.go | 2 +- pkg/types/cose/cose_test.go | 2 +- pkg/types/cose/v0.0.1/entry.go | 2 +- pkg/types/cose/v0.0.1/entry_test.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/types/cose/cose.go b/pkg/types/cose/cose.go index 0bcf8623c..7cc438bf2 100644 --- a/pkg/types/cose/cose.go +++ b/pkg/types/cose/cose.go @@ -1,5 +1,5 @@ // -// Copyright 2021 The Sigstore Authors. +// 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. diff --git a/pkg/types/cose/cose_test.go b/pkg/types/cose/cose_test.go index c1af3b8ac..04290ec46 100644 --- a/pkg/types/cose/cose_test.go +++ b/pkg/types/cose/cose_test.go @@ -1,5 +1,5 @@ // -// Copyright 2021 The Sigstore Authors. +// 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. diff --git a/pkg/types/cose/v0.0.1/entry.go b/pkg/types/cose/v0.0.1/entry.go index f4afe2084..87654cf75 100644 --- a/pkg/types/cose/v0.0.1/entry.go +++ b/pkg/types/cose/v0.0.1/entry.go @@ -1,5 +1,5 @@ // -// Copyright 2021 The Sigstore Authors. +// 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. diff --git a/pkg/types/cose/v0.0.1/entry_test.go b/pkg/types/cose/v0.0.1/entry_test.go index 6bc19731c..dafc429e5 100644 --- a/pkg/types/cose/v0.0.1/entry_test.go +++ b/pkg/types/cose/v0.0.1/entry_test.go @@ -1,5 +1,5 @@ // -// Copyright 2021 The Sigstore Authors. +// 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. From 7a393eb47236fdab3a6ea40846cf98945a2f88b2 Mon Sep 17 00:00:00 2001 From: Fredrik Skogman Date: Tue, 14 Jun 2022 09:56:27 +0200 Subject: [PATCH 09/14] Removed direct dependency on github.com/pkg/errors and replaced with stdlib errors package. Signed-off-by: Fredrik Skogman --- go.mod | 2 +- pkg/types/cose/cose.go | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b35c4737c..ef5bea61d 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +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 + 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 diff --git a/pkg/types/cose/cose.go b/pkg/types/cose/cose.go index 7cc438bf2..dcdc072a3 100644 --- a/pkg/types/cose/cose.go +++ b/pkg/types/cose/cose.go @@ -17,8 +17,9 @@ package cose import ( "context" + "errors" + "fmt" - "github.com/pkg/errors" "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/rekor/pkg/types" ) @@ -63,7 +64,7 @@ func (it *BaseCOSEType) CreateProposedEntry(ctx context.Context, version string, } ei, err := it.VersionedUnmarshal(nil, version) if err != nil { - return nil, errors.Wrap(err, "fetching COSE version implementation") + return nil, fmt.Errorf("fetching COSE version implementation: %w", err) } return ei.CreateFromArtifactProperties(ctx, props) } From afd7ace743e918d8752776fc7c72ddc1c9f2fcbf Mon Sep 17 00:00:00 2001 From: Fredrik Skogman Date: Tue, 14 Jun 2022 11:19:26 +0200 Subject: [PATCH 10/14] Fixed a bug where nil was wrongfully returned instead of err. Also increas the general test coverage. Signed-off-by: Fredrik Skogman --- cmd/rekor-cli/app/pflags_test.go | 21 ++++++++++++++++ pkg/types/cose/v0.0.1/entry.go | 2 +- pkg/types/cose/v0.0.1/entry_test.go | 38 +++++++++++++++++++++++++++++ tests/test_cose.cbor | 1 + tests/test_cose.pub | 4 +++ 5 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 tests/test_cose.cbor create mode 100644 tests/test_cose.pub diff --git a/cmd/rekor-cli/app/pflags_test.go b/cmd/rekor-cli/app/pflags_test.go index 625c74928..667e7964e 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,23 @@ 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, missing aad", + typeStr: "cose", + artifact: "../../../tests/test_cose.cbor", + publicKey: "../../../tests/test_cose.pub", + expectParseSuccess: true, + expectValidateSuccess: false, + }, } for _, tc := range tests { @@ -384,6 +402,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/pkg/types/cose/v0.0.1/entry.go b/pkg/types/cose/v0.0.1/entry.go index 87654cf75..dc38510cb 100644 --- a/pkg/types/cose/v0.0.1/entry.go +++ b/pkg/types/cose/v0.0.1/entry.go @@ -233,7 +233,7 @@ func (v *V001Entry) validate() error { alg, pk, err := getPublicKey(v.keyObj) if err != nil { - return nil + return err } bv, err := gocose.NewVerifier(alg, pk) diff --git a/pkg/types/cose/v0.0.1/entry_test.go b/pkg/types/cose/v0.0.1/entry_test.go index dafc429e5..62a6498d7 100644 --- a/pkg/types/cose/v0.0.1/entry_test.go +++ b/pkg/types/cose/v0.0.1/entry_test.go @@ -33,6 +33,7 @@ import ( "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" @@ -252,6 +253,18 @@ func TestV001Entry_Unmarshal(t *testing.T) { } }) } + + 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) { @@ -613,6 +626,31 @@ func TestGetPublicKey(t *testing.T) { } +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 { 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`5dgG{;Q|R+s SrÁAZc \ 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----- From 4c65fcc9df5e3f531a835f46da72367f29df6ed0 Mon Sep 17 00:00:00 2001 From: Fredrik Skogman Date: Tue, 14 Jun 2022 13:02:03 +0200 Subject: [PATCH 11/14] Changed aad in artifact properties struct to be []byte instead of string. This gives the caller the possibility to decide how to decode the data. Signed-off-by: Fredrik Skogman --- cmd/rekor-cli/app/pflag_groups.go | 6 +++++- pkg/types/cose/v0.0.1/entry.go | 5 +---- pkg/types/entries.go | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/cmd/rekor-cli/app/pflag_groups.go b/cmd/rekor-cli/app/pflag_groups.go index 553ddd7d1..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" @@ -157,7 +158,10 @@ func CreatePropsFromPflags() *types.ArtifactProperties { } props.PKIFormat = viper.GetString("pki-format") - props.AdditionalAuthenticatedData = viper.GetString("aad") + b64aad := viper.GetString("aad") + if b64aad != "" { + props.AdditionalAuthenticatedData, _ = base64.StdEncoding.DecodeString(b64aad) + } return props } diff --git a/pkg/types/cose/v0.0.1/entry.go b/pkg/types/cose/v0.0.1/entry.go index dc38510cb..1979a1716 100644 --- a/pkg/types/cose/v0.0.1/entry.go +++ b/pkg/types/cose/v0.0.1/entry.go @@ -22,7 +22,6 @@ import ( "crypto/ecdsa" "crypto/rsa" "crypto/sha256" - "encoding/base64" "encoding/hex" "encoding/json" "errors" @@ -325,18 +324,16 @@ func (v V001Entry) CreateFromArtifactProperties(_ context.Context, props types.A return nil, fmt.Errorf("error reading public key file: %w", err) } } - aadBytes, err := base64.StdEncoding.DecodeString(props.AdditionalAuthenticatedData) if err != nil { return nil, err } kb := strfmt.Base64(publicKeyBytes) mb := strfmt.Base64(messageBytes) - ab := strfmt.Base64(aadBytes) re := V001Entry{ CoseObj: models.CoseV001Schema{ Data: &models.CoseV001SchemaData{ - Aad: ab, + Aad: props.AdditionalAuthenticatedData, }, PublicKey: &kb, Message: mb, diff --git a/pkg/types/entries.go b/pkg/types/entries.go index 11a8f6f8b..65e7ff6c9 100644 --- a/pkg/types/entries.go +++ b/pkg/types/entries.go @@ -121,7 +121,7 @@ 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 { - AdditionalAuthenticatedData string + AdditionalAuthenticatedData []byte ArtifactPath *url.URL ArtifactHash string ArtifactBytes []byte From 19834df2ef7bc012ab251eb3faabbb626f831c96 Mon Sep 17 00:00:00 2001 From: Fredrik Skogman Date: Tue, 14 Jun 2022 13:17:57 +0200 Subject: [PATCH 12/14] Fixed a warning from the linter. Signed-off-by: Fredrik Skogman --- pkg/types/cose/v0.0.1/entry_test.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pkg/types/cose/v0.0.1/entry_test.go b/pkg/types/cose/v0.0.1/entry_test.go index 62a6498d7..77a030d2a 100644 --- a/pkg/types/cose/v0.0.1/entry_test.go +++ b/pkg/types/cose/v0.0.1/entry_test.go @@ -259,10 +259,8 @@ func TestV001Entry_Unmarshal(t *testing.T) { 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()) - } + } else if err.Error() != want { + t.Errorf("wrong error: %s", err.Error()) } }) } From 14d83fb7f0e4550cf7777be9b48937626a7d0a53 Mon Sep 17 00:00:00 2001 From: Fredrik Skogman Date: Tue, 14 Jun 2022 13:44:37 +0200 Subject: [PATCH 13/14] Added test case for malformed base64 aad parameter. Signed-off-by: Fredrik Skogman --- cmd/rekor-cli/app/pflags_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cmd/rekor-cli/app/pflags_test.go b/cmd/rekor-cli/app/pflags_test.go index 667e7964e..6b1faa07b 100644 --- a/cmd/rekor-cli/app/pflags_test.go +++ b/cmd/rekor-cli/app/pflags_test.go @@ -356,6 +356,15 @@ func TestArtifactPFlags(t *testing.T) { 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", From 9e8f8696c37a3b901e013e0cdf00db66aaaa14d4 Mon Sep 17 00:00:00 2001 From: Fredrik Skogman Date: Tue, 14 Jun 2022 13:46:13 +0200 Subject: [PATCH 14/14] During signature validation, do not store any state until entire validation is done. Signed-off-by: Fredrik Skogman --- pkg/types/cose/v0.0.1/entry.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/types/cose/v0.0.1/entry.go b/pkg/types/cose/v0.0.1/entry.go index 1979a1716..55dcf3008 100644 --- a/pkg/types/cose/v0.0.1/entry.go +++ b/pkg/types/cose/v0.0.1/entry.go @@ -239,15 +239,16 @@ func (v *V001Entry) validate() error { if err != nil { return err } - v.sign1Msg = gocose.NewSign1Message() - if err := v.sign1Msg.UnmarshalCBOR(v.CoseObj.Message); err != nil { + sign1Msg := gocose.NewSign1Message() + if err := sign1Msg.UnmarshalCBOR(v.CoseObj.Message); err != nil { return err } - if err := v.sign1Msg.Verify(v.CoseObj.Data.Aad, bv); err != nil { + if err := sign1Msg.Verify(v.CoseObj.Data.Aad, bv); err != nil { return err } + v.sign1Msg = sign1Msg return nil }