diff --git a/cmd/rekor-cli/app/root.go b/cmd/rekor-cli/app/root.go index a70632f0d..7dc211fa1 100644 --- a/cmd/rekor-cli/app/root.go +++ b/cmd/rekor-cli/app/root.go @@ -30,6 +30,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/dsse/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 768617fe6..b4504ea4b 100644 --- a/cmd/rekor-server/app/serve.go +++ b/cmd/rekor-server/app/serve.go @@ -30,6 +30,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/dsse" + dsse_v001 "github.com/sigstore/rekor/pkg/types/dsse/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" @@ -91,6 +93,7 @@ var serveCmd = &cobra.Command{ helm.KIND: helm_v001.APIVERSION, tuf.KIND: tuf_v001.APIVERSION, hashedrekord.KIND: hashedrekord_v001.APIVERSION, + dsse.KIND: dsse_v001.APIVERSION, } for k, v := range pluggableTypeMap { diff --git a/openapi.yaml b/openapi.yaml index 05e55dd62..9d0c6419a 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -444,6 +444,23 @@ definitions: - spec additionalProperties: false + dsse: + type: object + description: DSSE 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/dsse/dsse_schema.json' + required: + - apiVersion + - spec + additionalProperties: false + rfc3161: type: object description: RFC3161 Timestamp diff --git a/pkg/generated/models/dsse.go b/pkg/generated/models/dsse.go new file mode 100644 index 000000000..431e74b97 --- /dev/null +++ b/pkg/generated/models/dsse.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" +) + +// Dsse DSSE object +// +// swagger:model dsse +type Dsse 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 DsseSchema `json:"spec"` +} + +// Kind gets the kind of this subtype +func (m *Dsse) Kind() string { + return "dsse" +} + +// SetKind sets the kind of this subtype +func (m *Dsse) SetKind(val string) { +} + +// UnmarshalJSON unmarshals this object with a polymorphic type from a JSON structure +func (m *Dsse) 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 DsseSchema `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 Dsse + + 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 Dsse) 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 DsseSchema `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 dsse +func (m *Dsse) 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 *Dsse) 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 *Dsse) validateSpec(formats strfmt.Registry) error { + + if m.Spec == nil { + return errors.Required("spec", "body", nil) + } + + return nil +} + +// ContextValidate validate this dsse based on the context it is used +func (m *Dsse) 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 *Dsse) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *Dsse) UnmarshalBinary(b []byte) error { + var res Dsse + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/pkg/generated/models/dsse_schema.go b/pkg/generated/models/dsse_schema.go new file mode 100644 index 000000000..fcb71c86c --- /dev/null +++ b/pkg/generated/models/dsse_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 + +// DsseSchema DSSE Schema +// +// DSSE for Rekord objects +// +// swagger:model dsseSchema +type DsseSchema interface{} diff --git a/pkg/generated/models/dsse_v001_schema.go b/pkg/generated/models/dsse_v001_schema.go new file mode 100644 index 000000000..e0153ee64 --- /dev/null +++ b/pkg/generated/models/dsse_v001_schema.go @@ -0,0 +1,372 @@ +// 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" + "strconv" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// DsseV001Schema dsse v0.0.1 Schema +// +// Schema for dsse object +// +// swagger:model dsseV001Schema +type DsseV001Schema struct { + + // payload of the envelope + // Format: byte + Payload strfmt.Base64 `json:"payload,omitempty"` + + // payload hash + // Required: true + PayloadHash *DsseV001SchemaPayloadHash `json:"payloadHash"` + + // type descriping the payload + // Required: true + PayloadType *string `json:"payloadType"` + + // collection of all signatures of the envelope's payload + // Required: true + // Min Items: 1 + Signatures []*DsseV001SchemaSignaturesItems0 `json:"signatures"` +} + +// Validate validates this dsse v001 schema +func (m *DsseV001Schema) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validatePayloadHash(formats); err != nil { + res = append(res, err) + } + + if err := m.validatePayloadType(formats); err != nil { + res = append(res, err) + } + + if err := m.validateSignatures(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *DsseV001Schema) validatePayloadHash(formats strfmt.Registry) error { + + if err := validate.Required("payloadHash", "body", m.PayloadHash); err != nil { + return err + } + + if m.PayloadHash != nil { + if err := m.PayloadHash.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("payloadHash") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("payloadHash") + } + return err + } + } + + return nil +} + +func (m *DsseV001Schema) validatePayloadType(formats strfmt.Registry) error { + + if err := validate.Required("payloadType", "body", m.PayloadType); err != nil { + return err + } + + return nil +} + +func (m *DsseV001Schema) validateSignatures(formats strfmt.Registry) error { + + if err := validate.Required("signatures", "body", m.Signatures); err != nil { + return err + } + + iSignaturesSize := int64(len(m.Signatures)) + + if err := validate.MinItems("signatures", "body", iSignaturesSize, 1); err != nil { + return err + } + + for i := 0; i < len(m.Signatures); i++ { + if swag.IsZero(m.Signatures[i]) { // not required + continue + } + + if m.Signatures[i] != nil { + if err := m.Signatures[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("signatures" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("signatures" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// ContextValidate validate this dsse v001 schema based on the context it is used +func (m *DsseV001Schema) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidatePayloadHash(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateSignatures(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *DsseV001Schema) contextValidatePayloadHash(ctx context.Context, formats strfmt.Registry) error { + + if m.PayloadHash != nil { + if err := m.PayloadHash.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("payloadHash") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("payloadHash") + } + return err + } + } + + return nil +} + +func (m *DsseV001Schema) contextValidateSignatures(ctx context.Context, formats strfmt.Registry) error { + + for i := 0; i < len(m.Signatures); i++ { + + if m.Signatures[i] != nil { + if err := m.Signatures[i].ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("signatures" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("signatures" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// MarshalBinary interface implementation +func (m *DsseV001Schema) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *DsseV001Schema) UnmarshalBinary(b []byte) error { + var res DsseV001Schema + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} + +// DsseV001SchemaPayloadHash hash of the envelope's payload after being PAE encoded +// +// swagger:model DsseV001SchemaPayloadHash +type DsseV001SchemaPayloadHash struct { + + // The hasing function used to compue the hash value + // Enum: [sha256] + Algorithm string `json:"algorithm,omitempty"` + + // The hash value of the PAE encoded payload + Value string `json:"value,omitempty"` +} + +// Validate validates this dsse v001 schema payload hash +func (m *DsseV001SchemaPayloadHash) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateAlgorithm(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +var dsseV001SchemaPayloadHashTypeAlgorithmPropEnum []interface{} + +func init() { + var res []string + if err := json.Unmarshal([]byte(`["sha256"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + dsseV001SchemaPayloadHashTypeAlgorithmPropEnum = append(dsseV001SchemaPayloadHashTypeAlgorithmPropEnum, v) + } +} + +const ( + + // DsseV001SchemaPayloadHashAlgorithmSha256 captures enum value "sha256" + DsseV001SchemaPayloadHashAlgorithmSha256 string = "sha256" +) + +// prop value enum +func (m *DsseV001SchemaPayloadHash) validateAlgorithmEnum(path, location string, value string) error { + if err := validate.EnumCase(path, location, value, dsseV001SchemaPayloadHashTypeAlgorithmPropEnum, true); err != nil { + return err + } + return nil +} + +func (m *DsseV001SchemaPayloadHash) validateAlgorithm(formats strfmt.Registry) error { + if swag.IsZero(m.Algorithm) { // not required + return nil + } + + // value enum + if err := m.validateAlgorithmEnum("payloadHash"+"."+"algorithm", "body", m.Algorithm); err != nil { + return err + } + + return nil +} + +// ContextValidate validate this dsse v001 schema payload hash based on the context it is used +func (m *DsseV001SchemaPayloadHash) 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 *DsseV001SchemaPayloadHash) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *DsseV001SchemaPayloadHash) UnmarshalBinary(b []byte) error { + var res DsseV001SchemaPayloadHash + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} + +// DsseV001SchemaSignaturesItems0 a signature of the envelope's payload along with the public key for the signature +// +// swagger:model DsseV001SchemaSignaturesItems0 +type DsseV001SchemaSignaturesItems0 struct { + + // optional id of the key used to create the signature + Keyid string `json:"keyid,omitempty"` + + // public key that corresponds to this signature + // Read Only: true + // Format: byte + PublicKey strfmt.Base64 `json:"publicKey,omitempty"` + + // signature of the payload + // Format: byte + Sig strfmt.Base64 `json:"sig,omitempty"` +} + +// Validate validates this dsse v001 schema signatures items0 +func (m *DsseV001SchemaSignaturesItems0) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validate this dsse v001 schema signatures items0 based on the context it is used +func (m *DsseV001SchemaSignaturesItems0) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidatePublicKey(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *DsseV001SchemaSignaturesItems0) contextValidatePublicKey(ctx context.Context, formats strfmt.Registry) error { + + if err := validate.ReadOnly(ctx, "publicKey", "body", strfmt.Base64(m.PublicKey)); err != nil { + return err + } + + return nil +} + +// MarshalBinary interface implementation +func (m *DsseV001SchemaSignaturesItems0) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *DsseV001SchemaSignaturesItems0) UnmarshalBinary(b []byte) error { + var res DsseV001SchemaSignaturesItems0 + 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..53394c62e 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 "dsse": + var result Dsse + 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/generated/restapi/embedded_spec.go b/pkg/generated/restapi/embedded_spec.go index cdb7efd66..a684e860d 100644 --- a/pkg/generated/restapi/embedded_spec.go +++ b/pkg/generated/restapi/embedded_spec.go @@ -704,6 +704,32 @@ func init() { } ] }, + "dsse": { + "description": "DSSE object", + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/ProposedEntry" + }, + { + "required": [ + "apiVersion", + "spec" + ], + "properties": { + "apiVersion": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" + }, + "spec": { + "type": "object", + "$ref": "pkg/types/dsse/dsse_schema.json" + } + }, + "additionalProperties": false + } + ] + }, "hashedrekord": { "description": "Hashed Rekord object", "type": "object", @@ -1535,6 +1561,45 @@ func init() { } } }, + "DsseV001SchemaPayloadHash": { + "description": "hash of the envelope's payload after being PAE encoded", + "type": "object", + "properties": { + "algorithm": { + "description": "The hasing function used to compue the hash value", + "type": "string", + "enum": [ + "sha256" + ] + }, + "value": { + "description": "The hash value of the PAE encoded payload", + "type": "string" + } + }, + "readOnly": true + }, + "DsseV001SchemaSignaturesItems0": { + "description": "a signature of the envelope's payload along with the public key for the signature", + "type": "object", + "properties": { + "keyid": { + "description": "optional id of the key used to create the signature", + "type": "string" + }, + "publicKey": { + "description": "public key that corresponds to this signature", + "type": "string", + "format": "byte", + "readOnly": true + }, + "sig": { + "description": "signature of the payload", + "type": "string", + "format": "byte" + } + } + }, "Error": { "type": "object", "properties": { @@ -2722,6 +2787,93 @@ func init() { "$schema": "http://json-schema.org/draft-07/schema", "$id": "http://rekor.sigstore.dev/types/alpine/alpine_v0_0_1_schema.json" }, + "dsse": { + "description": "DSSE object", + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/ProposedEntry" + }, + { + "required": [ + "apiVersion", + "spec" + ], + "properties": { + "apiVersion": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" + }, + "spec": { + "$ref": "#/definitions/dsseSchema" + } + }, + "additionalProperties": false + } + ] + }, + "dsseSchema": { + "description": "DSSE for Rekord objects", + "type": "object", + "title": "DSSE Schema", + "oneOf": [ + { + "$ref": "#/definitions/dsseV001Schema" + } + ], + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://rekor.sigstore.dev/types/dsse/dsse_schema.json" + }, + "dsseV001Schema": { + "description": "Schema for dsse object", + "type": "object", + "title": "dsse v0.0.1 Schema", + "required": [ + "payloadType", + "payloadHash", + "signatures" + ], + "properties": { + "payload": { + "description": "payload of the envelope", + "type": "string", + "format": "byte", + "writeOnly": true + }, + "payloadHash": { + "description": "hash of the envelope's payload after being PAE encoded", + "type": "object", + "properties": { + "algorithm": { + "description": "The hasing function used to compue the hash value", + "type": "string", + "enum": [ + "sha256" + ] + }, + "value": { + "description": "The hash value of the PAE encoded payload", + "type": "string" + } + }, + "readOnly": true + }, + "payloadType": { + "description": "type descriping the payload", + "type": "string" + }, + "signatures": { + "description": "collection of all signatures of the envelope's payload", + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/DsseV001SchemaSignaturesItems0" + } + } + }, + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://rekor.sigstore.dev/types/dsse/dsse_v0_0_1_schema.json" + }, "hashedrekord": { "description": "Hashed Rekord object", "type": "object", diff --git a/pkg/types/dsse/dsse.go b/pkg/types/dsse/dsse.go new file mode 100644 index 000000000..42f1d94a9 --- /dev/null +++ b/pkg/types/dsse/dsse.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 dsse + +import ( + "context" + + "github.com/pkg/errors" + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/rekor/pkg/types" +) + +const ( + KIND = "dsse" +) + +type BaseDsseType struct { + types.RekorType +} + +func init() { + types.TypeMap.Store(KIND, New) +} + +func New() types.TypeImpl { + bdt := BaseDsseType{} + bdt.Kind = KIND + bdt.VersionMap = VersionMap + return &bdt +} + +var VersionMap = types.NewSemVerEntryFactoryMap() + +func (bdt BaseDsseType) UnmarshalEntry(pe models.ProposedEntry) (types.EntryImpl, error) { + if pe == nil { + return nil, errors.New("proposed entry cannot be nil") + } + + in, ok := pe.(*models.Dsse) + if !ok { + return nil, errors.New("cannot unmarshal non-Rekord types") + } + + return bdt.VersionedUnmarshal(in, *in.APIVersion) +} + +func (bdt *BaseDsseType) CreateProposedEntry(ctx context.Context, version string, props types.ArtifactProperties) (models.ProposedEntry, error) { + if version == "" { + version = bdt.DefaultVersion() + } + ei, err := bdt.VersionedUnmarshal(nil, version) + if err != nil { + return nil, errors.Wrap(err, "fetching DSSE version implementation") + } + return ei.CreateFromArtifactProperties(ctx, props) +} + +func (bdt BaseDsseType) DefaultVersion() string { + return "0.0.1" +} diff --git a/pkg/types/dsse/dsse_schema.json b/pkg/types/dsse/dsse_schema.json new file mode 100644 index 000000000..511130952 --- /dev/null +++ b/pkg/types/dsse/dsse_schema.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://rekor.sigstore.dev/types/dsse/dsse_schema.json", + "title": "DSSE Schema", + "description": "DSSE for Rekord objects", + "type": "object", + "oneOf": [ + { + "$ref": "v0.0.1/dsse_v0_0_1_schema.json" + } + ] +} diff --git a/pkg/types/dsse/v0.0.1/dsse_v0_0_1_schema.json b/pkg/types/dsse/v0.0.1/dsse_v0_0_1_schema.json new file mode 100644 index 000000000..3b61cebf5 --- /dev/null +++ b/pkg/types/dsse/v0.0.1/dsse_v0_0_1_schema.json @@ -0,0 +1,62 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://rekor.sigstore.dev/types/dsse/dsse_v0_0_1_schema.json", + "title": "dsse v0.0.1 Schema", + "description": "Schema for dsse object", + "type": "object", + "properties": { + "payload": { + "description": "payload of the envelope", + "type": "string", + "format": "byte", + "writeOnly": true + }, + "payloadType": { + "description": "type descriping the payload", + "type": "string" + }, + "payloadHash": { + "description": "hash of the envelope's payload after being PAE encoded", + "readOnly": true, + "type": "object", + "properties": { + "algorithm": { + "description": "The hasing function used to compue the hash value", + "type": "string", + "enum": ["sha256"] + }, + "value": { + "description": "The hash value of the PAE encoded payload", + "type": "string" + } + } + }, + "signatures": { + "description": "collection of all signatures of the envelope's payload", + "type": "array", + "minItems": 1, + "items": { + "description": "a signature of the envelope's payload along with the public key for the signature", + "type": "object", + "properties": { + "keyid": { + "description": "optional id of the key used to create the signature", + "type": "string" + }, + "sig": { + "description": "signature of the payload", + "type": "string", + "format": "byte" + }, + "publicKey": { + "description": "public key that corresponds to this signature", + "type": "string", + "format": "byte", + "readOnly": true + } + } + } + } + }, + "required": ["payloadType", "payloadHash", "signatures"] +} diff --git a/pkg/types/dsse/v0.0.1/entry.go b/pkg/types/dsse/v0.0.1/entry.go new file mode 100644 index 000000000..57272d352 --- /dev/null +++ b/pkg/types/dsse/v0.0.1/entry.go @@ -0,0 +1,326 @@ +// +// 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 dsse + +import ( + "bytes" + "context" + "crypto" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/url" + "path/filepath" + + "github.com/in-toto/in-toto-golang/in_toto" + "github.com/secure-systems-lab/go-securesystemslib/dsse" + "github.com/spf13/viper" + + "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/x509" + "github.com/sigstore/rekor/pkg/types" + rekordsse "github.com/sigstore/rekor/pkg/types/dsse" + "github.com/sigstore/sigstore/pkg/signature" +) + +const ( + APIVERSION = "0.0.1" +) + +func init() { + if err := rekordsse.VersionMap.SetEntryFactory(APIVERSION, NewEntry); err != nil { + log.Logger.Panic(err) + } +} + +type V001Entry struct { + DsseObj models.DsseV001Schema +} + +func (v V001Entry) APIVersion() string { + return APIVERSION +} + +func NewEntry() types.EntryImpl { + return &V001Entry{} +} + +func (v V001Entry) IndexKeys() ([]string, error) { + var result []string + payloadBytes := v.DsseObj.Payload + payloadType := *v.DsseObj.PayloadType + h := sha256.Sum256(payloadBytes) + payloadKey := "sha256:" + hex.EncodeToString(h[:]) + result = append(result, payloadKey) + result = append(result, v.DsseObj.PayloadHash.Algorithm+":"+v.DsseObj.PayloadHash.Value) + + for _, sig := range v.DsseObj.Signatures { + keyHash := sha256.Sum256(sig.PublicKey) + result = append(result, "sha256:"+hex.EncodeToString(keyHash[:])) + } + + switch payloadType { + case in_toto.PayloadType: + statement, err := parseIntotoStatement(payloadBytes) + if err != nil { + return result, err + } + + for _, s := range statement.Subject { + for alg, ds := range s.Digest { + result = append(result, alg+":"+ds) + } + } + default: + log.Logger.Infof("Cannot index payload of type: %s", payloadType) + } + + return result, nil +} + +func parseIntotoStatement(p []byte) (*in_toto.Statement, error) { + ps := in_toto.Statement{} + if err := json.Unmarshal(p, &ps); err != nil { + return nil, err + } + + return &ps, nil +} + +func (v *V001Entry) Unmarshal(pe models.ProposedEntry) error { + dsseModel, ok := pe.(*models.Dsse) + if !ok { + return errors.New("cannot unmarshal non DSSE v0.0.1 type") + } + + if err := types.DecodeEntry(dsseModel.Spec, &v.DsseObj); err != nil { + return err + } + + // field validation + if err := v.DsseObj.Validate(strfmt.Default); err != nil { + return err + } + + env := &dsse.Envelope{} + // this weird juggling is because dsse.Envelope expects env.Payload to be a base64 string, + // while v.DsseObj.Payload is a base64 byte array... but casting it to string seemd to decode it. + // so... cast it to a string to decode it, and then re-encode it as a base64 encoded string.... + payload := string(v.DsseObj.Payload) + env.Payload = base64.StdEncoding.EncodeToString([]byte(payload)) + env.PayloadType = *v.DsseObj.PayloadType + allPubKeyBytes := make([][]byte, 0) + for _, sig := range v.DsseObj.Signatures { + env.Signatures = append(env.Signatures, dsse.Signature{ + KeyID: sig.Keyid, + Sig: string(sig.Sig), + }) + + allPubKeyBytes = append(allPubKeyBytes, sig.PublicKey) + } + + _, err := verifyEnvelope(allPubKeyBytes, env) + if err != nil { + return fmt.Errorf("could not verify envelope: %w", err) + } + + return nil +} + +func (v *V001Entry) Canonicalize(ctx context.Context) ([]byte, error) { + canonicalEntry := models.DsseV001Schema{ + PayloadHash: v.DsseObj.PayloadHash, + Signatures: v.DsseObj.Signatures, + PayloadType: v.DsseObj.PayloadType, + } + + model := models.Dsse{} + model.APIVersion = swag.String(APIVERSION) + model.Spec = canonicalEntry + return json.Marshal(&model) +} + +func (v *V001Entry) Attestation() []byte { + payload := v.DsseObj.Payload + if len(payload) > viper.GetInt("max_attestation_size") { + log.Logger.Infof("Skipping attestation storage, size %d is greater than max %d", len(payload), viper.GetInt("max_attestation_size")) + return nil + } + + return payload +} + +type verifier struct { + v signature.Verifier + pub crypto.PublicKey + keyBytes []byte + id string +} + +func (v *verifier) KeyID() (string, error) { + return v.id, nil +} + +func (v *verifier) Public() crypto.PublicKey { + return v.pub +} + +func (v *verifier) Verify(data, sig []byte) error { + if v.v == nil { + return errors.New("nil verifier") + } + return v.v.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data)) +} + +func (v V001Entry) CreateFromArtifactProperties(_ context.Context, props types.ArtifactProperties) (models.ProposedEntry, error) { + returnVal := models.Dsse{} + re := V001Entry{} + + 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("dsse envelopes cannot be fetched over HTTP(S)") + } + artifactBytes, err = ioutil.ReadFile(filepath.Clean(props.ArtifactPath.Path)) + if err != nil { + return nil, err + } + } + + env := dsse.Envelope{} + if err := json.Unmarshal(artifactBytes, &env); err != nil { + return nil, fmt.Errorf("payload must be a valid dsse envelope: %w", err) + } + + allPubKeyBytes := make([][]byte, 0) + if props.PublicKeyBytes != nil { + allPubKeyBytes = append(allPubKeyBytes, props.PublicKeyBytes) + } + + allPubKeyBytes = append(allPubKeyBytes, props.PublicKeysBytes...) + allPubKeyPaths := make([]*url.URL, 0) + if props.PublicKeyPath != nil { + allPubKeyPaths = append(allPubKeyPaths, props.PublicKeysPaths...) + } + + for _, path := range allPubKeyPaths { + if path.IsAbs() { + return nil, errors.New("dsse public keys cannot be fetched over HTTP(S)") + } + + publicKeyBytes, err := ioutil.ReadFile(filepath.Clean(path.Path)) + if err != nil { + return nil, fmt.Errorf("error reading public key file: %w", err) + } + + allPubKeyBytes = append(allPubKeyBytes, publicKeyBytes) + } + + keysBySig, err := verifyEnvelope(allPubKeyBytes, &env) + if err != nil { + return nil, err + } + + decodedPayload, err := base64.StdEncoding.DecodeString(env.Payload) + if err != nil { + return nil, fmt.Errorf("could not decode envelope payload: %w", err) + } + + paeEncodedPayload := dsse.PAE(env.PayloadType, decodedPayload) + h := sha256.Sum256(paeEncodedPayload) + re.DsseObj.Payload = decodedPayload + re.DsseObj.PayloadType = &env.PayloadType + re.DsseObj.PayloadHash = &models.DsseV001SchemaPayloadHash{ + Algorithm: models.DsseV001SchemaPayloadHashAlgorithmSha256, + Value: hex.EncodeToString(h[:]), + } + + for _, sig := range env.Signatures { + key, ok := keysBySig[sig.Sig] + if !ok { + return nil, errors.New("all signatures must have a key that verifies it") + } + + canonKey, err := key.CanonicalValue() + if err != nil { + return nil, fmt.Errorf("could not canonicize key: %w", err) + } + + keyBytes := strfmt.Base64(canonKey) + sigBytes := strfmt.Base64([]byte(sig.Sig)) + re.DsseObj.Signatures = append(re.DsseObj.Signatures, &models.DsseV001SchemaSignaturesItems0{ + Keyid: sig.KeyID, + Sig: sigBytes, + PublicKey: keyBytes, + }) + } + + returnVal.APIVersion = swag.String(re.APIVersion()) + returnVal.Spec = re.DsseObj + return &returnVal, nil +} + +// verifyEnvelope takes in an array of possible key bytes and attempts to parse them as x509 public keys. +// it then uses these to verify the envelope and makes sure that every signature on the envelope is verified. +// it returns a map of verifiers indexed by the signature the verifier corresponds to. +func verifyEnvelope(allPubKeyBytes [][]byte, env *dsse.Envelope) (map[string]*x509.PublicKey, error) { + // generate a fake id for these keys so we can get back to the key bytes and match them to their corresponding signature + verifierBySig := make(map[string]*x509.PublicKey) + + for _, pubKeyBytes := range allPubKeyBytes { + key, err := x509.NewPublicKey(bytes.NewReader(pubKeyBytes)) + if err != nil { + return nil, fmt.Errorf("could not parse public key as x509: %w", err) + } + + vfr, err := signature.LoadVerifier(key.CryptoPubKey(), crypto.SHA256) + if err != nil { + return nil, fmt.Errorf("could not load verifier: %w", err) + } + + dsseVfr, err := dsse.NewEnvelopeVerifier(&verifier{ + v: vfr, + keyBytes: pubKeyBytes, + }) + + if err != nil { + return nil, fmt.Errorf("could not use public key as a dsse verifier: %w", err) + } + + accepted, err := dsseVfr.Verify(env) + if err != nil { + return nil, fmt.Errorf("could not verify envelope: %w", err) + } + + for _, accept := range accepted { + verifierBySig[accept.Sig.Sig] = key + } + } + + return verifierBySig, nil +} diff --git a/pkg/types/entries.go b/pkg/types/entries.go index dda638ba4..11ba9efd5 100644 --- a/pkg/types/entries.go +++ b/pkg/types/entries.go @@ -120,12 +120,14 @@ 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 + ArtifactPath *url.URL + ArtifactHash string + ArtifactBytes []byte + SignaturePath *url.URL + SignatureBytes []byte + PublicKeyPath *url.URL + PublicKeyBytes []byte + PublicKeysBytes [][]byte + PublicKeysPaths []*url.URL + PKIFormat string }