diff --git a/.github/workflows/validate-release.yml b/.github/workflows/validate-release.yml index 21e2688c6..cd7166dc5 100644 --- a/.github/workflows/validate-release.yml +++ b/.github/workflows/validate-release.yml @@ -51,7 +51,7 @@ jobs: go-version: ${{ env.GOVERSION }} - uses: anchore/sbom-action/download-syft@b5042e9d19d8b32849779bfe17673ff84aec702d # v0.12.0 - name: Install GoReleaser - uses: goreleaser/goreleaser-action@68acf3b1adf004ac9c2f0a4259e85c5f66e99bef # v3.0.0 + uses: goreleaser/goreleaser-action@ff11ca24a9b39f2d36796d1fbd7a4e39c182630a # v3.1.0 with: install-only: true diff --git a/Makefile.swagger b/Makefile.swagger index 1221342a1..781b4d37d 100644 --- a/Makefile.swagger +++ b/Makefile.swagger @@ -1,2 +1,2 @@ # This file is generated after swagger runs as part of the build; do not edit! -SWAGGER_GEN=pkg/generated/client/entries/create_log_entry_parameters.go pkg/generated/client/entries/create_log_entry_responses.go pkg/generated/client/entries/entries_client.go pkg/generated/client/entries/get_log_entry_by_index_parameters.go pkg/generated/client/entries/get_log_entry_by_index_responses.go pkg/generated/client/entries/get_log_entry_by_uuid_parameters.go pkg/generated/client/entries/get_log_entry_by_uuid_responses.go pkg/generated/client/entries/search_log_query_parameters.go pkg/generated/client/entries/search_log_query_responses.go pkg/generated/client/index/index_client.go pkg/generated/client/index/search_index_parameters.go pkg/generated/client/index/search_index_responses.go pkg/generated/client/pubkey/get_public_key_parameters.go pkg/generated/client/pubkey/get_public_key_responses.go pkg/generated/client/pubkey/pubkey_client.go pkg/generated/client/rekor_client.go pkg/generated/client/server/get_rekor_version_parameters.go pkg/generated/client/server/get_rekor_version_responses.go pkg/generated/client/server/server_client.go pkg/generated/client/tlog/get_log_info_parameters.go pkg/generated/client/tlog/get_log_info_responses.go pkg/generated/client/tlog/get_log_proof_parameters.go pkg/generated/client/tlog/get_log_proof_responses.go pkg/generated/client/tlog/tlog_client.go pkg/generated/models/alpine.go pkg/generated/models/alpine_schema.go pkg/generated/models/alpine_v001_schema.go pkg/generated/models/consistency_proof.go pkg/generated/models/cose.go pkg/generated/models/cose_schema.go pkg/generated/models/cose_v001_schema.go pkg/generated/models/error.go pkg/generated/models/hashedrekord.go pkg/generated/models/hashedrekord_schema.go pkg/generated/models/hashedrekord_v001_schema.go pkg/generated/models/helm.go pkg/generated/models/helm_schema.go pkg/generated/models/helm_v001_schema.go pkg/generated/models/inactive_shard_log_info.go pkg/generated/models/inclusion_proof.go pkg/generated/models/intoto.go pkg/generated/models/intoto_schema.go pkg/generated/models/intoto_v001_schema.go pkg/generated/models/jar.go pkg/generated/models/jar_schema.go pkg/generated/models/jar_v001_schema.go pkg/generated/models/log_entry.go pkg/generated/models/log_info.go pkg/generated/models/proposed_entry.go pkg/generated/models/rekord.go pkg/generated/models/rekord_schema.go pkg/generated/models/rekord_v001_schema.go pkg/generated/models/rekor_version.go pkg/generated/models/rfc3161.go pkg/generated/models/rfc3161_schema.go pkg/generated/models/rfc3161_v001_schema.go pkg/generated/models/rpm.go pkg/generated/models/rpm_schema.go pkg/generated/models/rpm_v001_schema.go pkg/generated/models/search_index.go pkg/generated/models/search_log_query.go pkg/generated/models/tuf.go pkg/generated/models/tuf_schema.go pkg/generated/models/tuf_v001_schema.go pkg/generated/restapi/doc.go pkg/generated/restapi/embedded_spec.go pkg/generated/restapi/operations/entries/create_log_entry.go pkg/generated/restapi/operations/entries/create_log_entry_parameters.go pkg/generated/restapi/operations/entries/create_log_entry_responses.go pkg/generated/restapi/operations/entries/create_log_entry_urlbuilder.go pkg/generated/restapi/operations/entries/get_log_entry_by_index.go pkg/generated/restapi/operations/entries/get_log_entry_by_index_parameters.go pkg/generated/restapi/operations/entries/get_log_entry_by_index_responses.go pkg/generated/restapi/operations/entries/get_log_entry_by_index_urlbuilder.go pkg/generated/restapi/operations/entries/get_log_entry_by_uuid.go pkg/generated/restapi/operations/entries/get_log_entry_by_uuid_parameters.go pkg/generated/restapi/operations/entries/get_log_entry_by_uuid_responses.go pkg/generated/restapi/operations/entries/get_log_entry_by_uuid_urlbuilder.go pkg/generated/restapi/operations/entries/search_log_query.go pkg/generated/restapi/operations/entries/search_log_query_parameters.go pkg/generated/restapi/operations/entries/search_log_query_responses.go pkg/generated/restapi/operations/entries/search_log_query_urlbuilder.go pkg/generated/restapi/operations/index/search_index.go pkg/generated/restapi/operations/index/search_index_parameters.go pkg/generated/restapi/operations/index/search_index_responses.go pkg/generated/restapi/operations/index/search_index_urlbuilder.go pkg/generated/restapi/operations/pubkey/get_public_key.go pkg/generated/restapi/operations/pubkey/get_public_key_parameters.go pkg/generated/restapi/operations/pubkey/get_public_key_responses.go pkg/generated/restapi/operations/pubkey/get_public_key_urlbuilder.go pkg/generated/restapi/operations/rekor_server_api.go pkg/generated/restapi/operations/server/get_rekor_version.go pkg/generated/restapi/operations/server/get_rekor_version_parameters.go pkg/generated/restapi/operations/server/get_rekor_version_responses.go pkg/generated/restapi/operations/server/get_rekor_version_urlbuilder.go pkg/generated/restapi/operations/tlog/get_log_info.go pkg/generated/restapi/operations/tlog/get_log_info_parameters.go pkg/generated/restapi/operations/tlog/get_log_info_responses.go pkg/generated/restapi/operations/tlog/get_log_info_urlbuilder.go pkg/generated/restapi/operations/tlog/get_log_proof.go pkg/generated/restapi/operations/tlog/get_log_proof_parameters.go pkg/generated/restapi/operations/tlog/get_log_proof_responses.go pkg/generated/restapi/operations/tlog/get_log_proof_urlbuilder.go pkg/generated/restapi/server.go +SWAGGER_GEN=pkg/generated/client/entries/create_log_entry_parameters.go pkg/generated/client/entries/create_log_entry_responses.go pkg/generated/client/entries/entries_client.go pkg/generated/client/entries/get_log_entry_by_index_parameters.go pkg/generated/client/entries/get_log_entry_by_index_responses.go pkg/generated/client/entries/get_log_entry_by_uuid_parameters.go pkg/generated/client/entries/get_log_entry_by_uuid_responses.go pkg/generated/client/entries/search_log_query_parameters.go pkg/generated/client/entries/search_log_query_responses.go pkg/generated/client/index/index_client.go pkg/generated/client/index/search_index_parameters.go pkg/generated/client/index/search_index_responses.go pkg/generated/client/pubkey/get_public_key_parameters.go pkg/generated/client/pubkey/get_public_key_responses.go pkg/generated/client/pubkey/pubkey_client.go pkg/generated/client/rekor_client.go pkg/generated/client/server/get_rekor_version_parameters.go pkg/generated/client/server/get_rekor_version_responses.go pkg/generated/client/server/server_client.go pkg/generated/client/tlog/get_log_info_parameters.go pkg/generated/client/tlog/get_log_info_responses.go pkg/generated/client/tlog/get_log_proof_parameters.go pkg/generated/client/tlog/get_log_proof_responses.go pkg/generated/client/tlog/tlog_client.go pkg/generated/models/alpine.go pkg/generated/models/alpine_schema.go pkg/generated/models/alpine_v001_schema.go pkg/generated/models/consistency_proof.go pkg/generated/models/cose.go pkg/generated/models/cose_schema.go pkg/generated/models/cose_v001_schema.go pkg/generated/models/error.go pkg/generated/models/hashedrekord.go pkg/generated/models/hashedrekord_schema.go pkg/generated/models/hashedrekord_v001_schema.go pkg/generated/models/helm.go pkg/generated/models/helm_schema.go pkg/generated/models/helm_v001_schema.go pkg/generated/models/inactive_shard_log_info.go pkg/generated/models/inclusion_proof.go pkg/generated/models/intoto.go pkg/generated/models/intoto_schema.go pkg/generated/models/intoto_v001_schema.go pkg/generated/models/intoto_v002_schema.go pkg/generated/models/jar.go pkg/generated/models/jar_schema.go pkg/generated/models/jar_v001_schema.go pkg/generated/models/log_entry.go pkg/generated/models/log_info.go pkg/generated/models/proposed_entry.go pkg/generated/models/rekord.go pkg/generated/models/rekord_schema.go pkg/generated/models/rekord_v001_schema.go pkg/generated/models/rekor_version.go pkg/generated/models/rfc3161.go pkg/generated/models/rfc3161_schema.go pkg/generated/models/rfc3161_v001_schema.go pkg/generated/models/rpm.go pkg/generated/models/rpm_schema.go pkg/generated/models/rpm_v001_schema.go pkg/generated/models/search_index.go pkg/generated/models/search_log_query.go pkg/generated/models/tuf.go pkg/generated/models/tuf_schema.go pkg/generated/models/tuf_v001_schema.go pkg/generated/restapi/doc.go pkg/generated/restapi/embedded_spec.go pkg/generated/restapi/operations/entries/create_log_entry.go pkg/generated/restapi/operations/entries/create_log_entry_parameters.go pkg/generated/restapi/operations/entries/create_log_entry_responses.go pkg/generated/restapi/operations/entries/create_log_entry_urlbuilder.go pkg/generated/restapi/operations/entries/get_log_entry_by_index.go pkg/generated/restapi/operations/entries/get_log_entry_by_index_parameters.go pkg/generated/restapi/operations/entries/get_log_entry_by_index_responses.go pkg/generated/restapi/operations/entries/get_log_entry_by_index_urlbuilder.go pkg/generated/restapi/operations/entries/get_log_entry_by_uuid.go pkg/generated/restapi/operations/entries/get_log_entry_by_uuid_parameters.go pkg/generated/restapi/operations/entries/get_log_entry_by_uuid_responses.go pkg/generated/restapi/operations/entries/get_log_entry_by_uuid_urlbuilder.go pkg/generated/restapi/operations/entries/search_log_query.go pkg/generated/restapi/operations/entries/search_log_query_parameters.go pkg/generated/restapi/operations/entries/search_log_query_responses.go pkg/generated/restapi/operations/entries/search_log_query_urlbuilder.go pkg/generated/restapi/operations/index/search_index.go pkg/generated/restapi/operations/index/search_index_parameters.go pkg/generated/restapi/operations/index/search_index_responses.go pkg/generated/restapi/operations/index/search_index_urlbuilder.go pkg/generated/restapi/operations/pubkey/get_public_key.go pkg/generated/restapi/operations/pubkey/get_public_key_parameters.go pkg/generated/restapi/operations/pubkey/get_public_key_responses.go pkg/generated/restapi/operations/pubkey/get_public_key_urlbuilder.go pkg/generated/restapi/operations/rekor_server_api.go pkg/generated/restapi/operations/server/get_rekor_version.go pkg/generated/restapi/operations/server/get_rekor_version_parameters.go pkg/generated/restapi/operations/server/get_rekor_version_responses.go pkg/generated/restapi/operations/server/get_rekor_version_urlbuilder.go pkg/generated/restapi/operations/tlog/get_log_info.go pkg/generated/restapi/operations/tlog/get_log_info_parameters.go pkg/generated/restapi/operations/tlog/get_log_info_responses.go pkg/generated/restapi/operations/tlog/get_log_info_urlbuilder.go pkg/generated/restapi/operations/tlog/get_log_proof.go pkg/generated/restapi/operations/tlog/get_log_proof_parameters.go pkg/generated/restapi/operations/tlog/get_log_proof_responses.go pkg/generated/restapi/operations/tlog/get_log_proof_urlbuilder.go pkg/generated/restapi/server.go diff --git a/cmd/rekor-cli/app/pflag_groups.go b/cmd/rekor-cli/app/pflag_groups.go index 9cf7818e1..ccbf2be62 100644 --- a/cmd/rekor-cli/app/pflag_groups.go +++ b/cmd/rekor-cli/app/pflag_groups.go @@ -69,7 +69,7 @@ func addArtifactPFlags(cmd *cobra.Command) error { false, }, "public-key": { - fileOrURLFlag, + multiFileOrURLFlag, "path or URL to public key file", false, }, @@ -149,12 +149,18 @@ func CreatePropsFromPflags() *types.ArtifactProperties { } publicKeyString := viper.GetString("public-key") - if publicKeyString != "" { - if isURL(publicKeyString) { - props.PublicKeyPath, _ = url.Parse(publicKeyString) - } else { - props.PublicKeyPath = &url.URL{Path: publicKeyString} + splitPubKeyString := strings.Split(publicKeyString, ",") + if len(splitPubKeyString) > 0 { + collectedKeys := []*url.URL{} + for _, key := range splitPubKeyString { + if isURL(key) { + keyPath, _ := url.Parse(key) + collectedKeys = append(collectedKeys, keyPath) + } else { + collectedKeys = append(collectedKeys, &url.URL{Path: key}) + } } + props.PublicKeyPaths = collectedKeys } props.PKIFormat = viper.GetString("pki-format") diff --git a/cmd/rekor-cli/app/pflags.go b/cmd/rekor-cli/app/pflags.go index 21f16021f..1a04b5ae4 100644 --- a/cmd/rekor-cli/app/pflags.go +++ b/cmd/rekor-cli/app/pflags.go @@ -35,20 +35,21 @@ import ( type FlagType string const ( - uuidFlag FlagType = "uuid" - shaFlag FlagType = "sha" - emailFlag FlagType = "email" - operatorFlag FlagType = "operator" - logIndexFlag FlagType = "logIndex" - pkiFormatFlag FlagType = "pkiFormat" - typeFlag FlagType = "type" - fileFlag FlagType = "file" - urlFlag FlagType = "url" - fileOrURLFlag FlagType = "fileOrURL" - oidFlag FlagType = "oid" - formatFlag FlagType = "format" - timeoutFlag FlagType = "timeout" - base64Flag FlagType = "base64" + uuidFlag FlagType = "uuid" + shaFlag FlagType = "sha" + emailFlag FlagType = "email" + operatorFlag FlagType = "operator" + logIndexFlag FlagType = "logIndex" + pkiFormatFlag FlagType = "pkiFormat" + typeFlag FlagType = "type" + fileFlag FlagType = "file" + urlFlag FlagType = "url" + fileOrURLFlag FlagType = "fileOrURL" + multiFileOrURLFlag FlagType = "multiFileOrURL" + oidFlag FlagType = "oid" + formatFlag FlagType = "format" + timeoutFlag FlagType = "timeout" + base64Flag FlagType = "base64" ) type newPFlagValueFunc func() pflag.Value @@ -100,6 +101,10 @@ func initializePFlagMap() { // applies logic of fileFlag OR urlFlag validators from above return valueFactory(fileOrURLFlag, validateFileOrURL, "") }, + multiFileOrURLFlag: func() pflag.Value { + // applies logic of fileFlag OR urlFlag validators from above for multi file and URL + return multiValueFactory(multiFileOrURLFlag, validateFileOrURL, []string{}) + }, oidFlag: func() pflag.Value { // this validates for an OID, which is a sequence of positive integers separated by periods return valueFactory(oidFlag, validateOID, "") @@ -142,6 +147,38 @@ func valueFactory(flagType FlagType, v validationFunc, defaultVal string) pflag. } } +func multiValueFactory(flagType FlagType, v validationFunc, defaultVal []string) pflag.Value { + return &multiBaseValue{ + flagType: flagType, + validationFunc: v, + value: defaultVal, + } +} + +// multiBaseValue implements pflag.Value +type multiBaseValue struct { + flagType FlagType + value []string + validationFunc validationFunc +} + +func (b *multiBaseValue) String() string { + return strings.Join(b.value, ",") +} + +// Type returns the type of this Value +func (b multiBaseValue) Type() string { + return string(b.flagType) +} + +func (b *multiBaseValue) Set(value string) error { + if err := b.validationFunc(value); err != nil { + return err + } + b.value = append(b.value, value) + return nil +} + // baseValue implements pflag.Value type baseValue struct { flagType FlagType diff --git a/cmd/rekor-cli/app/pflags_test.go b/cmd/rekor-cli/app/pflags_test.go index 64887c4aa..c48efc3f9 100644 --- a/cmd/rekor-cli/app/pflags_test.go +++ b/cmd/rekor-cli/app/pflags_test.go @@ -37,6 +37,7 @@ func TestArtifactPFlags(t *testing.T) { artifact string signature string publicKey string + multiPublicKey []string uuid string aad string uuidRequired bool @@ -373,6 +374,22 @@ func TestArtifactPFlags(t *testing.T) { expectParseSuccess: true, expectValidateSuccess: false, }, + { + caseDesc: "valid intoto - one keys", + typeStr: "intoto", + artifact: "../../../tests/intoto_dsse.json", + publicKey: "../../../tests/intoto_dsse.pem", + expectParseSuccess: true, + expectValidateSuccess: true, + }, + { + caseDesc: "valid intoto - multi keys", + typeStr: "intoto", + artifact: "../../../tests/intoto_multi_dsse.json", + multiPublicKey: []string{"../../../tests/intoto_dsse.pem", "../../../tests/intoto_multi_pub2.pem"}, + expectParseSuccess: true, + expectValidateSuccess: true, + }, } for _, tc := range tests { @@ -405,6 +422,11 @@ func TestArtifactPFlags(t *testing.T) { if tc.publicKey != "" { args = append(args, "--public-key", tc.publicKey) } + if len(tc.multiPublicKey) > 0 { + for _, key := range tc.multiPublicKey { + args = append(args, "--public-key", key) + } + } if tc.uuid != "" { args = append(args, "--uuid", tc.uuid) } @@ -740,6 +762,11 @@ func TestParseTypeFlag(t *testing.T) { { caseDesc: "explicit intoto v0.0.1", typeStr: "intoto:0.0.1", + expectSuccess: false, + }, + { + caseDesc: "explicit intoto v0.0.2", + typeStr: "intoto:0.0.2", expectSuccess: true, }, { diff --git a/cmd/rekor-cli/app/root.go b/cmd/rekor-cli/app/root.go index 400b9acb4..2cb38e282 100644 --- a/cmd/rekor-cli/app/root.go +++ b/cmd/rekor-cli/app/root.go @@ -32,6 +32,7 @@ import ( _ "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/intoto/v0.0.2" _ "github.com/sigstore/rekor/pkg/types/jar/v0.0.1" _ "github.com/sigstore/rekor/pkg/types/rekord/v0.0.1" _ "github.com/sigstore/rekor/pkg/types/rfc3161/v0.0.1" diff --git a/cmd/rekor-cli/app/search.go b/cmd/rekor-cli/app/search.go index a33c127f6..1e85dbd8b 100644 --- a/cmd/rekor-cli/app/search.go +++ b/cmd/rekor-cli/app/search.go @@ -164,15 +164,20 @@ var searchCmd = &cobra.Command{ default: return nil, fmt.Errorf("unknown pki-format %v", pkiFormat) } - publicKeyStr := viper.GetString("public-key") - if isURL(publicKeyStr) { - params.Query.PublicKey.URL = strfmt.URI(publicKeyStr) - } else { - keyBytes, err := ioutil.ReadFile(filepath.Clean(publicKeyStr)) - if err != nil { - return nil, fmt.Errorf("error reading public key file: %w", err) + + splitPubKeyString := strings.Split(publicKeyStr, ",") + if len(splitPubKeyString) == 1 { + if isURL(splitPubKeyString[0]) { + params.Query.PublicKey.URL = strfmt.URI(splitPubKeyString[0]) + } else { + keyBytes, err := ioutil.ReadFile(filepath.Clean(splitPubKeyString[0])) + if err != nil { + return nil, fmt.Errorf("error reading public key file: %w", err) + } + params.Query.PublicKey.Content = strfmt.Base64(keyBytes) } - params.Query.PublicKey.Content = strfmt.Base64(keyBytes) + } else { + return nil, errors.New("only one public key must be provided") } } diff --git a/cmd/rekor-server/app/serve.go b/cmd/rekor-server/app/serve.go index b3534ef5f..536443e8e 100644 --- a/cmd/rekor-server/app/serve.go +++ b/cmd/rekor-server/app/serve.go @@ -39,6 +39,7 @@ import ( helm_v001 "github.com/sigstore/rekor/pkg/types/helm/v0.0.1" "github.com/sigstore/rekor/pkg/types/intoto" intoto_v001 "github.com/sigstore/rekor/pkg/types/intoto/v0.0.1" + intoto_v002 "github.com/sigstore/rekor/pkg/types/intoto/v0.0.2" "github.com/sigstore/rekor/pkg/types/jar" jar_v001 "github.com/sigstore/rekor/pkg/types/jar/v0.0.1" "github.com/sigstore/rekor/pkg/types/rekord" @@ -84,17 +85,17 @@ var serveCmd = &cobra.Command{ //TODO: add command line option to print versions supported in binary // 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, - cose.KIND: cose_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, + pluggableTypeMap := map[string][]string{ + rekord.KIND: {rekord_v001.APIVERSION}, + rpm.KIND: {rpm_v001.APIVERSION}, + jar.KIND: {jar_v001.APIVERSION}, + intoto.KIND: {intoto_v001.APIVERSION, intoto_v002.APIVERSION}, + cose.KIND: {cose_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/pkg/generated/models/intoto_v002_schema.go b/pkg/generated/models/intoto_v002_schema.go new file mode 100644 index 000000000..3e3b7bb69 --- /dev/null +++ b/pkg/generated/models/intoto_v002_schema.go @@ -0,0 +1,724 @@ +// 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" +) + +// IntotoV002Schema intoto v0.0.2 Schema +// +// Schema for intoto object +// +// swagger:model intotoV002Schema +type IntotoV002Schema struct { + + // content + // Required: true + Content *IntotoV002SchemaContent `json:"content"` +} + +// Validate validates this intoto v002 schema +func (m *IntotoV002Schema) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateContent(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *IntotoV002Schema) validateContent(formats strfmt.Registry) error { + + if err := validate.Required("content", "body", m.Content); err != nil { + return err + } + + if m.Content != nil { + if err := m.Content.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("content") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("content") + } + return err + } + } + + return nil +} + +// ContextValidate validate this intoto v002 schema based on the context it is used +func (m *IntotoV002Schema) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateContent(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *IntotoV002Schema) contextValidateContent(ctx context.Context, formats strfmt.Registry) error { + + if m.Content != nil { + if err := m.Content.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("content") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("content") + } + return err + } + } + + return nil +} + +// MarshalBinary interface implementation +func (m *IntotoV002Schema) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *IntotoV002Schema) UnmarshalBinary(b []byte) error { + var res IntotoV002Schema + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} + +// IntotoV002SchemaContent intoto v002 schema content +// +// swagger:model IntotoV002SchemaContent +type IntotoV002SchemaContent struct { + + // envelope + Envelope *IntotoV002SchemaContentEnvelope `json:"envelope,omitempty"` + + // hash + Hash *IntotoV002SchemaContentHash `json:"hash,omitempty"` + + // payload hash + PayloadHash *IntotoV002SchemaContentPayloadHash `json:"payloadHash,omitempty"` +} + +// Validate validates this intoto v002 schema content +func (m *IntotoV002SchemaContent) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateEnvelope(formats); err != nil { + res = append(res, err) + } + + if err := m.validateHash(formats); err != nil { + res = append(res, err) + } + + if err := m.validatePayloadHash(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *IntotoV002SchemaContent) validateEnvelope(formats strfmt.Registry) error { + if swag.IsZero(m.Envelope) { // not required + return nil + } + + if m.Envelope != nil { + if err := m.Envelope.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("content" + "." + "envelope") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("content" + "." + "envelope") + } + return err + } + } + + return nil +} + +func (m *IntotoV002SchemaContent) 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("content" + "." + "hash") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("content" + "." + "hash") + } + return err + } + } + + return nil +} + +func (m *IntotoV002SchemaContent) validatePayloadHash(formats strfmt.Registry) error { + if swag.IsZero(m.PayloadHash) { // not required + return nil + } + + if m.PayloadHash != nil { + if err := m.PayloadHash.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("content" + "." + "payloadHash") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("content" + "." + "payloadHash") + } + return err + } + } + + return nil +} + +// ContextValidate validate this intoto v002 schema content based on the context it is used +func (m *IntotoV002SchemaContent) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateEnvelope(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateHash(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidatePayloadHash(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *IntotoV002SchemaContent) contextValidateEnvelope(ctx context.Context, formats strfmt.Registry) error { + + if m.Envelope != nil { + if err := m.Envelope.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("content" + "." + "envelope") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("content" + "." + "envelope") + } + return err + } + } + + return nil +} + +func (m *IntotoV002SchemaContent) 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("content" + "." + "hash") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("content" + "." + "hash") + } + return err + } + } + + return nil +} + +func (m *IntotoV002SchemaContent) 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("content" + "." + "payloadHash") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("content" + "." + "payloadHash") + } + return err + } + } + + return nil +} + +// MarshalBinary interface implementation +func (m *IntotoV002SchemaContent) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *IntotoV002SchemaContent) UnmarshalBinary(b []byte) error { + var res IntotoV002SchemaContent + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} + +// IntotoV002SchemaContentEnvelope dsse envelope +// +// swagger:model IntotoV002SchemaContentEnvelope +type IntotoV002SchemaContentEnvelope struct { + + // payload of the envelope + // Format: byte + Payload strfmt.Base64 `json:"payload,omitempty"` + + // type describing the payload + // Required: true + PayloadType *string `json:"payloadType"` + + // collection of all signatures of the envelope's payload + // Required: true + // Min Items: 1 + Signatures []*IntotoV002SchemaContentEnvelopeSignaturesItems0 `json:"signatures"` +} + +// Validate validates this intoto v002 schema content envelope +func (m *IntotoV002SchemaContentEnvelope) Validate(formats strfmt.Registry) error { + var res []error + + 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 *IntotoV002SchemaContentEnvelope) validatePayloadType(formats strfmt.Registry) error { + + if err := validate.Required("content"+"."+"envelope"+"."+"payloadType", "body", m.PayloadType); err != nil { + return err + } + + return nil +} + +func (m *IntotoV002SchemaContentEnvelope) validateSignatures(formats strfmt.Registry) error { + + if err := validate.Required("content"+"."+"envelope"+"."+"signatures", "body", m.Signatures); err != nil { + return err + } + + iSignaturesSize := int64(len(m.Signatures)) + + if err := validate.MinItems("content"+"."+"envelope"+"."+"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("content" + "." + "envelope" + "." + "signatures" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("content" + "." + "envelope" + "." + "signatures" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// ContextValidate validate this intoto v002 schema content envelope based on the context it is used +func (m *IntotoV002SchemaContentEnvelope) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateSignatures(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *IntotoV002SchemaContentEnvelope) 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("content" + "." + "envelope" + "." + "signatures" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("content" + "." + "envelope" + "." + "signatures" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// MarshalBinary interface implementation +func (m *IntotoV002SchemaContentEnvelope) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *IntotoV002SchemaContentEnvelope) UnmarshalBinary(b []byte) error { + var res IntotoV002SchemaContentEnvelope + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} + +// IntotoV002SchemaContentEnvelopeSignaturesItems0 a signature of the envelope's payload along with the public key for the signature +// +// swagger:model IntotoV002SchemaContentEnvelopeSignaturesItems0 +type IntotoV002SchemaContentEnvelopeSignaturesItems0 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 intoto v002 schema content envelope signatures items0 +func (m *IntotoV002SchemaContentEnvelopeSignaturesItems0) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validate this intoto v002 schema content envelope signatures items0 based on the context it is used +func (m *IntotoV002SchemaContentEnvelopeSignaturesItems0) 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 *IntotoV002SchemaContentEnvelopeSignaturesItems0) 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 *IntotoV002SchemaContentEnvelopeSignaturesItems0) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *IntotoV002SchemaContentEnvelopeSignaturesItems0) UnmarshalBinary(b []byte) error { + var res IntotoV002SchemaContentEnvelopeSignaturesItems0 + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} + +// IntotoV002SchemaContentHash Specifies the hash algorithm and value encompassing the entire signed envelope +// +// swagger:model IntotoV002SchemaContentHash +type IntotoV002SchemaContentHash struct { + + // The hashing function used to compute the hash value + // Required: true + // Enum: [sha256] + Algorithm *string `json:"algorithm"` + + // The hash value for the archive + // Required: true + Value *string `json:"value"` +} + +// Validate validates this intoto v002 schema content hash +func (m *IntotoV002SchemaContentHash) 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 intotoV002SchemaContentHashTypeAlgorithmPropEnum []interface{} + +func init() { + var res []string + if err := json.Unmarshal([]byte(`["sha256"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + intotoV002SchemaContentHashTypeAlgorithmPropEnum = append(intotoV002SchemaContentHashTypeAlgorithmPropEnum, v) + } +} + +const ( + + // IntotoV002SchemaContentHashAlgorithmSha256 captures enum value "sha256" + IntotoV002SchemaContentHashAlgorithmSha256 string = "sha256" +) + +// prop value enum +func (m *IntotoV002SchemaContentHash) validateAlgorithmEnum(path, location string, value string) error { + if err := validate.EnumCase(path, location, value, intotoV002SchemaContentHashTypeAlgorithmPropEnum, true); err != nil { + return err + } + return nil +} + +func (m *IntotoV002SchemaContentHash) validateAlgorithm(formats strfmt.Registry) error { + + if err := validate.Required("content"+"."+"hash"+"."+"algorithm", "body", m.Algorithm); err != nil { + return err + } + + // value enum + if err := m.validateAlgorithmEnum("content"+"."+"hash"+"."+"algorithm", "body", *m.Algorithm); err != nil { + return err + } + + return nil +} + +func (m *IntotoV002SchemaContentHash) validateValue(formats strfmt.Registry) error { + + if err := validate.Required("content"+"."+"hash"+"."+"value", "body", m.Value); err != nil { + return err + } + + return nil +} + +// ContextValidate validate this intoto v002 schema content hash based on the context it is used +func (m *IntotoV002SchemaContentHash) 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 *IntotoV002SchemaContentHash) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *IntotoV002SchemaContentHash) UnmarshalBinary(b []byte) error { + var res IntotoV002SchemaContentHash + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} + +// IntotoV002SchemaContentPayloadHash Specifies the hash algorithm and value covering the payload within the DSSE envelope +// +// swagger:model IntotoV002SchemaContentPayloadHash +type IntotoV002SchemaContentPayloadHash struct { + + // The hashing function used to compute the hash value + // Required: true + // Enum: [sha256] + Algorithm *string `json:"algorithm"` + + // The hash value of the payload + // Required: true + Value *string `json:"value"` +} + +// Validate validates this intoto v002 schema content payload hash +func (m *IntotoV002SchemaContentPayloadHash) 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 intotoV002SchemaContentPayloadHashTypeAlgorithmPropEnum []interface{} + +func init() { + var res []string + if err := json.Unmarshal([]byte(`["sha256"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + intotoV002SchemaContentPayloadHashTypeAlgorithmPropEnum = append(intotoV002SchemaContentPayloadHashTypeAlgorithmPropEnum, v) + } +} + +const ( + + // IntotoV002SchemaContentPayloadHashAlgorithmSha256 captures enum value "sha256" + IntotoV002SchemaContentPayloadHashAlgorithmSha256 string = "sha256" +) + +// prop value enum +func (m *IntotoV002SchemaContentPayloadHash) validateAlgorithmEnum(path, location string, value string) error { + if err := validate.EnumCase(path, location, value, intotoV002SchemaContentPayloadHashTypeAlgorithmPropEnum, true); err != nil { + return err + } + return nil +} + +func (m *IntotoV002SchemaContentPayloadHash) validateAlgorithm(formats strfmt.Registry) error { + + if err := validate.Required("content"+"."+"payloadHash"+"."+"algorithm", "body", m.Algorithm); err != nil { + return err + } + + // value enum + if err := m.validateAlgorithmEnum("content"+"."+"payloadHash"+"."+"algorithm", "body", *m.Algorithm); err != nil { + return err + } + + return nil +} + +func (m *IntotoV002SchemaContentPayloadHash) validateValue(formats strfmt.Registry) error { + + if err := validate.Required("content"+"."+"payloadHash"+"."+"value", "body", m.Value); err != nil { + return err + } + + return nil +} + +// ContextValidate validate this intoto v002 schema content payload hash based on the context it is used +func (m *IntotoV002SchemaContentPayloadHash) 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 *IntotoV002SchemaContentPayloadHash) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *IntotoV002SchemaContentPayloadHash) UnmarshalBinary(b []byte) error { + var res IntotoV002SchemaContentPayloadHash + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/pkg/generated/restapi/embedded_spec.go b/pkg/generated/restapi/embedded_spec.go index 792cfaade..708261d4e 100644 --- a/pkg/generated/restapi/embedded_spec.go +++ b/pkg/generated/restapi/embedded_spec.go @@ -1958,6 +1958,176 @@ func init() { }, "readOnly": true }, + "IntotoV002SchemaContent": { + "type": "object", + "properties": { + "envelope": { + "description": "dsse envelope", + "type": "object", + "required": [ + "payloadType", + "signatures" + ], + "properties": { + "payload": { + "description": "payload of the envelope", + "type": "string", + "format": "byte", + "writeOnly": true + }, + "payloadType": { + "description": "type describing the payload", + "type": "string" + }, + "signatures": { + "description": "collection of all signatures of the envelope's payload", + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/IntotoV002SchemaContentEnvelopeSignaturesItems0" + } + } + } + }, + "hash": { + "description": "Specifies the hash algorithm and value encompassing the entire signed envelope", + "type": "object", + "required": [ + "algorithm", + "value" + ], + "properties": { + "algorithm": { + "description": "The hashing function used to compute the hash value", + "type": "string", + "enum": [ + "sha256" + ] + }, + "value": { + "description": "The hash value for the archive", + "type": "string" + } + }, + "readOnly": true + }, + "payloadHash": { + "description": "Specifies the hash algorithm and value covering the payload within the DSSE envelope", + "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 of the payload", + "type": "string" + } + }, + "readOnly": true + } + } + }, + "IntotoV002SchemaContentEnvelope": { + "description": "dsse envelope", + "type": "object", + "required": [ + "payloadType", + "signatures" + ], + "properties": { + "payload": { + "description": "payload of the envelope", + "type": "string", + "format": "byte", + "writeOnly": true + }, + "payloadType": { + "description": "type describing the payload", + "type": "string" + }, + "signatures": { + "description": "collection of all signatures of the envelope's payload", + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/IntotoV002SchemaContentEnvelopeSignaturesItems0" + } + } + } + }, + "IntotoV002SchemaContentEnvelopeSignaturesItems0": { + "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" + } + } + }, + "IntotoV002SchemaContentHash": { + "description": "Specifies the hash algorithm and value encompassing the entire signed envelope", + "type": "object", + "required": [ + "algorithm", + "value" + ], + "properties": { + "algorithm": { + "description": "The hashing function used to compute the hash value", + "type": "string", + "enum": [ + "sha256" + ] + }, + "value": { + "description": "The hash value for the archive", + "type": "string" + } + }, + "readOnly": true + }, + "IntotoV002SchemaContentPayloadHash": { + "description": "Specifies the hash algorithm and value covering the payload within the DSSE envelope", + "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 of the payload", + "type": "string" + } + }, + "readOnly": true + }, "JarV001SchemaArchive": { "description": "Information about the archive associated with the entry", "type": "object", @@ -3074,6 +3244,9 @@ func init() { "oneOf": [ { "$ref": "#/definitions/intotoV001Schema" + }, + { + "$ref": "#/definitions/intotoV002Schema" } ], "$schema": "http://json-schema.org/draft-07/schema", @@ -3151,6 +3324,95 @@ func init() { "$schema": "http://json-schema.org/draft-07/schema", "$id": "http://rekor.sigstore.dev/types/intoto/intoto_v0_0_1_schema.json" }, + "intotoV002Schema": { + "description": "Schema for intoto object", + "type": "object", + "title": "intoto v0.0.2 Schema", + "required": [ + "content" + ], + "properties": { + "content": { + "type": "object", + "properties": { + "envelope": { + "description": "dsse envelope", + "type": "object", + "required": [ + "payloadType", + "signatures" + ], + "properties": { + "payload": { + "description": "payload of the envelope", + "type": "string", + "format": "byte", + "writeOnly": true + }, + "payloadType": { + "description": "type describing the payload", + "type": "string" + }, + "signatures": { + "description": "collection of all signatures of the envelope's payload", + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/IntotoV002SchemaContentEnvelopeSignaturesItems0" + } + } + } + }, + "hash": { + "description": "Specifies the hash algorithm and value encompassing the entire signed envelope", + "type": "object", + "required": [ + "algorithm", + "value" + ], + "properties": { + "algorithm": { + "description": "The hashing function used to compute the hash value", + "type": "string", + "enum": [ + "sha256" + ] + }, + "value": { + "description": "The hash value for the archive", + "type": "string" + } + }, + "readOnly": true + }, + "payloadHash": { + "description": "Specifies the hash algorithm and value covering the payload within the DSSE envelope", + "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 of the payload", + "type": "string" + } + }, + "readOnly": true + } + } + } + }, + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://rekor.sigstore.dev/types/intoto/intoto_v0_0_2_schema.json" + }, "jar": { "description": "Java Archive (JAR)", "type": "object", diff --git a/pkg/types/alpine/v0.0.1/entry.go b/pkg/types/alpine/v0.0.1/entry.go index 3a68cebe1..09da6c9e6 100644 --- a/pkg/types/alpine/v0.0.1/entry.go +++ b/pkg/types/alpine/v0.0.1/entry.go @@ -322,16 +322,21 @@ func (v V001Entry) CreateFromArtifactProperties(ctx context.Context, props types re.AlpineModel.PublicKey = &models.AlpineV001SchemaPublicKey{} publicKeyBytes := props.PublicKeyBytes - if publicKeyBytes == nil { - publicKeyBytes, err = ioutil.ReadFile(filepath.Clean(props.PublicKeyPath.Path)) + if len(publicKeyBytes) == 0 { + if len(props.PublicKeyPaths) != 1 { + return nil, errors.New("only one public key must be provided") + } + keyBytes, err := ioutil.ReadFile(filepath.Clean(props.PublicKeyPaths[0].Path)) if err != nil { return nil, fmt.Errorf("error reading public key file: %w", err) } - re.AlpineModel.PublicKey.Content = (*strfmt.Base64)(&publicKeyBytes) - } else { - re.AlpineModel.PublicKey.Content = (*strfmt.Base64)(&publicKeyBytes) + publicKeyBytes = append(publicKeyBytes, keyBytes) + } else if len(publicKeyBytes) != 1 { + return nil, errors.New("only one public key must be provided") } + re.AlpineModel.PublicKey.Content = (*strfmt.Base64)(&publicKeyBytes[0]) + if err := re.validate(); err != nil { return nil, err } diff --git a/pkg/types/cose/v0.0.1/entry.go b/pkg/types/cose/v0.0.1/entry.go index 7ddb5a508..48b4296cb 100644 --- a/pkg/types/cose/v0.0.1/entry.go +++ b/pkg/types/cose/v0.0.1/entry.go @@ -316,19 +316,20 @@ func (v V001Entry) CreateFromArtifactProperties(_ context.Context, props types.A } } publicKeyBytes := props.PublicKeyBytes - if publicKeyBytes == nil { - if props.PublicKeyPath == nil { - return nil, errors.New("public key must be provided to verify signature") + if len(publicKeyBytes) == 0 { + if len(props.PublicKeyPaths) != 1 { + return nil, errors.New("only one public key must be provided to verify signature") } - publicKeyBytes, err = ioutil.ReadFile(filepath.Clean(props.PublicKeyPath.Path)) + keyBytes, err := ioutil.ReadFile(filepath.Clean(props.PublicKeyPaths[0].Path)) if err != nil { return nil, fmt.Errorf("error reading public key file: %w", err) } + publicKeyBytes = append(publicKeyBytes, keyBytes) + } else if len(publicKeyBytes) != 1 { + return nil, errors.New("only one public key must be provided") } - if err != nil { - return nil, err - } - kb := strfmt.Base64(publicKeyBytes) + + kb := strfmt.Base64(publicKeyBytes[0]) mb := strfmt.Base64(messageBytes) re := V001Entry{ diff --git a/pkg/types/entries.go b/pkg/types/entries.go index cda7f2782..55559752d 100644 --- a/pkg/types/entries.go +++ b/pkg/types/entries.go @@ -152,7 +152,7 @@ type ArtifactProperties struct { ArtifactBytes []byte SignaturePath *url.URL SignatureBytes []byte - PublicKeyPath *url.URL - PublicKeyBytes []byte + PublicKeyPaths []*url.URL + PublicKeyBytes [][]byte PKIFormat string } diff --git a/pkg/types/hashedrekord/v0.0.1/entry.go b/pkg/types/hashedrekord/v0.0.1/entry.go index aea92fcb0..74d6654e8 100644 --- a/pkg/types/hashedrekord/v0.0.1/entry.go +++ b/pkg/types/hashedrekord/v0.0.1/entry.go @@ -217,17 +217,20 @@ func (v V001Entry) CreateFromArtifactProperties(ctx context.Context, props types 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") + if len(publicKeyBytes) == 0 { + if len(props.PublicKeyPaths) != 1 { + return nil, errors.New("only one public key must be provided to verify detached signature") } - publicKeyBytes, err = ioutil.ReadFile(filepath.Clean(props.PublicKeyPath.Path)) + keyBytes, err := ioutil.ReadFile(filepath.Clean(props.PublicKeyPaths[0].Path)) if err != nil { return nil, fmt.Errorf("error reading public key file: %w", err) } + publicKeyBytes = append(publicKeyBytes, keyBytes) + } else if len(publicKeyBytes) != 1 { + return nil, errors.New("only one public key must be provided") } - re.HashedRekordObj.Signature.PublicKey.Content = strfmt.Base64(publicKeyBytes) + re.HashedRekordObj.Signature.PublicKey.Content = strfmt.Base64(publicKeyBytes[0]) re.HashedRekordObj.Data.Hash = &models.HashedrekordV001SchemaDataHash{ Algorithm: swag.String(models.HashedrekordV001SchemaDataHashAlgorithmSha256), Value: swag.String(props.ArtifactHash), diff --git a/pkg/types/helm/v0.0.1/entry.go b/pkg/types/helm/v0.0.1/entry.go index 55f68fe30..000cb2c70 100644 --- a/pkg/types/helm/v0.0.1/entry.go +++ b/pkg/types/helm/v0.0.1/entry.go @@ -317,16 +317,20 @@ func (v V001Entry) CreateFromArtifactProperties(ctx context.Context, props types re.HelmObj.PublicKey = &models.HelmV001SchemaPublicKey{} publicKeyBytes := props.PublicKeyBytes - if publicKeyBytes == nil { - publicKeyBytes, err = ioutil.ReadFile(filepath.Clean(props.PublicKeyPath.Path)) + if len(publicKeyBytes) == 0 { + if len(props.PublicKeyPaths) != 1 { + return nil, errors.New("only one public key must be provided") + } + keyBytes, err := ioutil.ReadFile(filepath.Clean(props.PublicKeyPaths[0].Path)) if err != nil { return nil, fmt.Errorf("error reading public key file: %w", err) } - re.HelmObj.PublicKey.Content = (*strfmt.Base64)(&publicKeyBytes) - } else { - re.HelmObj.PublicKey.Content = (*strfmt.Base64)(&publicKeyBytes) + publicKeyBytes = append(publicKeyBytes, keyBytes) + } else if len(publicKeyBytes) != 1 { + return nil, errors.New("only one public key must be provided") } + re.HelmObj.PublicKey.Content = (*strfmt.Base64)(&publicKeyBytes[0]) if err := re.validate(); err != nil { return nil, err } diff --git a/pkg/types/intoto/intoto.go b/pkg/types/intoto/intoto.go index f48daacbe..81d2ceeaa 100644 --- a/pkg/types/intoto/intoto.go +++ b/pkg/types/intoto/intoto.go @@ -22,6 +22,7 @@ import ( "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/rekor/pkg/types" + "golang.org/x/exp/slices" ) const ( @@ -70,5 +71,17 @@ func (it *BaseIntotoType) CreateProposedEntry(ctx context.Context, version strin } func (it BaseIntotoType) DefaultVersion() string { - return "0.0.1" + return "0.0.2" +} + +// SupportedVersions returns the supported versions for this type; +// it deliberately omits 0.0.1 from the list of supported versions as that +// version did not persist signatures inside the log entry +func (it BaseIntotoType) SupportedVersions() []string { + return []string{"0.0.2"} +} + +// IsSupportedVersion returns true if the version can be inserted into the log, and false if not +func (it *BaseIntotoType) IsSupportedVersion(proposedVersion string) bool { + return slices.Contains(it.SupportedVersions(), proposedVersion) } diff --git a/pkg/types/intoto/intoto_schema.json b/pkg/types/intoto/intoto_schema.json index b99b3c2d9..16f6172af 100644 --- a/pkg/types/intoto/intoto_schema.json +++ b/pkg/types/intoto/intoto_schema.json @@ -7,6 +7,9 @@ "oneOf": [ { "$ref": "v0.0.1/intoto_v0_0_1_schema.json" + }, + { + "$ref": "v0.0.2/intoto_v0_0_2_schema.json" } ] } diff --git a/pkg/types/intoto/v0.0.1/entry.go b/pkg/types/intoto/v0.0.1/entry.go index 49e42ccea..438f08392 100644 --- a/pkg/types/intoto/v0.0.1/entry.go +++ b/pkg/types/intoto/v0.0.1/entry.go @@ -292,16 +292,20 @@ func (v V001Entry) CreateFromArtifactProperties(_ context.Context, props types.A } } publicKeyBytes := props.PublicKeyBytes - if publicKeyBytes == nil { - if props.PublicKeyPath == nil { - return nil, errors.New("public key must be provided to verify signature") + if len(publicKeyBytes) == 0 { + if len(props.PublicKeyPaths) != 1 { + return nil, errors.New("only one public key must be provided to verify signature") } - publicKeyBytes, err = ioutil.ReadFile(filepath.Clean(props.PublicKeyPath.Path)) + keyBytes, err := ioutil.ReadFile(filepath.Clean(props.PublicKeyPaths[0].Path)) if err != nil { return nil, fmt.Errorf("error reading public key file: %w", err) } + publicKeyBytes = append(publicKeyBytes, keyBytes) + } else if len(publicKeyBytes) != 1 { + return nil, errors.New("only one public key must be provided") } - kb := strfmt.Base64(publicKeyBytes) + + kb := strfmt.Base64(publicKeyBytes[0]) re := V001Entry{ IntotoObj: models.IntotoV001Schema{ diff --git a/pkg/types/intoto/v0.0.2/entry.go b/pkg/types/intoto/v0.0.2/entry.go new file mode 100644 index 000000000..7313cce39 --- /dev/null +++ b/pkg/types/intoto/v0.0.2/entry.go @@ -0,0 +1,423 @@ +// +// Copyright 2022 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package intoto + +import ( + "bytes" + "context" + "crypto" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "path/filepath" + "strings" + + "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" + "github.com/sigstore/rekor/pkg/types/intoto" + "github.com/sigstore/sigstore/pkg/signature" + "github.com/sigstore/sigstore/pkg/signature/options" +) + +const ( + APIVERSION = "0.0.2" +) + +func init() { + if err := intoto.VersionMap.SetEntryFactory(APIVERSION, NewEntry); err != nil { + log.Logger.Panic(err) + } +} + +type V002Entry struct { + IntotoObj models.IntotoV002Schema + env dsse.Envelope +} + +func (v V002Entry) APIVersion() string { + return APIVERSION +} + +func NewEntry() types.EntryImpl { + return &V002Entry{} +} + +func (v V002Entry) IndexKeys() ([]string, error) { + var result []string + + if v.IntotoObj.Content == nil || v.IntotoObj.Content.Envelope == nil { + log.Logger.Info("IntotoObj content or dsse envelope is nil") + return result, nil + } + + for _, sig := range v.IntotoObj.Content.Envelope.Signatures { + keyObj, err := x509.NewPublicKey(bytes.NewReader(sig.PublicKey)) + if err != nil { + return result, err + } + + canonKey, err := keyObj.CanonicalValue() + if err != nil { + return result, fmt.Errorf("could not canonicize key: %w", err) + } + + keyHash := sha256.Sum256(canonKey) + result = append(result, "sha256:"+hex.EncodeToString(keyHash[:])) + + result = append(result, keyObj.Subjects()...) + } + + payloadKey := strings.ToLower(fmt.Sprintf("%s:%s", *v.IntotoObj.Content.PayloadHash.Algorithm, *v.IntotoObj.Content.PayloadHash.Value)) + result = append(result, payloadKey) + + hashkey := strings.ToLower(fmt.Sprintf("%s:%s", *v.IntotoObj.Content.Hash.Algorithm, *v.IntotoObj.Content.Hash.Value)) + result = append(result, hashkey) + + switch *v.IntotoObj.Content.Envelope.PayloadType { + case in_toto.PayloadType: + + if v.IntotoObj.Content.Envelope.Payload == nil { + log.Logger.Info("IntotoObj DSSE payload is empty") + return result, nil + } + decodedPayload, err := base64.StdEncoding.DecodeString(string(v.IntotoObj.Content.Envelope.Payload)) + if err != nil { + return result, fmt.Errorf("could not decode envelope payload: %w", err) + } + statement, err := parseStatement(decodedPayload) + if err != nil { + return result, err + } + for _, s := range statement.Subject { + for alg, ds := range s.Digest { + result = append(result, alg+":"+ds) + } + } + // Not all in-toto statements will contain a SLSA provenance predicate. + // See https://github.com/in-toto/attestation/blob/main/spec/README.md#predicate + // for other predicates. + if predicate, err := parseSlsaPredicate(decodedPayload); err == nil { + if predicate.Predicate.Materials != nil { + for _, s := range predicate.Predicate.Materials { + for alg, ds := range s.Digest { + result = append(result, alg+":"+ds) + } + } + } + } + default: + log.Logger.Infof("Unknown in_toto DSSE envelope Type: %s", *v.IntotoObj.Content.Envelope.PayloadType) + } + return result, nil +} + +func parseStatement(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 parseSlsaPredicate(p []byte) (*in_toto.ProvenanceStatement, error) { + predicate := in_toto.ProvenanceStatement{} + if err := json.Unmarshal(p, &predicate); err != nil { + return nil, err + } + return &predicate, nil +} + +func (v *V002Entry) Unmarshal(pe models.ProposedEntry) error { + it, ok := pe.(*models.Intoto) + if !ok { + return errors.New("cannot unmarshal non Intoto v0.0.2 type") + } + + var err error + if err := types.DecodeEntry(it.Spec, &v.IntotoObj); err != nil { + return err + } + + // field validation + if err := v.IntotoObj.Validate(strfmt.Default); err != nil { + return err + } + + if string(v.IntotoObj.Content.Envelope.Payload) == "" { + return nil + } + + env := &dsse.Envelope{ + Payload: string(v.IntotoObj.Content.Envelope.Payload), + PayloadType: *v.IntotoObj.Content.Envelope.PayloadType, + } + + allPubKeyBytes := make([][]byte, 0) + for _, sig := range v.IntotoObj.Content.Envelope.Signatures { + env.Signatures = append(env.Signatures, dsse.Signature{ + KeyID: sig.Keyid, + Sig: string(sig.Sig), + }) + + allPubKeyBytes = append(allPubKeyBytes, sig.PublicKey) + } + + if _, err := verifyEnvelope(allPubKeyBytes, env); err != nil { + return err + } + + v.env = *env + + decodedPayload, err := base64.StdEncoding.DecodeString(string(v.IntotoObj.Content.Envelope.Payload)) + if err != nil { + return fmt.Errorf("could not decode envelope payload: %w", err) + } + + h := sha256.Sum256(decodedPayload) + v.IntotoObj.Content.PayloadHash = &models.IntotoV002SchemaContentPayloadHash{ + Algorithm: swag.String(models.IntotoV002SchemaContentPayloadHashAlgorithmSha256), + Value: swag.String(hex.EncodeToString(h[:])), + } + + return nil +} + +func (v *V002Entry) Canonicalize(ctx context.Context) ([]byte, error) { + + canonicalEntry := models.IntotoV002Schema{ + Content: &models.IntotoV002SchemaContent{ + Envelope: &models.IntotoV002SchemaContentEnvelope{ + PayloadType: v.IntotoObj.Content.Envelope.PayloadType, + Signatures: v.IntotoObj.Content.Envelope.Signatures, + }, + Hash: v.IntotoObj.Content.Hash, + PayloadHash: v.IntotoObj.Content.PayloadHash, + }, + } + itObj := models.Intoto{} + itObj.APIVersion = swag.String(APIVERSION) + itObj.Spec = &canonicalEntry + + return json.Marshal(&itObj) +} + +// AttestationKey returns the digest of the attestation that was uploaded, to be used to lookup the attestation from storage +func (v *V002Entry) AttestationKey() string { + if v.IntotoObj.Content != nil && v.IntotoObj.Content.PayloadHash != nil { + return fmt.Sprintf("%s:%s", *v.IntotoObj.Content.PayloadHash.Algorithm, *v.IntotoObj.Content.PayloadHash.Value) + } + return "" +} + +// AttestationKeyValue returns both the key and value to be persisted into attestation storage +func (v *V002Entry) AttestationKeyValue() (string, []byte) { + storageSize := base64.StdEncoding.DecodedLen(len(v.env.Payload)) + if storageSize > viper.GetInt("max_attestation_size") { + log.Logger.Infof("Skipping attestation storage, size %d is greater than max %d", storageSize, viper.GetInt("max_attestation_size")) + return "", nil + } + attBytes, err := base64.StdEncoding.DecodeString(v.env.Payload) + if err != nil { + log.Logger.Infof("could not decode envelope payload: %w", err) + return "", nil + } + return v.AttestationKey(), attBytes +} + +type verifier struct { + s signature.Signer + v signature.Verifier +} + +func (v *verifier) KeyID() (string, error) { + return "", nil +} + +func (v *verifier) Public() crypto.PublicKey { + // the dsse library uses this to generate a key ID if the KeyID function returns an empty string + // as well for the AcceptedKey return value. Unfortunately since key ids can be arbitrary, we don't + // know how to generate a matching id for the key id on the envelope's signature... + // dsse verify will skip verifiers whose key id doesn't match the signature's key id, unless it fails + // to generate one from the public key... so we trick it by returning nil ¯\_(ツ)_/¯ + return nil +} + +func (v *verifier) Sign(data []byte) (sig []byte, err error) { + if v.s == nil { + return nil, errors.New("nil signer") + } + sig, err = v.s.SignMessage(bytes.NewReader(data), options.WithCryptoSignerOpts(crypto.SHA256)) + if err != nil { + return nil, err + } + return sig, nil +} + +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 V002Entry) CreateFromArtifactProperties(_ context.Context, props types.ArtifactProperties) (models.ProposedEntry, error) { + returnVal := models.Intoto{} + re := V002Entry{ + IntotoObj: models.IntotoV002Schema{ + Content: &models.IntotoV002SchemaContent{ + Envelope: &models.IntotoV002SchemaContentEnvelope{}, + }, + }} + 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("intoto 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 len(props.PublicKeyBytes) > 0 { + allPubKeyBytes = append(allPubKeyBytes, props.PublicKeyBytes...) + } + + if len(props.PublicKeyPaths) > 0 { + for _, path := range props.PublicKeyPaths { + 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 + } + + b64 := strfmt.Base64([]byte(env.Payload)) + re.IntotoObj.Content.Envelope.Payload = b64 + re.IntotoObj.Content.Envelope.PayloadType = &env.PayloadType + + 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) + re.IntotoObj.Content.Envelope.Signatures = append(re.IntotoObj.Content.Envelope.Signatures, &models.IntotoV002SchemaContentEnvelopeSignaturesItems0{ + Keyid: sig.KeyID, + Sig: strfmt.Base64([]byte(sig.Sig)), + PublicKey: keyBytes, + }) + } + + h := sha256.Sum256([]byte(artifactBytes)) + re.IntotoObj.Content.Hash = &models.IntotoV002SchemaContentHash{ + Algorithm: swag.String(models.IntotoV001SchemaContentHashAlgorithmSha256), + Value: swag.String(hex.EncodeToString(h[:])), + } + + returnVal.Spec = re.IntotoObj + returnVal.APIVersion = swag.String(re.APIVersion()) + + 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) + allSigs := make(map[string]struct{}) + for _, sig := range env.Signatures { + allSigs[sig.Sig] = struct{}{} + } + + 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, + }) + + 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 { + delete(allSigs, accept.Sig.Sig) + verifierBySig[accept.Sig.Sig] = key + } + } + + if len(allSigs) > 0 { + return nil, errors.New("all signatures must have a key that verifies it") + } + + return verifierBySig, nil +} diff --git a/pkg/types/intoto/v0.0.2/entry_test.go b/pkg/types/intoto/v0.0.2/entry_test.go new file mode 100644 index 000000000..66bd21a23 --- /dev/null +++ b/pkg/types/intoto/v0.0.2/entry_test.go @@ -0,0 +1,470 @@ +// +// Copyright 2022 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package intoto + +import ( + "bytes" + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/hex" + "encoding/json" + "encoding/pem" + "fmt" + "math/big" + "reflect" + "sort" + "strings" + "testing" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/in-toto/in-toto-golang/in_toto" + slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2" + "github.com/secure-systems-lab/go-securesystemslib/dsse" + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/rekor/pkg/types" + "github.com/sigstore/sigstore/pkg/signature" + "go.uber.org/goleak" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} + +func TestNewEntryReturnType(t *testing.T) { + entry := NewEntry() + if reflect.TypeOf(entry) != reflect.ValueOf(&V002Entry{}).Type() { + t.Errorf("invalid type returned from NewEntry: %T", entry) + } +} + +func envelope(t *testing.T, k *ecdsa.PrivateKey, payload []byte) *dsse.Envelope { + + s, err := signature.LoadECDSASigner(k, crypto.SHA256) + if err != nil { + t.Fatal(err) + } + signer, err := in_toto.NewDSSESigner( + &verifier{ + s: s, + }) + if err != nil { + t.Fatal(err) + } + dsseEnv, err := signer.SignPayload(payload) + if err != nil { + t.Fatal(err) + } + + return dsseEnv +} + +func multiSignEnvelope(t *testing.T, k []*ecdsa.PrivateKey, payload []byte) *dsse.Envelope { + evps := []*verifier{} + for _, key := range k { + s, err := signature.LoadECDSASigner(key, crypto.SHA256) + if err != nil { + t.Fatal(err) + } + evps = append(evps, &verifier{ + s: s, + }) + } + + signer, err := dsse.NewMultiEnvelopeSigner(2, evps[0], evps[1]) + if err != nil { + t.Fatal(err) + } + dsseEnv, err := signer.SignPayload(in_toto.PayloadType, payload) + if err != nil { + t.Fatal(err) + } + + return dsseEnv +} + +func createRekorEnvelope(dsseEnv *dsse.Envelope, pub [][]byte) *models.IntotoV002SchemaContentEnvelope { + + env := &models.IntotoV002SchemaContentEnvelope{} + b64 := strfmt.Base64([]byte(dsseEnv.Payload)) + env.Payload = b64 + env.PayloadType = &dsseEnv.PayloadType + + for i, sig := range dsseEnv.Signatures { + env.Signatures = append(env.Signatures, &models.IntotoV002SchemaContentEnvelopeSignaturesItems0{ + Keyid: sig.KeyID, + Sig: strfmt.Base64([]byte(sig.Sig)), + PublicKey: strfmt.Base64(pub[i]), + }) + } + return env +} + +func envelopeHash(t *testing.T, dsseEnv *dsse.Envelope) string { + val, err := json.Marshal(dsseEnv) + if err != nil { + t.Fatal(err) + } + h := sha256.Sum256(val) + return hex.EncodeToString(h[:]) +} + +func TestV002Entry_Unmarshal(t *testing.T) { + 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) + } + pub := pem.EncodeToMemory(&pem.Block{ + Bytes: der, + Type: "PUBLIC KEY", + }) + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + + ca := &x509.Certificate{ + SerialNumber: big.NewInt(1), + } + caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &priv.PublicKey, priv) + if err != nil { + t.Fatal(err) + } + pemBytes := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: caBytes, + }) + + invalid := dsse.Envelope{ + Payload: "hello", + Signatures: []dsse.Signature{ + { + Sig: string(strfmt.Base64("foobar")), + }, + }, + } + + validPayload := "hellothispayloadisvalid" + + tests := []struct { + env *dsse.Envelope + name string + it *models.IntotoV002Schema + wantErr bool + }{ + { + name: "empty", + it: &models.IntotoV002Schema{}, + wantErr: true, + }, + { + name: "missing envelope", + it: &models.IntotoV002Schema{ + Content: &models.IntotoV002SchemaContent{ + Hash: &models.IntotoV002SchemaContentHash{ + Algorithm: swag.String(models.IntotoV002SchemaContentHashAlgorithmSha256), + }, + }, + }, + wantErr: true, + }, + { + env: envelope(t, key, []byte(validPayload)), + name: "valid", + it: &models.IntotoV002Schema{ + Content: &models.IntotoV002SchemaContent{ + Envelope: createRekorEnvelope(envelope(t, key, []byte(validPayload)), [][]byte{pub}), + Hash: &models.IntotoV002SchemaContentHash{ + Algorithm: swag.String(models.IntotoV002SchemaContentHashAlgorithmSha256), + Value: swag.String(envelopeHash(t, envelope(t, key, []byte(validPayload)))), + }, + }, + }, + wantErr: false, + }, + { + env: envelope(t, priv, []byte(validPayload)), + name: "cert", + it: &models.IntotoV002Schema{ + Content: &models.IntotoV002SchemaContent{ + Envelope: createRekorEnvelope(envelope(t, priv, []byte(validPayload)), [][]byte{pemBytes}), + Hash: &models.IntotoV002SchemaContentHash{ + Algorithm: swag.String(models.IntotoV002SchemaContentHashAlgorithmSha256), + Value: swag.String(envelopeHash(t, envelope(t, priv, []byte(validPayload)))), + }, + }, + }, + wantErr: false, + }, + { + env: &invalid, + name: "invalid", + it: &models.IntotoV002Schema{ + Content: &models.IntotoV002SchemaContent{ + Envelope: createRekorEnvelope(&invalid, [][]byte{pub}), + Hash: &models.IntotoV002SchemaContentHash{ + Algorithm: swag.String(models.IntotoV002SchemaContentHashAlgorithmSha256), + Value: swag.String(envelopeHash(t, &invalid)), + }, + }, + }, + wantErr: true, + }, + { + env: envelope(t, key, []byte(validPayload)), + name: "invalid key", + it: &models.IntotoV002Schema{ + Content: &models.IntotoV002SchemaContent{ + Envelope: createRekorEnvelope(envelope(t, key, []byte(validPayload)), [][]byte{[]byte("notavalidkey")}), + Hash: &models.IntotoV002SchemaContentHash{ + Algorithm: swag.String(models.IntotoV002SchemaContentHashAlgorithmSha256), + Value: swag.String(envelopeHash(t, envelope(t, key, []byte(validPayload)))), + }, + }, + }, + wantErr: true, + }, + { + env: multiSignEnvelope(t, []*ecdsa.PrivateKey{key, priv}, []byte(validPayload)), + name: "multi-key", + it: &models.IntotoV002Schema{ + Content: &models.IntotoV002SchemaContent{ + Envelope: createRekorEnvelope(multiSignEnvelope(t, []*ecdsa.PrivateKey{key, priv}, []byte(validPayload)), [][]byte{pub, pemBytes}), + Hash: &models.IntotoV002SchemaContentHash{ + Algorithm: swag.String(models.IntotoV002SchemaContentHashAlgorithmSha256), + Value: swag.String(envelopeHash(t, multiSignEnvelope(t, []*ecdsa.PrivateKey{key, priv}, []byte(validPayload)))), + }, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := &V002Entry{} + + it := &models.Intoto{ + Spec: tt.it, + } + + var uv = func() error { + if err := v.Unmarshal(it); err != nil { + return err + } + want := []string{} + for _, sig := range v.IntotoObj.Content.Envelope.Signatures { + keyHash := sha256.Sum256(sig.PublicKey) + want = append(want, "sha256:"+hex.EncodeToString(keyHash[:])) + } + decodedPayload, err := base64.StdEncoding.DecodeString(tt.env.Payload) + if err != nil { + return fmt.Errorf("could not decode envelope payload: %w", err) + } + h := sha256.Sum256(decodedPayload) + want = append(want, "sha256:"+hex.EncodeToString(h[:])) + + if !reflect.DeepEqual(v.AttestationKey(), "sha256:"+hex.EncodeToString(h[:])) { + t.Errorf("V002Entry.AttestationKey() = %v, want %v", v.AttestationKey(), "sha256:"+hex.EncodeToString(h[:])) + } + + hashkey := strings.ToLower(fmt.Sprintf("%s:%s", *tt.it.Content.Hash.Algorithm, *tt.it.Content.Hash.Value)) + want = append(want, hashkey) + got, _ := v.IndexKeys() + sort.Strings(got) + sort.Strings(want) + if !reflect.DeepEqual(got, want) { + t.Errorf("V002Entry.IndexKeys() = %v, want %v", got, want) + } + payloadBytes, _ := v.env.DecodeB64Payload() + payloadSha := sha256.Sum256(payloadBytes) + payloadHash := hex.EncodeToString(payloadSha[:]) + + canonicalBytes, err := v.Canonicalize(context.Background()) + if err != nil { + t.Errorf("error canonicalizing entry: %v", err) + } + + pe, err := models.UnmarshalProposedEntry(bytes.NewReader(canonicalBytes), runtime.JSONConsumer()) + if err != nil { + t.Errorf("unexpected err from Unmarshalling canonicalized entry for '%v': %v", tt.name, err) + } + canonicalEntry, err := types.UnmarshalEntry(pe) + if err != nil { + t.Errorf("unexpected err from type-specific unmarshalling for '%v': %v", tt.name, err) + } + canonicalV002 := canonicalEntry.(*V002Entry) + fmt.Printf("%v", canonicalV002.IntotoObj.Content) + if *canonicalV002.IntotoObj.Content.Hash.Value != *tt.it.Content.Hash.Value { + t.Errorf("envelope hashes do not match post canonicalization: %v %v", *canonicalV002.IntotoObj.Content.Hash.Value, *tt.it.Content.Hash.Value) + } + if canonicalV002.AttestationKey() != "" && *canonicalV002.IntotoObj.Content.PayloadHash.Value != payloadHash { + t.Errorf("payload hashes do not match post canonicalization: %v %v", canonicalV002.IntotoObj.Content.PayloadHash.Value, payloadHash) + } + canonicalIndexKeys, _ := canonicalV002.IndexKeys() + if !cmp.Equal(got, canonicalIndexKeys, cmpopts.SortSlices(func(x, y string) bool { return x < y })) { + t.Errorf("index keys from hydrated object do not match those generated from canonicalized (and re-hydrated) object: %v %v", got, canonicalIndexKeys) + } + + return nil + } + if err := uv(); (err != nil) != tt.wantErr { + t.Errorf("V002Entry.Unmarshal() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestV002Entry_IndexKeys(t *testing.T) { + 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) + } + pub := pem.EncodeToMemory(&pem.Block{ + Bytes: der, + Type: "PUBLIC KEY", + }) + + tests := []struct { + name string + statement in_toto.Statement + want []string + }{ + { + name: "standard", + want: []string{}, + statement: in_toto.Statement{ + Predicate: "hello", + }, + }, + { + name: "subject", + want: []string{"sha256:foo"}, + statement: in_toto.Statement{ + StatementHeader: in_toto.StatementHeader{ + Subject: []in_toto.Subject{ + { + Name: "foo", + Digest: map[string]string{ + "sha256": "foo", + }, + }, + }, + }, + Predicate: "hello", + }, + }, + { + name: "slsa", + want: []string{"sha256:bar"}, + statement: in_toto.Statement{ + Predicate: slsa.ProvenancePredicate{ + Materials: []slsa.ProvenanceMaterial{ + { + URI: "foo", + Digest: map[string]string{ + "sha256": "bar", + }}, + }, + }, + }, + }, + { + name: "slsa wit header", + want: []string{"sha256:foo", "sha256:bar"}, + statement: in_toto.Statement{ + StatementHeader: in_toto.StatementHeader{ + Subject: []in_toto.Subject{ + { + Name: "foo", + Digest: map[string]string{ + "sha256": "foo", + }, + }, + }, + }, + Predicate: slsa.ProvenancePredicate{ + Materials: []slsa.ProvenanceMaterial{ + { + URI: "foo", + Digest: map[string]string{ + "sha256": "bar", + }}, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b, err := json.Marshal(tt.statement) + if err != nil { + t.Fatal(err) + } + payloadHash := sha256.Sum256(b) + v := V002Entry{ + IntotoObj: models.IntotoV002Schema{ + Content: &models.IntotoV002SchemaContent{ + Envelope: createRekorEnvelope(envelope(t, key, b), [][]byte{pub}), + Hash: &models.IntotoV002SchemaContentHash{ + Algorithm: swag.String(models.IntotoV001SchemaContentHashAlgorithmSha256), + Value: swag.String(envelopeHash(t, envelope(t, key, b))), + }, + PayloadHash: &models.IntotoV002SchemaContentPayloadHash{ + Algorithm: swag.String(models.IntotoV001SchemaContentHashAlgorithmSha256), + Value: swag.String(hex.EncodeToString(payloadHash[:])), + }, + }, + }, + env: *envelope(t, key, b), + } + want := []string{} + for _, sig := range v.IntotoObj.Content.Envelope.Signatures { + keyHash := sha256.Sum256(sig.PublicKey) + want = append(want, "sha256:"+hex.EncodeToString(keyHash[:])) + } + + want = append(want, "sha256:"+hex.EncodeToString(payloadHash[:])) + + hashkey := strings.ToLower("sha256:" + *v.IntotoObj.Content.Hash.Value) + want = append(want, hashkey) + want = append(want, tt.want...) + got, _ := v.IndexKeys() + sort.Strings(got) + sort.Strings(want) + if !cmp.Equal(got, want) { + t.Errorf("V001Entry.IndexKeys() = %v, want %v", got, want) + } + }) + } +} diff --git a/pkg/types/intoto/v0.0.2/intoto_v0_0_2_schema.json b/pkg/types/intoto/v0.0.2/intoto_v0_0_2_schema.json new file mode 100644 index 000000000..0008e46fb --- /dev/null +++ b/pkg/types/intoto/v0.0.2/intoto_v0_0_2_schema.json @@ -0,0 +1,102 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://rekor.sigstore.dev/types/intoto/intoto_v0_0_2_schema.json", + "title": "intoto v0.0.2 Schema", + "description": "Schema for intoto object", + "type": "object", + "properties": { + "content": { + "type": "object", + "properties": { + "envelope": { + "description": "dsse envelope", + "type": "object", + "properties": { + "payload": { + "description": "payload of the envelope", + "type": "string", + "format": "byte", + "writeOnly": true + }, + "payloadType": { + "description": "type describing the 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", "signatures"] + }, + "hash": { + "description": "Specifies the hash algorithm and value encompassing the entire signed envelope", + "type": "object", + "properties": { + "algorithm": { + "description": "The hashing function used to compute the hash value", + "type": "string", + "enum": [ + "sha256" + ] + }, + "value": { + "description": "The hash value for the archive", + "type": "string" + } + }, + "required": [ + "algorithm", + "value" + ], + "readOnly": true + }, + "payloadHash": { + "description": "Specifies the hash algorithm and value covering the payload within the DSSE envelope", + "type": "object", + "properties": { + "algorithm": { + "description": "The hashing function used to compute the hash value", + "type": "string", + "enum": [ "sha256" ] + }, + "value": { + "description": "The hash value of the payload", + "type": "string" + } + }, + "required": [ + "algorithm", + "value" + ], + "readOnly": true + } + } + } + }, + "required": [ + "content" + ] +} diff --git a/pkg/types/rekord/v0.0.1/entry.go b/pkg/types/rekord/v0.0.1/entry.go index 4c566a054..95feb3ba4 100644 --- a/pkg/types/rekord/v0.0.1/entry.go +++ b/pkg/types/rekord/v0.0.1/entry.go @@ -391,19 +391,21 @@ func (v V001Entry) CreateFromArtifactProperties(ctx context.Context, props types re.RekordObj.Signature.PublicKey = &models.RekordV001SchemaSignaturePublicKey{} publicKeyBytes := props.PublicKeyBytes - if publicKeyBytes == nil { - if props.PublicKeyPath == nil { - return nil, errors.New("public key must be provided to verify detached signature") + if len(publicKeyBytes) == 0 { + if len(props.PublicKeyPaths) != 1 { + return nil, errors.New("only one public key must be provided to verify detached signature") } - publicKeyBytes, err = ioutil.ReadFile(filepath.Clean(props.PublicKeyPath.Path)) + keyBytes, err := ioutil.ReadFile(filepath.Clean(props.PublicKeyPaths[0].Path)) if err != nil { return nil, fmt.Errorf("error reading public key file: %w", err) } - re.RekordObj.Signature.PublicKey.Content = (*strfmt.Base64)(&publicKeyBytes) - } else { - re.RekordObj.Signature.PublicKey.Content = (*strfmt.Base64)(&publicKeyBytes) + publicKeyBytes = append(publicKeyBytes, keyBytes) + } else if len(publicKeyBytes) != 1 { + return nil, errors.New("only one public key must be provided") } + re.RekordObj.Signature.PublicKey.Content = (*strfmt.Base64)(&publicKeyBytes[0]) + if err := re.validate(); err != nil { return nil, err } diff --git a/pkg/types/rpm/v0.0.1/entry.go b/pkg/types/rpm/v0.0.1/entry.go index b8b00a8f5..1523a4297 100644 --- a/pkg/types/rpm/v0.0.1/entry.go +++ b/pkg/types/rpm/v0.0.1/entry.go @@ -342,19 +342,21 @@ func (v V001Entry) CreateFromArtifactProperties(ctx context.Context, props types re.RPMModel.PublicKey = &models.RpmV001SchemaPublicKey{} publicKeyBytes := props.PublicKeyBytes - if publicKeyBytes == nil { - if props.PublicKeyPath == nil { - return nil, errors.New("public key must be provided to verify RPM signature") + if len(publicKeyBytes) == 0 { + if len(props.PublicKeyPaths) != 1 { + return nil, errors.New("only one public key must be provided to verify RPM signature") } - publicKeyBytes, err = ioutil.ReadFile(filepath.Clean(props.PublicKeyPath.Path)) + keyBytes, err := ioutil.ReadFile(filepath.Clean(props.PublicKeyPaths[0].Path)) if err != nil { return nil, fmt.Errorf("error reading public key file: %w", err) } - re.RPMModel.PublicKey.Content = (*strfmt.Base64)(&publicKeyBytes) - } else { - re.RPMModel.PublicKey.Content = (*strfmt.Base64)(&publicKeyBytes) + publicKeyBytes = append(publicKeyBytes, keyBytes) + } else if len(publicKeyBytes) != 1 { + return nil, errors.New("only one public key must be provided") } + re.RPMModel.PublicKey.Content = (*strfmt.Base64)(&publicKeyBytes[0]) + if err := re.validate(); err != nil { return nil, err } diff --git a/pkg/types/tuf/v0.0.1/entry.go b/pkg/types/tuf/v0.0.1/entry.go index e53e069a9..8c3a9ff15 100644 --- a/pkg/types/tuf/v0.0.1/entry.go +++ b/pkg/types/tuf/v0.0.1/entry.go @@ -333,26 +333,25 @@ func (v V001Entry) CreateFromArtifactProperties(ctx context.Context, props types rootBytes := props.PublicKeyBytes re.TufObj.Root = &models.TUFV001SchemaRoot{} - if rootBytes == nil { - if props.PublicKeyPath == nil { - return nil, errors.New("path to root file must be specified") + if len(rootBytes) == 0 { + if len(props.PublicKeyPaths) != 1 { + return nil, errors.New("only one path to root file must be specified") } - rootBytes, err = ioutil.ReadFile(filepath.Clean(props.PublicKeyPath.Path)) + keyBytes, err := ioutil.ReadFile(filepath.Clean(props.PublicKeyPaths[0].Path)) if err != nil { return nil, fmt.Errorf("error reading root file: %w", err) } - s := &data.Signed{} - if err := json.Unmarshal(rootBytes, s); err != nil { - return nil, err - } - re.TufObj.Root.Content = s - } else { - s := &data.Signed{} - if err := json.Unmarshal(rootBytes, s); err != nil { - return nil, err - } - re.TufObj.Root.Content = s + rootBytes = append(rootBytes, keyBytes) + + } else if len(rootBytes) != 1 { + return nil, errors.New("only one root key must be provided") + } + + root := &data.Signed{} + if err := json.Unmarshal(rootBytes[0], root); err != nil { + return nil, err } + re.TufObj.Root.Content = root if err := re.Validate(); err != nil { return nil, err diff --git a/tests/e2e_test.go b/tests/e2e_test.go index e28c4bd1d..545a90c44 100644 --- a/tests/e2e_test.go +++ b/tests/e2e_test.go @@ -23,6 +23,7 @@ import ( "context" "crypto" "crypto/ecdsa" + "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/base64" @@ -32,16 +33,19 @@ import ( "fmt" "io/ioutil" "net/http" + "net/url" "os" "os/exec" "path/filepath" "reflect" + "runtime" "strconv" "strings" "testing" "time" "golang.org/x/sync/errgroup" + "sigs.k8s.io/release-utils/version" "github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer" "github.com/go-openapi/strfmt" @@ -56,6 +60,7 @@ import ( "github.com/sigstore/rekor/pkg/sharding" "github.com/sigstore/rekor/pkg/signer" "github.com/sigstore/rekor/pkg/types" + _ "github.com/sigstore/rekor/pkg/types/intoto/v0.0.1" rekord "github.com/sigstore/rekor/pkg/types/rekord/v0.0.1" "github.com/sigstore/rekor/pkg/util" "github.com/sigstore/sigstore/pkg/cryptoutils" @@ -496,14 +501,20 @@ func TestIntoto(t *testing.T) { if err != nil { t.Fatal(err) } - signer, err := dsse.NewEnvelopeSigner(&IntotoSigner{ - priv: priv.(*ecdsa.PrivateKey), + + s, err := signature.LoadECDSASigner(priv.(*ecdsa.PrivateKey), crypto.SHA256) + if err != nil { + t.Fatal(err) + } + + signer, err := dsse.NewEnvelopeSigner(&verifier{ + s: s, }) if err != nil { t.Fatal(err) } - env, err := signer.SignPayload("application/vnd.in-toto+json", b) + env, err := signer.SignPayload(in_toto.PayloadType, b) if err != nil { t.Fatal(err) } @@ -516,6 +527,10 @@ func TestIntoto(t *testing.T) { write(t, string(eb), attestationPath) write(t, ecdsaPub, pubKeyPath) + // ensure that we can't upload a intoto v0.0.1 entry + v001out := runCliErr(t, "upload", "--artifact", attestationPath, "--type", "intoto:0.0.1", "--public-key", pubKeyPath) + outputContains(t, v001out, "type intoto does not support version 0.0.1") + // If we do it twice, it should already exist out := runCli(t, "upload", "--artifact", attestationPath, "--type", "intoto", "--public-key", pubKeyPath) outputContains(t, out, "Created entry at") @@ -538,7 +553,7 @@ func TestIntoto(t *testing.T) { attHash := sha256.Sum256(b) - intotoModel := &models.IntotoV001Schema{} + intotoModel := &models.IntotoV002Schema{} if err := types.DecodeEntry(g.Body.(map[string]interface{})["IntotoObj"], intotoModel); err != nil { t.Errorf("could not convert body into intoto type: %v", err) } @@ -560,6 +575,237 @@ func TestIntoto(t *testing.T) { } +func TestIntotoMultiSig(t *testing.T) { + td := t.TempDir() + attestationPath := filepath.Join(td, "attestation.json") + ecdsapubKeyPath := filepath.Join(td, "ecdsapub.pem") + rsapubKeyPath := filepath.Join(td, "rsapub.pem") + + // Get some random data so it's unique each run + d := randomData(t, 10) + id := base64.StdEncoding.EncodeToString(d) + + it := in_toto.ProvenanceStatement{ + StatementHeader: in_toto.StatementHeader{ + Type: in_toto.StatementInTotoV01, + PredicateType: slsa.PredicateSLSAProvenance, + Subject: []in_toto.Subject{ + { + Name: "foobar", + Digest: slsa.DigestSet{ + "foo": "bar", + }, + }, + }, + }, + Predicate: slsa.ProvenancePredicate{ + Builder: slsa.ProvenanceBuilder{ + ID: "foo" + id, + }, + }, + } + + b, err := json.Marshal(it) + if err != nil { + t.Fatal(err) + } + + evps := []*verifier{} + + pb, _ := pem.Decode([]byte(ecdsaPriv)) + priv, err := x509.ParsePKCS8PrivateKey(pb.Bytes) + if err != nil { + t.Fatal(err) + } + + signECDSA, err := signature.LoadECDSASigner(priv.(*ecdsa.PrivateKey), crypto.SHA256) + if err != nil { + t.Fatal(err) + } + + evps = append(evps, &verifier{ + s: signECDSA, + }) + + pbRSA, _ := pem.Decode([]byte(rsaKey)) + rsaPriv, err := x509.ParsePKCS8PrivateKey(pbRSA.Bytes) + if err != nil { + t.Fatal(err) + } + + signRSA, err := signature.LoadRSAPKCS1v15Signer(rsaPriv.(*rsa.PrivateKey), crypto.SHA256) + if err != nil { + t.Fatal(err) + } + + evps = append(evps, &verifier{ + s: signRSA, + }) + + signer, err := dsse.NewMultiEnvelopeSigner(2, evps[0], evps[1]) + if err != nil { + t.Fatal(err) + } + + env, err := signer.SignPayload(in_toto.PayloadType, b) + if err != nil { + t.Fatal(err) + } + + eb, err := json.Marshal(env) + if err != nil { + t.Fatal(err) + } + + write(t, string(eb), attestationPath) + write(t, ecdsaPub, ecdsapubKeyPath) + write(t, pubKey, rsapubKeyPath) + + // ensure that we can't upload a intoto v0.0.1 entry + v001out := runCliErr(t, "upload", "--artifact", attestationPath, "--type", "intoto:0.0.1", "--public-key", ecdsapubKeyPath, "--public-key", rsapubKeyPath) + outputContains(t, v001out, "type intoto does not support version 0.0.1") + + // If we do it twice, it should already exist + out := runCli(t, "upload", "--artifact", attestationPath, "--type", "intoto", "--public-key", ecdsapubKeyPath, "--public-key", rsapubKeyPath) + outputContains(t, out, "Created entry at") + uuid := getUUIDFromUploadOutput(t, out) + + out = runCli(t, "get", "--uuid", uuid, "--format=json") + g := getOut{} + if err := json.Unmarshal([]byte(out), &g); err != nil { + t.Fatal(err) + } + // The attestation should be stored at /var/run/attestations/$uuid + + got := in_toto.ProvenanceStatement{} + if err := json.Unmarshal([]byte(g.Attestation), &got); err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(it, got); diff != "" { + t.Errorf("diff: %s", diff) + } + + attHash := sha256.Sum256([]byte(g.Attestation)) + + intotoV002Model := &models.IntotoV002Schema{} + if err := types.DecodeEntry(g.Body.(map[string]interface{})["IntotoObj"], intotoV002Model); err != nil { + t.Errorf("could not convert body into intoto type: %v", err) + } + if intotoV002Model.Content.Hash == nil { + t.Errorf("could not find hash over attestation %v", intotoV002Model) + } + recordedPayloadHash, err := hex.DecodeString(*intotoV002Model.Content.PayloadHash.Value) + if err != nil { + t.Errorf("error converting attestation hash to []byte: %v", err) + } + + if !bytes.Equal(attHash[:], recordedPayloadHash) { + t.Fatal(fmt.Errorf("attestation hash %v doesnt match the payload we sent %v", hex.EncodeToString(attHash[:]), + *intotoV002Model.Content.PayloadHash.Value)) + } + + out = runCli(t, "upload", "--artifact", attestationPath, "--type", "intoto", "--public-key", ecdsapubKeyPath, "--public-key", rsapubKeyPath) + outputContains(t, out, "Entry already exists") + +} + +func TestIntotoBlockV001(t *testing.T) { + td := t.TempDir() + attestationPath := filepath.Join(td, "attestation.json") + pubKeyPath := filepath.Join(td, "pub.pem") + + // Get some random data so it's unique each run + d := randomData(t, 10) + id := base64.StdEncoding.EncodeToString(d) + + it := in_toto.ProvenanceStatement{ + StatementHeader: in_toto.StatementHeader{ + Type: in_toto.StatementInTotoV01, + PredicateType: slsa.PredicateSLSAProvenance, + Subject: []in_toto.Subject{ + { + Name: "foobar", + Digest: slsa.DigestSet{ + "foo": "bar", + }, + }, + }, + }, + Predicate: slsa.ProvenancePredicate{ + Builder: slsa.ProvenanceBuilder{ + ID: "foo" + id, + }, + }, + } + + b, err := json.Marshal(it) + if err != nil { + t.Fatal(err) + } + + pb, _ := pem.Decode([]byte(ecdsaPriv)) + priv, err := x509.ParsePKCS8PrivateKey(pb.Bytes) + if err != nil { + t.Fatal(err) + } + + s, err := signature.LoadECDSASigner(priv.(*ecdsa.PrivateKey), crypto.SHA256) + if err != nil { + t.Fatal(err) + } + + signer, err := dsse.NewEnvelopeSigner(&verifier{ + s: s, + }) + if err != nil { + t.Fatal(err) + } + + env, err := signer.SignPayload(in_toto.PayloadType, b) + if err != nil { + t.Fatal(err) + } + + eb, err := json.Marshal(env) + if err != nil { + t.Fatal(err) + } + + uaString := fmt.Sprintf("rekor-cli/%s (%s; %s)", version.GetVersionInfo().GitVersion, runtime.GOOS, runtime.GOARCH) + + write(t, string(eb), attestationPath) + write(t, ecdsaPub, pubKeyPath) + + rekorClient, err := client.GetRekorClient("http://localhost:3000", client.WithUserAgent(uaString)) + if err != nil { + t.Fatal(err) + } + var entry models.ProposedEntry + params := entries.NewCreateLogEntryParams() + params.SetTimeout(time.Duration(30) * time.Second) + + props := &types.ArtifactProperties{} + + props.ArtifactPath = &url.URL{Path: attestationPath} + + collectedKeys := []*url.URL{{Path: pubKeyPath}} + props.PublicKeyPaths = collectedKeys + + entry, err = types.NewProposedEntry(context.Background(), "intoto", "0.0.1", *props) + if err != nil { + t.Fatal(err) + } + params.SetProposedEntry(entry) + + _, err = rekorClient.Entries.CreateLogEntry(params) + if err == nil { + t.Fatal("insertion of v0.0.1 entry should fail") + } + if !strings.Contains(err.Error(), "entry kind 'intoto' does not support inserting entries of version '0.0.1'") { + t.Errorf("Expected error as intoto v0.0.1 should not be allowed to be entered into rekor") + } +} + func TestTimestampArtifact(t *testing.T) { var out string out = runCli(t, "upload", "--type", "rfc3161", "--artifact", "test.tsr") diff --git a/tests/harness_test.go b/tests/harness_test.go index 3286cba0a..88441c41f 100644 --- a/tests/harness_test.go +++ b/tests/harness_test.go @@ -19,6 +19,7 @@ package e2e import ( "bytes" + "crypto" "crypto/ecdsa" "crypto/sha256" "crypto/x509" @@ -42,6 +43,7 @@ import ( "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/rekor/pkg/sharding" "github.com/sigstore/rekor/pkg/types" + "github.com/sigstore/sigstore/pkg/signature" ) type StoredEntry struct { @@ -121,8 +123,14 @@ func TestHarnessAddIntoto(t *testing.T) { if err != nil { t.Fatal(err) } - signer, err := dsse.NewEnvelopeSigner(&IntotoSigner{ - priv: priv.(*ecdsa.PrivateKey), + + s, err := signature.LoadECDSASigner(priv.(*ecdsa.PrivateKey), crypto.SHA256) + if err != nil { + t.Fatal(err) + } + + signer, err := dsse.NewEnvelopeSigner(&verifier{ + s: s, }) if err != nil { t.Fatal(err) @@ -164,7 +172,7 @@ func TestHarnessAddIntoto(t *testing.T) { attHash := sha256.Sum256(b) - intotoModel := &models.IntotoV001Schema{} + intotoModel := &models.IntotoV002Schema{} if err := types.DecodeEntry(g.Body.(map[string]interface{})["IntotoObj"], intotoModel); err != nil { t.Errorf("could not convert body into intoto type: %v", err) } diff --git a/tests/intoto_multi_dsse.json b/tests/intoto_multi_dsse.json new file mode 100644 index 000000000..7e245117a --- /dev/null +++ b/tests/intoto_multi_dsse.json @@ -0,0 +1,6 @@ +{"payloadType":"application/vnd.in-toto+json", +"payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjAuMiIsInN1YmplY3QiOlt7Im5hbWUiOiJmb29iYXIiLCJkaWdlc3QiOnsiZm9vIjoiYmFyIn19XSwicHJlZGljYXRlIjp7ImJ1aWxkZXIiOnsiaWQiOiJmb29BNi9QWW1CNmNCQXRZQT09In0sImJ1aWxkVHlwZSI6IiIsImludm9jYXRpb24iOnsiY29uZmlnU291cmNlIjp7fX19fQ==", +"signatures":[{ + "keyid":"","sig":"MEUCIQC4/1MwOahpGT/oh6DZFuwZiG9yXpfo22wh+7mGn9lQ4gIgIL2LFt0lxYbFngf4ze/FNvkybYzwtmkX2XrcnGMpOqo="}, + {"keyid":"","sig":"PhNAdsvKEVqv+cF/QvjEOz+fTtoNWQfa9gCWnOpm5yWVFRiu6he5jvw6A8ESRXxV3KnEcyBFCfCbITNK2fpXQEOc0gNDcsil1m/Pzv/JonhtH/TwIjdJ8zy4WEUVHZMfj5IIeibp3U2ACvmCc9HEUPCc6VM0hq7ri/VnKLcCCGbKmxwHrVXArv1hKBrcP7s52tuxCsVr5+3Z7eKPx5WkycOhpUhSVhMHjOCj9e3mveiw4dYuwdrgQHtqZJhUg0WFlVCQTQdcLxIII1g7BudA38yzfOgkbwgfoZ2Dh3iF7uRHIUdFxxpqF2oQeU2uDv3dT9cCfRd/kDtU6hbuVgixgw=="}] +} diff --git a/tests/intoto_multi_pub2.pem b/tests/intoto_multi_pub2.pem new file mode 100644 index 000000000..313580684 --- /dev/null +++ b/tests/intoto_multi_pub2.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3wqI/TysUiKTgY1bz+wd +JfEOil4MEsRASKGzJddZ6x9hb+rn2UVoJmuxN62XI0TMoMn4mukgfCgY6jgTB58V ++/LaeSA8Wz1p4gOxhk1mcgbF4HyxR+xlRgYfH4iSbXy+Ez/8ZjM2OO68fKr4JZEA +5LXZkhJr32JqH+UiFw/wgSPWA8aV0AfRAXHdekJ48B1ChxJTrOJWSPTnj/E0lfLV +srJKtXDuC8T0vFmVU726tI6fODsEE6VrSahvw1ENUHzI34sbfrmrggwPO4iMAQvq +wu2gn2lx6ajWsh806FItiXN+DuizMnx4KMBI0IJynoQpWOFbstGiV0LygZkQ6soz +vwIDAQAB +-----END PUBLIC KEY----- diff --git a/tests/x509.go b/tests/x509.go index 0ffefbaae..81914b9fe 100644 --- a/tests/x509.go +++ b/tests/x509.go @@ -19,8 +19,8 @@ package e2e import ( + "bytes" "crypto" - "crypto/ecdsa" "crypto/rand" "crypto/rsa" "crypto/sha256" @@ -30,7 +30,8 @@ import ( "io/ioutil" "testing" - "github.com/secure-systems-lab/go-securesystemslib/dsse" + "github.com/sigstore/sigstore/pkg/signature" + "github.com/sigstore/sigstore/pkg/signature/options" ) // Generated with: @@ -156,34 +157,33 @@ func createdX509SignedArtifact(t *testing.T, artifactPath, sigPath string) { } } -type IntotoSigner struct { - priv *ecdsa.PrivateKey +type verifier struct { + s signature.Signer + v signature.Verifier } -var _ dsse.SignVerifier = &IntotoSigner{} +func (v *verifier) KeyID() (string, error) { + return "", nil +} -func (it *IntotoSigner) Sign(data []byte) ([]byte, error) { - h := sha256.Sum256(data) - sig, err := it.priv.Sign(rand.Reader, h[:], crypto.SHA256) +func (v *verifier) Public() crypto.PublicKey { + return v.v.PublicKey +} + +func (v *verifier) Sign(data []byte) (sig []byte, err error) { + if v.s == nil { + return nil, errors.New("nil signer") + } + sig, err = v.s.SignMessage(bytes.NewReader(data), options.WithCryptoSignerOpts(crypto.SHA256)) if err != nil { return nil, err } return sig, nil } -func (it *IntotoSigner) KeyID() (string, error) { - return "", nil -} - -func (it *IntotoSigner) Public() crypto.PublicKey { - return it.priv.Public() -} - -func (it *IntotoSigner) Verify(data, sig []byte) error { - h := sha256.Sum256(data) - ok := ecdsa.VerifyASN1(&it.priv.PublicKey, h[:], sig) - if ok { - return nil +func (v *verifier) Verify(data, sig []byte) error { + if v.v == nil { + return errors.New("nil verifier") } - return errors.New("invalid signature") + return v.v.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data)) }