diff --git a/cmd/rekor-cli/app/pflag_groups.go b/cmd/rekor-cli/app/pflag_groups.go index a005e26c3..c94c140e3 100644 --- a/cmd/rekor-cli/app/pflag_groups.go +++ b/cmd/rekor-cli/app/pflag_groups.go @@ -109,11 +109,11 @@ func validateArtifactPFlags(uuidValid, indexValid bool) error { } // if neither --entry or --artifact were given, then a reference to a uuid or index is needed - if viper.GetString("entry") == "" && viper.GetString("artifact") == "" { + if viper.GetString("entry") == "" && viper.GetString("artifact") == "" && viper.GetString("artifact-hash") == "" { if (uuidGiven && uuidValid) || (indexGiven && indexValid) { return nil } - return errors.New("either 'entry' or 'artifact' must be specified") + return errors.New("either 'entry' or 'artifact' or 'artifact-hash' must be specified") } return nil diff --git a/cmd/rekor-cli/app/root.go b/cmd/rekor-cli/app/root.go index 3968cb699..d05de5616 100644 --- a/cmd/rekor-cli/app/root.go +++ b/cmd/rekor-cli/app/root.go @@ -29,6 +29,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/hashedrekord/v0.0.1" _ "github.com/sigstore/rekor/pkg/types/helm/v0.0.1" _ "github.com/sigstore/rekor/pkg/types/intoto/v0.0.1" _ "github.com/sigstore/rekor/pkg/types/jar/v0.0.1" diff --git a/cmd/rekor-server/app/serve.go b/cmd/rekor-server/app/serve.go index 3e963aecd..ae13a6367 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" + 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" helm_v001 "github.com/sigstore/rekor/pkg/types/helm/v0.0.1" "github.com/sigstore/rekor/pkg/types/intoto" @@ -80,14 +82,15 @@ var serveCmd = &cobra.Command{ // these trigger loading of package and therefore init() methods to run pluggableTypeMap := map[string]string{ - rekord.KIND: rekord_v001.APIVERSION, - rpm.KIND: rpm_v001.APIVERSION, - jar.KIND: jar_v001.APIVERSION, - intoto.KIND: intoto_v001.APIVERSION, - rfc3161.KIND: rfc3161_v001.APIVERSION, - alpine.KIND: alpine_v001.APIVERSION, - helm.KIND: helm_v001.APIVERSION, - tuf.KIND: tuf_v001.APIVERSION, + rekord.KIND: rekord_v001.APIVERSION, + rpm.KIND: rpm_v001.APIVERSION, + jar.KIND: jar_v001.APIVERSION, + intoto.KIND: intoto_v001.APIVERSION, + rfc3161.KIND: rfc3161_v001.APIVERSION, + alpine.KIND: alpine_v001.APIVERSION, + helm.KIND: helm_v001.APIVERSION, + tuf.KIND: tuf_v001.APIVERSION, + hashedrekord.KIND: hashedrekord_v001.APIVERSION, } for k, v := range pluggableTypeMap { diff --git a/openapi.yaml b/openapi.yaml index ffa9ad293..37086dda1 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -312,6 +312,23 @@ definitions: - spec additionalProperties: false + hashedrekord: + type: object + description: Hashed Rekord 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/hashedrekord/hashedrekord_schema.json' + required: + - apiVersion + - spec + additionalProperties: false + rpm: type: object description: RPM package diff --git a/pkg/generated/models/hashedrekord.go b/pkg/generated/models/hashedrekord.go new file mode 100644 index 000000000..b3e1f8a3b --- /dev/null +++ b/pkg/generated/models/hashedrekord.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" +) + +// Hashedrekord Hashed Rekord object +// +// swagger:model hashedrekord +type Hashedrekord 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 HashedrekordSchema `json:"spec"` +} + +// Kind gets the kind of this subtype +func (m *Hashedrekord) Kind() string { + return "hashedrekord" +} + +// SetKind sets the kind of this subtype +func (m *Hashedrekord) SetKind(val string) { +} + +// UnmarshalJSON unmarshals this object with a polymorphic type from a JSON structure +func (m *Hashedrekord) 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 HashedrekordSchema `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 Hashedrekord + + 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 Hashedrekord) 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 HashedrekordSchema `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 hashedrekord +func (m *Hashedrekord) 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 *Hashedrekord) 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 *Hashedrekord) validateSpec(formats strfmt.Registry) error { + + if m.Spec == nil { + return errors.Required("spec", "body", nil) + } + + return nil +} + +// ContextValidate validate this hashedrekord based on the context it is used +func (m *Hashedrekord) 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 *Hashedrekord) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *Hashedrekord) UnmarshalBinary(b []byte) error { + var res Hashedrekord + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/pkg/generated/models/hashedrekord_schema.go b/pkg/generated/models/hashedrekord_schema.go new file mode 100644 index 000000000..49d5831f8 --- /dev/null +++ b/pkg/generated/models/hashedrekord_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 + +// HashedrekordSchema Rekor Schema +// +// Schema for Rekord objects +// +// swagger:model hashedrekordSchema +type HashedrekordSchema interface{} diff --git a/pkg/generated/models/hashedrekord_v001_schema.go b/pkg/generated/models/hashedrekord_v001_schema.go new file mode 100644 index 000000000..50fd24827 --- /dev/null +++ b/pkg/generated/models/hashedrekord_v001_schema.go @@ -0,0 +1,501 @@ +// 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" +) + +// HashedrekordV001Schema Hashed Rekor v0.0.1 Schema +// +// Schema for Hashed Rekord object +// +// swagger:model hashedrekordV001Schema +type HashedrekordV001Schema struct { + + // data + // Required: true + Data *HashedrekordV001SchemaData `json:"data"` + + // signature + // Required: true + Signature *HashedrekordV001SchemaSignature `json:"signature"` +} + +// Validate validates this hashedrekord v001 schema +func (m *HashedrekordV001Schema) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateData(formats); err != nil { + res = append(res, err) + } + + if err := m.validateSignature(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *HashedrekordV001Schema) 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 *HashedrekordV001Schema) validateSignature(formats strfmt.Registry) error { + + if err := validate.Required("signature", "body", m.Signature); err != nil { + return err + } + + if m.Signature != nil { + if err := m.Signature.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("signature") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("signature") + } + return err + } + } + + return nil +} + +// ContextValidate validate this hashedrekord v001 schema based on the context it is used +func (m *HashedrekordV001Schema) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateData(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateSignature(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *HashedrekordV001Schema) 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 +} + +func (m *HashedrekordV001Schema) contextValidateSignature(ctx context.Context, formats strfmt.Registry) error { + + if m.Signature != nil { + if err := m.Signature.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("signature") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("signature") + } + return err + } + } + + return nil +} + +// MarshalBinary interface implementation +func (m *HashedrekordV001Schema) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *HashedrekordV001Schema) UnmarshalBinary(b []byte) error { + var res HashedrekordV001Schema + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} + +// HashedrekordV001SchemaData Information about the content associated with the entry +// +// swagger:model HashedrekordV001SchemaData +type HashedrekordV001SchemaData struct { + + // hash + Hash *HashedrekordV001SchemaDataHash `json:"hash,omitempty"` +} + +// Validate validates this hashedrekord v001 schema data +func (m *HashedrekordV001SchemaData) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateHash(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *HashedrekordV001SchemaData) 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 hashedrekord v001 schema data based on the context it is used +func (m *HashedrekordV001SchemaData) 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 *HashedrekordV001SchemaData) 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 *HashedrekordV001SchemaData) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *HashedrekordV001SchemaData) UnmarshalBinary(b []byte) error { + var res HashedrekordV001SchemaData + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} + +// HashedrekordV001SchemaDataHash Specifies the hash algorithm and value for the content +// +// swagger:model HashedrekordV001SchemaDataHash +type HashedrekordV001SchemaDataHash 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 hashedrekord v001 schema data hash +func (m *HashedrekordV001SchemaDataHash) 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 hashedrekordV001SchemaDataHashTypeAlgorithmPropEnum []interface{} + +func init() { + var res []string + if err := json.Unmarshal([]byte(`["sha256"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + hashedrekordV001SchemaDataHashTypeAlgorithmPropEnum = append(hashedrekordV001SchemaDataHashTypeAlgorithmPropEnum, v) + } +} + +const ( + + // HashedrekordV001SchemaDataHashAlgorithmSha256 captures enum value "sha256" + HashedrekordV001SchemaDataHashAlgorithmSha256 string = "sha256" +) + +// prop value enum +func (m *HashedrekordV001SchemaDataHash) validateAlgorithmEnum(path, location string, value string) error { + if err := validate.EnumCase(path, location, value, hashedrekordV001SchemaDataHashTypeAlgorithmPropEnum, true); err != nil { + return err + } + return nil +} + +func (m *HashedrekordV001SchemaDataHash) 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 *HashedrekordV001SchemaDataHash) validateValue(formats strfmt.Registry) error { + + if err := validate.Required("data"+"."+"hash"+"."+"value", "body", m.Value); err != nil { + return err + } + + return nil +} + +// ContextValidate validates this hashedrekord v001 schema data hash based on context it is used +func (m *HashedrekordV001SchemaDataHash) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *HashedrekordV001SchemaDataHash) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *HashedrekordV001SchemaDataHash) UnmarshalBinary(b []byte) error { + var res HashedrekordV001SchemaDataHash + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} + +// HashedrekordV001SchemaSignature Information about the detached signature associated with the entry +// +// swagger:model HashedrekordV001SchemaSignature +type HashedrekordV001SchemaSignature struct { + + // Specifies the content of the signature inline within the document + // Format: byte + Content strfmt.Base64 `json:"content,omitempty"` + + // public key + PublicKey *HashedrekordV001SchemaSignaturePublicKey `json:"publicKey,omitempty"` +} + +// Validate validates this hashedrekord v001 schema signature +func (m *HashedrekordV001SchemaSignature) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validatePublicKey(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *HashedrekordV001SchemaSignature) validatePublicKey(formats strfmt.Registry) error { + if swag.IsZero(m.PublicKey) { // not required + return nil + } + + if m.PublicKey != nil { + if err := m.PublicKey.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("signature" + "." + "publicKey") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("signature" + "." + "publicKey") + } + return err + } + } + + return nil +} + +// ContextValidate validate this hashedrekord v001 schema signature based on the context it is used +func (m *HashedrekordV001SchemaSignature) 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 *HashedrekordV001SchemaSignature) contextValidatePublicKey(ctx context.Context, formats strfmt.Registry) error { + + if m.PublicKey != nil { + if err := m.PublicKey.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("signature" + "." + "publicKey") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("signature" + "." + "publicKey") + } + return err + } + } + + return nil +} + +// MarshalBinary interface implementation +func (m *HashedrekordV001SchemaSignature) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *HashedrekordV001SchemaSignature) UnmarshalBinary(b []byte) error { + var res HashedrekordV001SchemaSignature + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} + +// HashedrekordV001SchemaSignaturePublicKey The public key that can verify the signature +// +// swagger:model HashedrekordV001SchemaSignaturePublicKey +type HashedrekordV001SchemaSignaturePublicKey struct { + + // Specifies the content of the public key inline within the document + // Format: byte + Content strfmt.Base64 `json:"content,omitempty"` +} + +// Validate validates this hashedrekord v001 schema signature public key +func (m *HashedrekordV001SchemaSignaturePublicKey) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validates this hashedrekord v001 schema signature public key based on context it is used +func (m *HashedrekordV001SchemaSignaturePublicKey) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *HashedrekordV001SchemaSignaturePublicKey) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *HashedrekordV001SchemaSignaturePublicKey) UnmarshalBinary(b []byte) error { + var res HashedrekordV001SchemaSignaturePublicKey + 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 20a1c94a5..a9c360586 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 "hashedrekord": + var result Hashedrekord + if err := consumer.Consume(buf2, &result); err != nil { + return nil, err + } + return &result, nil case "helm": var result Helm 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 8008877a3..4242c544c 100644 --- a/pkg/generated/restapi/embedded_spec.go +++ b/pkg/generated/restapi/embedded_spec.go @@ -664,6 +664,32 @@ func init() { } ] }, + "hashedrekord": { + "description": "Hashed Rekord 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/hashedrekord/hashedrekord_schema.json" + } + }, + "additionalProperties": false + } + ] + }, "helm": { "description": "Helm chart", "type": "object", @@ -1457,6 +1483,87 @@ func init() { } } }, + "HashedrekordV001SchemaData": { + "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", + "required": [ + "algorithm", + "value" + ], + "properties": { + "algorithm": { + "description": "The hashing function used to compute the hash value", + "type": "string", + "enum": [ + "sha256" + ] + }, + "value": { + "description": "The hash value for the content", + "type": "string" + } + } + } + } + }, + "HashedrekordV001SchemaDataHash": { + "description": "Specifies the hash algorithm and value for the content", + "type": "object", + "required": [ + "algorithm", + "value" + ], + "properties": { + "algorithm": { + "description": "The hashing function used to compute the hash value", + "type": "string", + "enum": [ + "sha256" + ] + }, + "value": { + "description": "The hash value for the content", + "type": "string" + } + } + }, + "HashedrekordV001SchemaSignature": { + "description": "Information about the detached signature associated with the entry", + "type": "object", + "properties": { + "content": { + "description": "Specifies the content of the signature inline within the document", + "type": "string", + "format": "byte" + }, + "publicKey": { + "description": "The public key that can verify the signature", + "type": "object", + "properties": { + "content": { + "description": "Specifies the content of the public key inline within the document", + "type": "string", + "format": "byte" + } + } + } + } + }, + "HashedrekordV001SchemaSignaturePublicKey": { + "description": "The public key that can verify the signature", + "type": "object", + "properties": { + "content": { + "description": "Specifies the content of the public key inline within the document", + "type": "string", + "format": "byte" + } + } + }, "HelmV001SchemaChart": { "description": "Information about the Helm chart associated with the entry", "type": "object", @@ -2535,6 +2642,105 @@ func init() { "$schema": "http://json-schema.org/draft-07/schema", "$id": "http://rekor.sigstore.dev/types/alpine/alpine_v0_0_1_schema.json" }, + "hashedrekord": { + "description": "Hashed Rekord 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/hashedrekordSchema" + } + }, + "additionalProperties": false + } + ] + }, + "hashedrekordSchema": { + "description": "Schema for Rekord objects", + "type": "object", + "title": "Rekor Schema", + "oneOf": [ + { + "$ref": "#/definitions/hashedrekordV001Schema" + } + ], + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://rekor.sigstore.dev/types/hashedrekord/hasehedrekord_schema.json" + }, + "hashedrekordV001Schema": { + "description": "Schema for Hashed Rekord object", + "type": "object", + "title": "Hashed Rekor v0.0.1 Schema", + "required": [ + "signature", + "data" + ], + "properties": { + "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", + "required": [ + "algorithm", + "value" + ], + "properties": { + "algorithm": { + "description": "The hashing function used to compute the hash value", + "type": "string", + "enum": [ + "sha256" + ] + }, + "value": { + "description": "The hash value for the content", + "type": "string" + } + } + } + } + }, + "signature": { + "description": "Information about the detached signature associated with the entry", + "type": "object", + "properties": { + "content": { + "description": "Specifies the content of the signature inline within the document", + "type": "string", + "format": "byte" + }, + "publicKey": { + "description": "The public key that can verify the signature", + "type": "object", + "properties": { + "content": { + "description": "Specifies the content of the public key inline within the document", + "type": "string", + "format": "byte" + } + } + } + } + } + }, + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://rekor.sigstore.dev/types/rekord/rekord_v0_0_1_schema.json" + }, "helm": { "description": "Helm chart", "type": "object", diff --git a/pkg/pki/minisign/minisign.go b/pkg/pki/minisign/minisign.go index 2697d96b3..2026a33ad 100644 --- a/pkg/pki/minisign/minisign.go +++ b/pkg/pki/minisign/minisign.go @@ -85,7 +85,7 @@ func (s Signature) CanonicalValue() ([]byte, error) { } // Verify implements the pki.Signature interface -func (s Signature) Verify(r io.Reader, k interface{}) error { +func (s Signature) Verify(r io.Reader, k interface{}, opts ...sigsig.VerifyOption) error { if s.signature == nil { return fmt.Errorf("minisign signature has not been initialized") } diff --git a/pkg/pki/pgp/pgp.go b/pkg/pki/pgp/pgp.go index 7e3c7bba0..80fcd43dc 100644 --- a/pkg/pki/pgp/pgp.go +++ b/pkg/pki/pgp/pgp.go @@ -28,6 +28,8 @@ import ( "golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp/armor" "golang.org/x/crypto/openpgp/packet" + + sigsig "github.com/sigstore/sigstore/pkg/signature" ) // Signature Signature that follows the PGP standard; supports both armored & binary detached signatures @@ -130,7 +132,7 @@ func (s Signature) CanonicalValue() ([]byte, error) { } // Verify implements the pki.Signature interface -func (s Signature) Verify(r io.Reader, k interface{}) error { +func (s Signature) Verify(r io.Reader, k interface{}, opts ...sigsig.VerifyOption) error { if len(s.signature) == 0 { return fmt.Errorf("PGP signature has not been initialized") } diff --git a/pkg/pki/pkcs7/pkcs7.go b/pkg/pki/pkcs7/pkcs7.go index 8e842621b..10f29a21e 100644 --- a/pkg/pki/pkcs7/pkcs7.go +++ b/pkg/pki/pkcs7/pkcs7.go @@ -29,6 +29,7 @@ import ( "strings" "github.com/sassoftware/relic/lib/pkcs7" + sigsig "github.com/sigstore/sigstore/pkg/signature" ) // EmailAddressOID defined by https://oidref.com/1.2.840.113549.1.9.1 @@ -105,7 +106,7 @@ func (s Signature) CanonicalValue() ([]byte, error) { } // Verify implements the pki.Signature interface -func (s Signature) Verify(r io.Reader, k interface{}) error { +func (s Signature) Verify(r io.Reader, k interface{}, opts ...sigsig.VerifyOption) error { if len(*s.raw) == 0 { return fmt.Errorf("PKCS7 signature has not been initialized") } diff --git a/pkg/pki/pki.go b/pkg/pki/pki.go index cca198716..d1618034d 100644 --- a/pkg/pki/pki.go +++ b/pkg/pki/pki.go @@ -17,6 +17,8 @@ package pki import ( "io" + + sigsig "github.com/sigstore/sigstore/pkg/signature" ) // PublicKey Generic object representing a public key (regardless of format & algorithm) @@ -28,5 +30,5 @@ type PublicKey interface { // Signature Generic object representing a signature (regardless of format & algorithm) type Signature interface { CanonicalValue() ([]byte, error) - Verify(r io.Reader, k interface{}) error + Verify(r io.Reader, k interface{}, opts ...sigsig.VerifyOption) error } diff --git a/pkg/pki/ssh/ssh.go b/pkg/pki/ssh/ssh.go index faa4d6d25..8012905aa 100644 --- a/pkg/pki/ssh/ssh.go +++ b/pkg/pki/ssh/ssh.go @@ -20,6 +20,7 @@ import ( "io" "io/ioutil" + sigsig "github.com/sigstore/sigstore/pkg/signature" "golang.org/x/crypto/ssh" ) @@ -48,7 +49,7 @@ func (s Signature) CanonicalValue() ([]byte, error) { } // Verify implements the pki.Signature interface -func (s Signature) Verify(r io.Reader, k interface{}) error { +func (s Signature) Verify(r io.Reader, k interface{}, opts ...sigsig.VerifyOption) error { if s.signature == nil { return fmt.Errorf("ssh signature has not been initialized") } diff --git a/pkg/pki/tuf/tuf.go b/pkg/pki/tuf/tuf.go index db4cb5c78..1b63049b2 100644 --- a/pkg/pki/tuf/tuf.go +++ b/pkg/pki/tuf/tuf.go @@ -22,6 +22,7 @@ import ( "io/ioutil" "time" + sigsig "github.com/sigstore/sigstore/pkg/signature" cjson "github.com/tent/canonical-json-go" "github.com/theupdateframework/go-tuf/data" "github.com/theupdateframework/go-tuf/verify" @@ -80,7 +81,7 @@ func (s Signature) CanonicalValue() ([]byte, error) { } // Verify implements the pki.Signature interface -func (s Signature) Verify(_ io.Reader, k interface{}) error { +func (s Signature) Verify(_ io.Reader, k interface{}, opts ...sigsig.VerifyOption) error { key, ok := k.(*PublicKey) if !ok { return fmt.Errorf("invalid public key type for: %v", k) diff --git a/pkg/pki/x509/x509.go b/pkg/pki/x509/x509.go index 4933d3a1b..0513b0a4b 100644 --- a/pkg/pki/x509/x509.go +++ b/pkg/pki/x509/x509.go @@ -56,7 +56,7 @@ func (s Signature) CanonicalValue() ([]byte, error) { } // Verify implements the pki.Signature interface -func (s Signature) Verify(r io.Reader, k interface{}) error { +func (s Signature) Verify(r io.Reader, k interface{}, opts ...sigsig.VerifyOption) error { if len(s.signature) == 0 { //lint:ignore ST1005 X509 is proper use of term return fmt.Errorf("X509 signature has not been initialized") @@ -76,7 +76,7 @@ func (s Signature) Verify(r io.Reader, k interface{}) error { if err != nil { return err } - return verifier.VerifySignature(bytes.NewReader(s.signature), r) + return verifier.VerifySignature(bytes.NewReader(s.signature), r, opts...) } // PublicKey Public Key that follows the x509 standard diff --git a/pkg/types/hashedrekord/hashedrekord.go b/pkg/types/hashedrekord/hashedrekord.go new file mode 100644 index 000000000..4ae6b52f8 --- /dev/null +++ b/pkg/types/hashedrekord/hashedrekord.go @@ -0,0 +1,75 @@ +// +// 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 hashedrekord + +import ( + "context" + "fmt" + + "github.com/pkg/errors" + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/rekor/pkg/types" +) + +const ( + KIND = "hashedrekord" +) + +type BaseRekordType struct { + types.RekorType +} + +func init() { + types.TypeMap.Store(KIND, New) +} + +func New() types.TypeImpl { + brt := BaseRekordType{} + brt.Kind = KIND + brt.VersionMap = VersionMap + return &brt +} + +var VersionMap = types.NewSemVerEntryFactoryMap() + +func (rt BaseRekordType) UnmarshalEntry(pe models.ProposedEntry) (types.EntryImpl, error) { + if pe == nil { + return nil, errors.New("proposed entry cannot be nil") + } + + rekord, ok := pe.(*models.Hashedrekord) + if !ok { + return nil, errors.New(fmt.Sprintf("%s, %s", "cannot unmarshal non-hashed Rekord types", pe.Kind())) + } + + return rt.VersionedUnmarshal(rekord, *rekord.APIVersion) +} + +func (rt *BaseRekordType) CreateProposedEntry(ctx context.Context, version string, props types.ArtifactProperties) (models.ProposedEntry, error) { + if version == "" { + version = rt.DefaultVersion() + } + ei, err := rt.VersionedUnmarshal(nil, version) + if err != nil { + return nil, errors.Wrap(err, "fetching hashed Rekord version implementation") + } + + return ei.CreateFromArtifactProperties(ctx, props) +} + +func (rt BaseRekordType) DefaultVersion() string { + return "0.0.1" +} diff --git a/pkg/types/hashedrekord/hashedrekord_schema.json b/pkg/types/hashedrekord/hashedrekord_schema.json new file mode 100644 index 000000000..e6104f4c8 --- /dev/null +++ b/pkg/types/hashedrekord/hashedrekord_schema.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://rekor.sigstore.dev/types/hashedrekord/hasehedrekord_schema.json", + "title": "Rekor Schema", + "description": "Schema for Rekord objects", + "type": "object", + "oneOf": [ + { + "$ref": "v0.0.1/hashedrekord_v0_0_1_schema.json" + } + ] +} diff --git a/pkg/types/hashedrekord/hashedrekord_test.go b/pkg/types/hashedrekord/hashedrekord_test.go new file mode 100644 index 000000000..d3ed2e28d --- /dev/null +++ b/pkg/types/hashedrekord/hashedrekord_test.go @@ -0,0 +1,128 @@ +// +// 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 hashedrekord + +import ( + "context" + "errors" + "testing" + + "github.com/go-openapi/swag" + + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/rekor/pkg/types" +) + +type UnmarshalTester struct { + models.Hashedrekord +} + +func (u UnmarshalTester) NewEntry() types.EntryImpl { + return &UnmarshalTester{} +} + +func (u UnmarshalTester) Validate() error { + return nil +} + +func (u UnmarshalTester) APIVersion() string { + return "2.0.1" +} + +func (u UnmarshalTester) IndexKeys() []string { + return []string{} +} + +func (u UnmarshalTester) Canonicalize(ctx context.Context) ([]byte, error) { + return nil, nil +} + +func (u UnmarshalTester) HasExternalEntities() bool { + return false +} + +func (u UnmarshalTester) Unmarshal(pe models.ProposedEntry) error { + return nil +} + +func (u UnmarshalTester) Attestation() (string, []byte) { + return "", nil +} + +func (u UnmarshalTester) CreateFromArtifactProperties(_ context.Context, _ types.ArtifactProperties) (models.ProposedEntry, error) { + return nil, nil +} + +type UnmarshalFailsTester struct { + UnmarshalTester +} + +func (u UnmarshalFailsTester) NewEntry() types.EntryImpl { + return &UnmarshalFailsTester{} +} + +func (u UnmarshalFailsTester) Unmarshal(pe models.ProposedEntry) error { + return errors.New("error") +} + +func TestRekordType(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.Hashedrekord.APIVersion = swag.String("2.0.1") + brt := New() + + // version requested matches implementation in map + if _, err := brt.UnmarshalEntry(&u.Hashedrekord); err != nil { + t.Errorf("unexpected error in Unmarshal: %v", err) + } + + // version requested fails to match implementation in map + u.Hashedrekord.APIVersion = swag.String("1.2.2") + if _, err := brt.UnmarshalEntry(&u.Hashedrekord); err == nil { + t.Error("unexpected success in Unmarshal for non-matching version") + } + + // error in Unmarshal call is raised appropriately + u.Hashedrekord.APIVersion = swag.String("2.2.0") + u2 := UnmarshalFailsTester{} + _ = VersionMap.SetEntryFactory(">= 1.2.3", u2.NewEntry) + if _, err := brt.UnmarshalEntry(&u.Hashedrekord); err == nil { + t.Error("unexpected success in Unmarshal when error is thrown") + } + + // version requested fails to match implementation in map + u.Hashedrekord.APIVersion = swag.String("not_a_version") + if _, err := brt.UnmarshalEntry(&u.Hashedrekord); err == nil { + t.Error("unexpected success in Unmarshal for invalid version") + } +} diff --git a/pkg/types/hashedrekord/v0.0.1/entry.go b/pkg/types/hashedrekord/v0.0.1/entry.go new file mode 100644 index 000000000..740530a31 --- /dev/null +++ b/pkg/types/hashedrekord/v0.0.1/entry.go @@ -0,0 +1,249 @@ +// +// 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 hashedrekord + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io/ioutil" + "path/filepath" + "strings" + + "github.com/asaskevich/govalidator" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/pkg/errors" + + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/rekor/pkg/log" + "github.com/sigstore/rekor/pkg/pki" + "github.com/sigstore/rekor/pkg/types" + hashedrekord "github.com/sigstore/rekor/pkg/types/hashedrekord" + "github.com/sigstore/sigstore/pkg/signature/options" +) + +const ( + APIVERSION = "0.0.1" +) + +func init() { + if err := hashedrekord.VersionMap.SetEntryFactory(APIVERSION, NewEntry); err != nil { + log.Logger.Panic(err) + } +} + +type V001Entry struct { + HashedRekordObj models.HashedrekordV001Schema + keyObj pki.PublicKey + sigObj pki.Signature +} + +func (v V001Entry) APIVersion() string { + return APIVERSION +} + +func NewEntry() types.EntryImpl { + return &V001Entry{} +} + +func (v V001Entry) IndexKeys() []string { + var result []string + + key, err := v.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, v.keyObj.EmailAddresses()...) + + if v.HashedRekordObj.Data.Hash != nil { + hashKey := strings.ToLower(fmt.Sprintf("%s:%s", *v.HashedRekordObj.Data.Hash.Algorithm, *v.HashedRekordObj.Data.Hash.Value)) + result = append(result, hashKey) + } + + return result +} + +func (v *V001Entry) Unmarshal(pe models.ProposedEntry) error { + rekord, ok := pe.(*models.Hashedrekord) + if !ok { + return errors.New("cannot unmarshal non Rekord v0.0.1 type") + } + + if err := types.DecodeEntry(rekord.Spec, &v.HashedRekordObj); err != nil { + return err + } + + // field validation + if err := v.HashedRekordObj.Validate(strfmt.Default); err != nil { + return err + } + + // cross field validation + return v.validate() +} + +func (v *V001Entry) Canonicalize(ctx context.Context) ([]byte, error) { + if err := v.validate(); err != nil { + return nil, types.ValidationError(err) + } + + if v.sigObj == nil { + return nil, errors.New("signature object not initialized before canonicalization") + } + + if v.keyObj == nil { + return nil, errors.New("key object not initialized before canonicalization") + } + + canonicalEntry := models.HashedrekordV001Schema{} + + // need to canonicalize signature & key content + canonicalEntry.Signature = &models.HashedrekordV001SchemaSignature{} + var err error + canonicalEntry.Signature.Content, err = v.sigObj.CanonicalValue() + if err != nil { + return nil, err + } + + // key URL (if known) is not set deliberately + canonicalEntry.Signature.PublicKey = &models.HashedrekordV001SchemaSignaturePublicKey{} + canonicalEntry.Signature.PublicKey.Content, err = v.keyObj.CanonicalValue() + if err != nil { + return nil, err + } + + canonicalEntry.Data = &models.HashedrekordV001SchemaData{} + canonicalEntry.Data.Hash = v.HashedRekordObj.Data.Hash + // data content is not set deliberately + + // wrap in valid object with kind and apiVersion set + rekordObj := models.Hashedrekord{} + rekordObj.APIVersion = swag.String(APIVERSION) + rekordObj.Spec = &canonicalEntry + + return json.Marshal(&rekordObj) +} + +// validate performs cross-field validation for fields in object +func (v *V001Entry) validate() error { + sig := v.HashedRekordObj.Signature + if sig == nil { + return types.ValidationError(errors.New("missing signature")) + } + // Hashed rekord type only works for x509 signature types + artifactFactory, err := pki.NewArtifactFactory(pki.X509) + if err != nil { + return types.ValidationError(err) + } + v.sigObj, err = artifactFactory.NewSignature(bytes.NewReader(sig.Content)) + if err != nil { + return errors.Wrap(err, "creating new signature object") + } + + key := sig.PublicKey + if key == nil { + return types.ValidationError(errors.New("missing public key")) + } + v.keyObj, err = artifactFactory.NewPublicKey(bytes.NewReader(key.Content)) + if err != nil { + return errors.Wrap(err, "creating new public key object") + } + + data := v.HashedRekordObj.Data + if data == nil { + return types.ValidationError(errors.New("missing data")) + } + + hash := data.Hash + if hash == nil { + return types.ValidationError(errors.New("missing hash")) + } + if !govalidator.IsHash(swag.StringValue(hash.Value), swag.StringValue(hash.Algorithm)) { + return types.ValidationError(errors.New("invalid value for hash")) + } + + decoded, err := hex.DecodeString(*hash.Value) + if err != nil { + return err + } + if err = v.sigObj.Verify(nil, v.keyObj, options.WithDigest(decoded)); err != nil { + return types.ValidationError(errors.Wrap(err, "verifying signature")) + } + + return nil +} + +func (v V001Entry) Attestation() (string, []byte) { + return "", nil +} + +func (v V001Entry) CreateFromArtifactProperties(ctx context.Context, props types.ArtifactProperties) (models.ProposedEntry, error) { + returnVal := models.Hashedrekord{} + re := V001Entry{} + + // we will need artifact, public-key, signature + re.HashedRekordObj.Data = &models.HashedrekordV001SchemaData{} + + var err error + + re.HashedRekordObj.Signature = &models.HashedrekordV001SchemaSignature{} + sigBytes := props.SignatureBytes + if sigBytes == nil { + if props.SignaturePath == nil { + return nil, errors.New("a detached signature must be provided") + } + sigBytes, err = ioutil.ReadFile(filepath.Clean(props.SignaturePath.Path)) + if err != nil { + return nil, fmt.Errorf("error reading signature file: %w", err) + } + } + re.HashedRekordObj.Signature.Content = strfmt.Base64(sigBytes) + + re.HashedRekordObj.Signature.PublicKey = &models.HashedrekordV001SchemaSignaturePublicKey{} + publicKeyBytes := props.PublicKeyBytes + if publicKeyBytes == nil { + if props.PublicKeyPath == nil { + return nil, errors.New("public key must be provided to verify detached signature") + } + publicKeyBytes, err = ioutil.ReadFile(filepath.Clean(props.PublicKeyPath.Path)) + if err != nil { + return nil, fmt.Errorf("error reading public key file: %w", err) + } + } + re.HashedRekordObj.Signature.PublicKey.Content = strfmt.Base64(publicKeyBytes) + + re.HashedRekordObj.Data.Hash = &models.HashedrekordV001SchemaDataHash{ + Algorithm: swag.String(models.HashedrekordV001SchemaDataHashAlgorithmSha256), + Value: swag.String(props.ArtifactHash), + } + + if err := re.validate(); err != nil { + return nil, err + } + + returnVal.APIVersion = swag.String(re.APIVersion()) + returnVal.Spec = re.HashedRekordObj + + return &returnVal, nil +} diff --git a/pkg/types/hashedrekord/v0.0.1/entry_test.go b/pkg/types/hashedrekord/v0.0.1/entry_test.go new file mode 100644 index 000000000..7f43851d8 --- /dev/null +++ b/pkg/types/hashedrekord/v0.0.1/entry_test.go @@ -0,0 +1,261 @@ +// +// 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 hashedrekord + +import ( + "bytes" + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "encoding/pem" + "reflect" + "testing" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/swag" + "go.uber.org/goleak" + + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/rekor/pkg/types" + "github.com/sigstore/sigstore/pkg/signature" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} + +func TestNewEntryReturnType(t *testing.T) { + entry := NewEntry() + if reflect.TypeOf(entry) != reflect.ValueOf(&V001Entry{}).Type() { + t.Errorf("invalid type returned from NewEntry: %T", entry) + } +} + +func TestCrossFieldValidation(t *testing.T) { + type TestCase struct { + caseDesc string + entry V001Entry + expectUnmarshalSuccess bool + expectCanonicalizeSuccess bool + } + + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + der, err := x509.MarshalPKIXPublicKey(&key.PublicKey) + if err != nil { + t.Fatal(err) + } + keyBytes := pem.EncodeToMemory(&pem.Block{ + Bytes: der, + Type: "PUBLIC KEY", + }) + + dataBytes := []byte("sign me!") + h := sha256.Sum256(dataBytes) + dataSHA := hex.EncodeToString(h[:]) + + signer, _ := signature.LoadSigner(key, crypto.SHA256) + sigBytes, _ := signer.SignMessage(bytes.NewReader(dataBytes)) + + incorrectLengthHash := sha256.Sum224(dataBytes) + incorrectLengthSHA := hex.EncodeToString(incorrectLengthHash[:]) + + badHash := sha256.Sum256(keyBytes) + badDataSHA := hex.EncodeToString(badHash[:]) + + testCases := []TestCase{ + { + caseDesc: "empty obj", + entry: V001Entry{}, + expectUnmarshalSuccess: false, + }, + { + caseDesc: "signature without url or content", + entry: V001Entry{ + HashedRekordObj: models.HashedrekordV001Schema{ + Signature: &models.HashedrekordV001SchemaSignature{}, + }, + }, + expectUnmarshalSuccess: false, + }, + { + caseDesc: "signature without public key", + entry: V001Entry{ + HashedRekordObj: models.HashedrekordV001Schema{ + Signature: &models.HashedrekordV001SchemaSignature{ + Content: sigBytes, + }, + }, + }, + expectUnmarshalSuccess: false, + }, + { + caseDesc: "signature with empty public key", + entry: V001Entry{ + HashedRekordObj: models.HashedrekordV001Schema{ + Signature: &models.HashedrekordV001SchemaSignature{ + Content: sigBytes, + PublicKey: &models.HashedrekordV001SchemaSignaturePublicKey{}, + }, + }, + }, + expectUnmarshalSuccess: false, + }, + { + caseDesc: "signature without data", + entry: V001Entry{ + HashedRekordObj: models.HashedrekordV001Schema{ + Signature: &models.HashedrekordV001SchemaSignature{ + Content: sigBytes, + PublicKey: &models.HashedrekordV001SchemaSignaturePublicKey{ + Content: keyBytes, + }, + }, + }, + }, + expectUnmarshalSuccess: false, + }, + { + caseDesc: "signature with empty data", + entry: V001Entry{ + HashedRekordObj: models.HashedrekordV001Schema{ + Signature: &models.HashedrekordV001SchemaSignature{ + Content: sigBytes, + PublicKey: &models.HashedrekordV001SchemaSignaturePublicKey{ + Content: keyBytes, + }, + }, + Data: &models.HashedrekordV001SchemaData{}, + }, + }, + expectUnmarshalSuccess: false, + }, + { + caseDesc: "signature with hash", + entry: V001Entry{ + HashedRekordObj: models.HashedrekordV001Schema{ + Signature: &models.HashedrekordV001SchemaSignature{ + Content: sigBytes, + PublicKey: &models.HashedrekordV001SchemaSignaturePublicKey{ + Content: keyBytes, + }, + }, + Data: &models.HashedrekordV001SchemaData{ + Hash: &models.HashedrekordV001SchemaDataHash{ + Value: swag.String(dataSHA), + Algorithm: swag.String(models.HashedrekordV001SchemaDataHashAlgorithmSha256), + }, + }, + }, + }, + expectUnmarshalSuccess: true, + expectCanonicalizeSuccess: true, + }, + { + caseDesc: "signature with invalid sha length", + entry: V001Entry{ + HashedRekordObj: models.HashedrekordV001Schema{ + Signature: &models.HashedrekordV001SchemaSignature{ + Content: sigBytes, + PublicKey: &models.HashedrekordV001SchemaSignaturePublicKey{ + Content: keyBytes, + }, + }, + Data: &models.HashedrekordV001SchemaData{ + Hash: &models.HashedrekordV001SchemaDataHash{ + Value: swag.String(incorrectLengthSHA), + Algorithm: swag.String(models.HashedrekordV001SchemaDataHashAlgorithmSha256), + }, + }, + }, + }, + expectUnmarshalSuccess: false, + expectCanonicalizeSuccess: false, + }, + { + caseDesc: "signature with hash & invalid signature", + entry: V001Entry{ + HashedRekordObj: models.HashedrekordV001Schema{ + Signature: &models.HashedrekordV001SchemaSignature{ + Content: sigBytes, + PublicKey: &models.HashedrekordV001SchemaSignaturePublicKey{ + Content: keyBytes, + }, + }, + Data: &models.HashedrekordV001SchemaData{ + Hash: &models.HashedrekordV001SchemaDataHash{ + Value: swag.String(badDataSHA), + Algorithm: swag.String(models.HashedrekordV001SchemaDataHashAlgorithmSha256), + }, + }, + }, + }, + expectUnmarshalSuccess: false, + expectCanonicalizeSuccess: false, + }, + } + + for _, tc := range testCases { + if err := tc.entry.validate(); (err == nil) != tc.expectUnmarshalSuccess { + t.Errorf("unexpected result in '%v': %v", tc.caseDesc, err) + } + + v := &V001Entry{} + r := models.Hashedrekord{ + APIVersion: swag.String(tc.entry.APIVersion()), + Spec: tc.entry.HashedRekordObj, + } + + unmarshalAndValidate := func() error { + if err := v.Unmarshal(&r); err != nil { + return err + } + if err := v.validate(); err != nil { + return err + } + return nil + } + + if err := unmarshalAndValidate(); (err == nil) != tc.expectUnmarshalSuccess { + t.Errorf("unexpected result in '%v': %v", tc.caseDesc, err) + } + + b, err := v.Canonicalize(context.TODO()) + if (err == nil) != tc.expectCanonicalizeSuccess { + t.Errorf("unexpected result from Canonicalize for '%v': %v", tc.caseDesc, err) + } else if err != nil { + if _, ok := err.(types.ValidationError); !ok { + t.Errorf("canonicalize returned an unexpected error that isn't of type types.ValidationError: %v", err) + } + } + if b != nil { + pe, err := models.UnmarshalProposedEntry(bytes.NewReader(b), runtime.JSONConsumer()) + if err != nil { + t.Errorf("unexpected err from Unmarshalling canonicalized entry for '%v': %v", tc.caseDesc, err) + } + if _, err := types.NewEntry(pe); err != nil { + t.Errorf("unexpected err from type-specific unmarshalling for '%v': %v", tc.caseDesc, err) + } + } + } +} diff --git a/pkg/types/hashedrekord/v0.0.1/hashedrekord_v0_0_1_schema.json b/pkg/types/hashedrekord/v0.0.1/hashedrekord_v0_0_1_schema.json new file mode 100644 index 000000000..b1ef65052 --- /dev/null +++ b/pkg/types/hashedrekord/v0.0.1/hashedrekord_v0_0_1_schema.json @@ -0,0 +1,54 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://rekor.sigstore.dev/types/rekord/rekord_v0_0_1_schema.json", + "title": "Hashed Rekor v0.0.1 Schema", + "description": "Schema for Hashed Rekord object", + "type": "object", + "properties": { + "signature": { + "description": "Information about the detached signature associated with the entry", + "type": "object", + "properties": { + "content": { + "description": "Specifies the content of the signature inline within the document", + "type": "string", + "format": "byte" + }, + "publicKey" : { + "description": "The public key that can verify the signature", + "type": "object", + "properties": { + "content": { + "description": "Specifies the content of the public key inline within the document", + "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" ] + } + } + } + }, + "required": [ "signature", "data" ] +} diff --git a/tests/e2e_test.go b/tests/e2e_test.go index 4a2344bda..94790b42b 100644 --- a/tests/e2e_test.go +++ b/tests/e2e_test.go @@ -136,6 +136,35 @@ func TestUploadVerifyRekord(t *testing.T) { outputContains(t, out, "Inclusion Proof:") } +func TestUploadVerifyHashedRekord(t *testing.T) { + + // Create a random artifact and sign it. + artifactPath := filepath.Join(t.TempDir(), "artifact") + sigPath := filepath.Join(t.TempDir(), "signature.asc") + + createdX509SignedArtifact(t, artifactPath, sigPath) + dataBytes, _ := ioutil.ReadFile(artifactPath) + h := sha256.Sum256(dataBytes) + dataSHA := hex.EncodeToString(h[:]) + + // Write the public key to a file + pubPath := filepath.Join(t.TempDir(), "pubKey.asc") + if err := ioutil.WriteFile(pubPath, []byte(rsaCert), 0644); err != nil { + t.Fatal(err) + } + + // Verify should fail initially + runCliErr(t, "verify", "--type=hashedrekord", "--artifact-hash", dataSHA, "--signature", sigPath, "--public-key", pubPath) + + // It should upload successfully. + out := runCli(t, "upload", "--type=hashedrekord", "--artifact-hash", dataSHA, "--signature", sigPath, "--public-key", pubPath) + outputContains(t, out, "Created entry at") + + // Now we should be able to verify it. + out = runCli(t, "verify", "--type=hashedrekord", "--artifact-hash", dataSHA, "--signature", sigPath, "--public-key", pubPath) + outputContains(t, out, "Inclusion Proof:") +} + func TestUploadVerifyRpm(t *testing.T) { // Create a random rpm and sign it. diff --git a/types.md b/types.md index 400e23040..a7b142222 100644 --- a/types.md +++ b/types.md @@ -287,3 +287,57 @@ Body: { } ``` + + +## Hashed rekord + +This is similar to a rekord type, but allows hashed data instead of supplying the full content that was signed. This is suitable for uploading signatures on large payloads. This is only compatible with x509 / PKIX signature types. + +Generate a keypair with: + +```console +$ openssl ecparam -genkey -name prime256v1 > ec_private.pem +$ openssl ec -in ec_private.pem -pubout > ec_public.pem +read EC key +writing EC key +``` + +Sign the file with: + +```console +$ openssl dgst -sha256 -sign ec_private.pem -out README.md.sig README.md +``` + +Upload it to rekor with: + +```console +$ ./rekor-cli upload --type hashedrekord:0.0.1 --artifact-hash $(sha256sum README.md | awk '{print $1}') --signature README.md.sig --pki-format=x509 --public-key=ec_public.pem +Created entry at index 12, available at: https://rekor.sigstore.dev/api/v1/log/entries/31a51c1bc20da83b66b2f24899184b85dbf8261c2de8571479165619ad87cd5d +``` + +View the entry with: + +```console +$ rekor-cli get --uuid=31a51c1bc20da83b66b2f24899184b85dbf8261c2de8571479165619ad87cd5d +LogID: b3e217db795022552080ed8b22596131c63f3aa198e83450f3dba9e686633641 +Index: 12 +IntegratedTime: 2021-11-17T21:59:49Z +UUID: 31a51c1bc20da83b66b2f24899184b85dbf8261c2de8571479165619ad87cd5d +Body: { + "HashedRekordObj": { + "data": { + "hash": { + "algorithm": "sha256", + "value": "9249e5dfa2ede1c5bd89c49bf9beaf3e9afda2d961dea7cda7f639210179cd16" + } + }, + "signature": { + "content": "MEQCIG9s7GVWH67OkeXPQvM/XAcLW7N0xiFZWez95uR+GlXyAiBW+DPRaYvgtpQglQLtqujwb+xQBd8I70Vk/2vDB+G3uQ==", + "publicKey": { + "content": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFR0JPTVdDanViVTVldkJ0OGcxWTZTR1ZoZ29OVwpjY2lrbHlpTEJQajQ5Um40WVFhTjRJS0xySi9nQlROU2tOREdQbHFvNHVjTVg3L21PZmlBNkVHS09BPT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==" + } + } + } +} + +``` \ No newline at end of file