From 19f56374f5754d78e21b2c5873ea7eff4ab8380e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Aug 2022 09:39:53 -0500 Subject: [PATCH 1/2] Bump goreleaser/goreleaser-action from 3.0.0 to 3.1.0 (#1008) * Bump goreleaser/goreleaser-action from 3.0.0 to 3.1.0 Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 3.0.0 to 3.1.0. - [Release notes](https://github.com/goreleaser/goreleaser-action/releases) - [Commits](https://github.com/goreleaser/goreleaser-action/compare/68acf3b1adf004ac9c2f0a4259e85c5f66e99bef...ff11ca24a9b39f2d36796d1fbd7a4e39c182630a) --- updated-dependencies: - dependency-name: goreleaser/goreleaser-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * update version comment Signed-off-by: cpanato Signed-off-by: dependabot[bot] Signed-off-by: cpanato Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: cpanato --- .github/workflows/validate-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 568e31a9cbee9831f0e3a99ce5adf6f2356f7709 Mon Sep 17 00:00:00 2001 From: Parth Patel <88045217+pxp928@users.noreply.github.com> Date: Mon, 29 Aug 2022 14:56:17 -0400 Subject: [PATCH 2/2] Intoto v0.0.2 (#973) * add DSSE rekor type to support any DSSE envelope Adds a DSSE envelope type to rekor. If the DSSE envelope's payload is an in-toto statement the in-toto subjects will be used as indices for the envelope's rekord. If the envelope's payload is within the server's configured attestation size the payload will be stored as an attestation. Signed-off-by: Mikhail Swift Signed-off-by: pxp928 * added intoto v0.0.2 Signed-off-by: pxp928 * implemented fixes and added new cli flag Signed-off-by: pxp928 * changed public-key to slice Signed-off-by: pxp928 * changes to if statements Signed-off-by: pxp928 * changed to PublicKeyPaths Signed-off-by: pxp928 * added updated based on linked PRs Signed-off-by: pxp928 * added test to check for v001 blocking Signed-off-by: pxp928 Signed-off-by: Mikhail Swift Signed-off-by: pxp928 Co-authored-by: Mikhail Swift --- Makefile.swagger | 2 +- cmd/rekor-cli/app/pflag_groups.go | 18 +- cmd/rekor-cli/app/pflags.go | 65 +- cmd/rekor-cli/app/pflags_test.go | 27 + cmd/rekor-cli/app/root.go | 1 + cmd/rekor-cli/app/search.go | 21 +- cmd/rekor-server/app/serve.go | 23 +- pkg/generated/models/intoto_v002_schema.go | 724 ++++++++++++++++++ pkg/generated/restapi/embedded_spec.go | 262 +++++++ pkg/types/alpine/v0.0.1/entry.go | 15 +- pkg/types/cose/v0.0.1/entry.go | 17 +- pkg/types/entries.go | 4 +- pkg/types/hashedrekord/v0.0.1/entry.go | 13 +- pkg/types/helm/v0.0.1/entry.go | 14 +- pkg/types/intoto/intoto.go | 15 +- pkg/types/intoto/intoto_schema.json | 3 + pkg/types/intoto/v0.0.1/entry.go | 14 +- pkg/types/intoto/v0.0.2/entry.go | 423 ++++++++++ pkg/types/intoto/v0.0.2/entry_test.go | 470 ++++++++++++ .../intoto/v0.0.2/intoto_v0_0_2_schema.json | 102 +++ pkg/types/rekord/v0.0.1/entry.go | 16 +- pkg/types/rpm/v0.0.1/entry.go | 16 +- pkg/types/tuf/v0.0.1/entry.go | 29 +- tests/e2e_test.go | 257 ++++++- tests/harness_test.go | 14 +- tests/intoto_multi_dsse.json | 6 + tests/intoto_multi_pub2.pem | 9 + tests/x509.go | 44 +- 28 files changed, 2494 insertions(+), 130 deletions(-) create mode 100644 pkg/generated/models/intoto_v002_schema.go create mode 100644 pkg/types/intoto/v0.0.2/entry.go create mode 100644 pkg/types/intoto/v0.0.2/entry_test.go create mode 100644 pkg/types/intoto/v0.0.2/intoto_v0_0_2_schema.json create mode 100644 tests/intoto_multi_dsse.json create mode 100644 tests/intoto_multi_pub2.pem 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 ef89e89cb..6744d7cee 100644 --- a/pkg/generated/restapi/embedded_spec.go +++ b/pkg/generated/restapi/embedded_spec.go @@ -1955,6 +1955,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", @@ -3066,6 +3236,9 @@ func init() { "oneOf": [ { "$ref": "#/definitions/intotoV001Schema" + }, + { + "$ref": "#/definitions/intotoV002Schema" } ], "$schema": "http://json-schema.org/draft-07/schema", @@ -3143,6 +3316,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 327c04762..4e99d272a 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" @@ -30,18 +31,22 @@ import ( "encoding/json" "encoding/pem" "fmt" - "golang.org/x/sync/errgroup" "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" "github.com/go-openapi/swag" @@ -55,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" @@ -495,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) } @@ -515,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") @@ -537,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) } @@ -559,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)) }