From 0ee9d1beabbaa00f6e7b848c1c2b5b8631d243cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Apr 2022 07:12:23 -0500 Subject: [PATCH 01/53] Bump github.com/hashicorp/go-secure-stdlib/parseutil from 0.1.3 to 0.1.4 (#1620) * Bump github.com/hashicorp/go-secure-stdlib/parseutil from 0.1.3 to 0.1.4 Bumps [github.com/hashicorp/go-secure-stdlib/parseutil](https://github.com/hashicorp/go-secure-stdlib) from 0.1.3 to 0.1.4. - [Release notes](https://github.com/hashicorp/go-secure-stdlib/releases) - [Commits](https://github.com/hashicorp/go-secure-stdlib/compare/awsutil/v0.1.3...awsutil/v0.1.4) --- updated-dependencies: - dependency-name: github.com/hashicorp/go-secure-stdlib/parseutil dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * update codegen Signed-off-by: cpanato Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: cpanato --- go.mod | 2 +- go.sum | 4 ++-- .../hashicorp/go-secure-stdlib/parseutil/parseutil.go | 5 +++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 57826b43872..f88e3b62b92 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/hashicorp/go-cleanhttp v0.5.2 github.com/hashicorp/go-retryablehttp v0.7.0 github.com/hashicorp/go-rootcerts v1.0.2 - github.com/hashicorp/go-secure-stdlib/parseutil v0.1.3 + github.com/hashicorp/go-secure-stdlib/parseutil v0.1.4 github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 github.com/in-toto/in-toto-golang v0.3.4-0.20211211042327-af1f9fb822bf github.com/kelseyhightower/envconfig v1.4.0 diff --git a/go.sum b/go.sum index ec81bc735db..5732ae23fbe 100644 --- a/go.sum +++ b/go.sum @@ -1335,8 +1335,8 @@ github.com/hashicorp/go-secure-stdlib/mlock v0.1.2 h1:p4AKXPPS24tO8Wc8i1gLvSKdmk github.com/hashicorp/go-secure-stdlib/mlock v0.1.2/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.2/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= -github.com/hashicorp/go-secure-stdlib/parseutil v0.1.3 h1:geBw3SBrxQq+buvbf4K+Qltv1gjaXJxy8asD4CjGYow= -github.com/hashicorp/go-secure-stdlib/parseutil v0.1.3/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.4 h1:hrIH/qrOTHfG9a1Jz6Z2jQf7Xe77AaD464W1fCFLwPQ= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.4/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= github.com/hashicorp/go-secure-stdlib/password v0.1.1/go.mod h1:9hH302QllNwu1o2TGYtSk8I8kTAN0ca1EHpwhm5Mmzo= github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= diff --git a/third_party/VENDOR-LICENSE/github.com/hashicorp/go-secure-stdlib/parseutil/parseutil.go b/third_party/VENDOR-LICENSE/github.com/hashicorp/go-secure-stdlib/parseutil/parseutil.go index db808105b4b..167953158c8 100644 --- a/third_party/VENDOR-LICENSE/github.com/hashicorp/go-secure-stdlib/parseutil/parseutil.go +++ b/third_party/VENDOR-LICENSE/github.com/hashicorp/go-secure-stdlib/parseutil/parseutil.go @@ -337,6 +337,11 @@ func ParseString(in interface{}) (string, error) { } func ParseCommaStringSlice(in interface{}) ([]string, error) { + jsonIn, ok := in.(json.Number) + if ok { + in = jsonIn.String() + } + rawString, ok := in.(string) if ok && rawString == "" { return []string{}, nil From 09f1b79b8150b7521d6d8251ce6ff6ab4c1be851 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Apr 2022 14:28:21 -0500 Subject: [PATCH 02/53] Bump github.com/xanzy/go-gitlab from 0.62.0 to 0.63.0 (#1745) Bumps [github.com/xanzy/go-gitlab](https://github.com/xanzy/go-gitlab) from 0.62.0 to 0.63.0. - [Release notes](https://github.com/xanzy/go-gitlab/releases) - [Changelog](https://github.com/xanzy/go-gitlab/blob/master/releases_test.go) - [Commits](https://github.com/xanzy/go-gitlab/compare/v0.62.0...v0.63.0) --- updated-dependencies: - dependency-name: github.com/xanzy/go-gitlab dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f88e3b62b92..54817b28b04 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,7 @@ require ( github.com/stretchr/testify v1.7.1 github.com/theupdateframework/go-tuf v0.0.0-20220211205608-f0c3294f63b9 github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 - github.com/xanzy/go-gitlab v0.62.0 + github.com/xanzy/go-gitlab v0.63.0 golang.org/x/net v0.0.0-20220325170049-de3da57026de golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886 diff --git a/go.sum b/go.sum index 5732ae23fbe..eac01859be7 100644 --- a/go.sum +++ b/go.sum @@ -2220,8 +2220,8 @@ github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr github.com/withfig/autocomplete-tools/packages/cobra v0.0.0-20220122124547-31d3821a6898 h1:2Z+iziYPiyWk5hVJ3EYLn/i33Tj5ukytaJA0Th9tbgc= github.com/withfig/autocomplete-tools/packages/cobra v0.0.0-20220122124547-31d3821a6898/go.mod h1:cKObXQ6PVFO7bHUd5jpApXvMIt55Ewz7UdMiC05ONxI= github.com/xanzy/go-gitlab v0.31.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= -github.com/xanzy/go-gitlab v0.62.0 h1:D3WuIK1UJ7JPSiYI077PQaU5dcPEshpimCSP07Do1aQ= -github.com/xanzy/go-gitlab v0.62.0/go.mod h1:F0QEXwmqiBUxCgJm8fE9S+1veX4XC9Z4cfaAbqwk4YM= +github.com/xanzy/go-gitlab v0.63.0 h1:a9fXpKWykUS6dowapFej/2Wjf4aOAEFC1q2ZIcz4IpI= +github.com/xanzy/go-gitlab v0.63.0/go.mod h1:F0QEXwmqiBUxCgJm8fE9S+1veX4XC9Z4cfaAbqwk4YM= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= From 7d503e13071902c384b34217ebec9c37299d5ef5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Apr 2022 14:28:36 -0500 Subject: [PATCH 03/53] Bump mikefarah/yq from 4.24.2 to 4.24.4 (#1746) Bumps [mikefarah/yq](https://github.com/mikefarah/yq) from 4.24.2 to 4.24.4. - [Release notes](https://github.com/mikefarah/yq/releases) - [Changelog](https://github.com/mikefarah/yq/blob/master/release_notes.txt) - [Commits](https://github.com/mikefarah/yq/compare/bc2118736bca883de2e2c345bb7f7ef52c994920...13a27e8b2da75353d4172a98d9ead8a79b36edc9) --- updated-dependencies: - dependency-name: mikefarah/yq dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/kind-cluster-image-policy.yaml | 2 +- .github/workflows/kind-e2e-cosigned.yaml | 2 +- .github/workflows/kind-verify-attestation.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/kind-cluster-image-policy.yaml b/.github/workflows/kind-cluster-image-policy.yaml index f63e353677d..01666ed65af 100644 --- a/.github/workflows/kind-cluster-image-policy.yaml +++ b/.github/workflows/kind-cluster-image-policy.yaml @@ -59,7 +59,7 @@ jobs: - uses: imranismail/setup-kustomize@8fa954828ed3cfa7a487a2ba9f7104899bb48b2f # v1.6.1 - name: Install yq - uses: mikefarah/yq@bc2118736bca883de2e2c345bb7f7ef52c994920 # v4.16.2 + uses: mikefarah/yq@13a27e8b2da75353d4172a98d9ead8a79b36edc9 # v4.16.2 - name: Setup mirror uses: chainguard-dev/actions/setup-mirror@main diff --git a/.github/workflows/kind-e2e-cosigned.yaml b/.github/workflows/kind-e2e-cosigned.yaml index 32295f0c06e..c6efdc7f13a 100644 --- a/.github/workflows/kind-e2e-cosigned.yaml +++ b/.github/workflows/kind-e2e-cosigned.yaml @@ -53,7 +53,7 @@ jobs: - uses: imranismail/setup-kustomize@8fa954828ed3cfa7a487a2ba9f7104899bb48b2f # v1.6.1 - name: Install yq - uses: mikefarah/yq@bc2118736bca883de2e2c345bb7f7ef52c994920 # v4.16.2 + uses: mikefarah/yq@13a27e8b2da75353d4172a98d9ead8a79b36edc9 # v4.16.2 - name: Install Cosign run: | diff --git a/.github/workflows/kind-verify-attestation.yaml b/.github/workflows/kind-verify-attestation.yaml index 553313d04ce..db5b4c41260 100644 --- a/.github/workflows/kind-verify-attestation.yaml +++ b/.github/workflows/kind-verify-attestation.yaml @@ -57,7 +57,7 @@ jobs: - uses: imjasonh/setup-ko@2c3450ca27f6e6f2b02e72a40f2163c281a1f675 # v0.4 - name: Install yq - uses: mikefarah/yq@bc2118736bca883de2e2c345bb7f7ef52c994920 # v4.16.2 + uses: mikefarah/yq@13a27e8b2da75353d4172a98d9ead8a79b36edc9 # v4.16.2 - name: build cosign run: | From 19279dd169a3c1b114790323d1419845687f3363 Mon Sep 17 00:00:00 2001 From: Matt Moore Date: Tue, 12 Apr 2022 13:19:52 -0700 Subject: [PATCH 04/53] Move the KMS integration imports into the binary entrypoints (#1744) * Move the KMS integration imports into the binary entrypoints Related: https://github.com/sigstore/sigstore/issues/384 Related: https://github.com/sigstore/sigstore/issues/386 Signed-off-by: Matt Moore * Remove the fake import Signed-off-by: Matt Moore --- cmd/cosign/main.go | 6 ++++++ cmd/cosign/policy_webhook/main.go | 6 ++++++ pkg/reconciler/clusterimagepolicy/clusterimagepolicy.go | 7 ------- pkg/signature/keys.go | 6 ------ 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/cmd/cosign/main.go b/cmd/cosign/main.go index d6d1b89c230..00bb0b81ede 100644 --- a/cmd/cosign/main.go +++ b/cmd/cosign/main.go @@ -21,6 +21,12 @@ import ( "strings" "github.com/sigstore/cosign/cmd/cosign/cli" + + // Register the provider-specific plugins + _ "github.com/sigstore/sigstore/pkg/signature/kms/aws" + _ "github.com/sigstore/sigstore/pkg/signature/kms/azure" + _ "github.com/sigstore/sigstore/pkg/signature/kms/gcp" + _ "github.com/sigstore/sigstore/pkg/signature/kms/hashivault" ) func main() { diff --git a/cmd/cosign/policy_webhook/main.go b/cmd/cosign/policy_webhook/main.go index da136e19303..44a4700419d 100644 --- a/cmd/cosign/policy_webhook/main.go +++ b/cmd/cosign/policy_webhook/main.go @@ -34,6 +34,12 @@ import ( "github.com/sigstore/cosign/pkg/apis/cosigned/v1alpha1" "github.com/sigstore/cosign/pkg/reconciler/clusterimagepolicy" + + // Register the provider-specific plugins + _ "github.com/sigstore/sigstore/pkg/signature/kms/aws" + _ "github.com/sigstore/sigstore/pkg/signature/kms/azure" + _ "github.com/sigstore/sigstore/pkg/signature/kms/gcp" + _ "github.com/sigstore/sigstore/pkg/signature/kms/hashivault" ) func main() { diff --git a/pkg/reconciler/clusterimagepolicy/clusterimagepolicy.go b/pkg/reconciler/clusterimagepolicy/clusterimagepolicy.go index 7b95067a3af..5ccfd51cb8e 100644 --- a/pkg/reconciler/clusterimagepolicy/clusterimagepolicy.go +++ b/pkg/reconciler/clusterimagepolicy/clusterimagepolicy.go @@ -44,13 +44,6 @@ import ( sigs "github.com/sigstore/cosign/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/kms" signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" - - // Register the provider-specific plugins - _ "github.com/sigstore/sigstore/pkg/signature/kms/aws" - _ "github.com/sigstore/sigstore/pkg/signature/kms/azure" - _ "github.com/sigstore/sigstore/pkg/signature/kms/fake" - _ "github.com/sigstore/sigstore/pkg/signature/kms/gcp" - _ "github.com/sigstore/sigstore/pkg/signature/kms/hashivault" ) // Reconciler implements clusterimagepolicyreconciler.Interface for diff --git a/pkg/signature/keys.go b/pkg/signature/keys.go index 3fa1310354e..775be117d15 100644 --- a/pkg/signature/keys.go +++ b/pkg/signature/keys.go @@ -35,12 +35,6 @@ import ( "github.com/sigstore/sigstore/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/kms" - - // Register the provider-specific plugins - _ "github.com/sigstore/sigstore/pkg/signature/kms/aws" - _ "github.com/sigstore/sigstore/pkg/signature/kms/azure" - _ "github.com/sigstore/sigstore/pkg/signature/kms/gcp" - _ "github.com/sigstore/sigstore/pkg/signature/kms/hashivault" ) var ( From 427d83a63109e83c0a3dba725b52467882f2a189 Mon Sep 17 00:00:00 2001 From: Denny Date: Tue, 12 Apr 2022 17:20:47 -0400 Subject: [PATCH 05/53] Create convert functions for internal CIP (#1736) Signed-off-by: Denny Hoang --- pkg/apis/config/image_policies_test.go | 1 - .../clusterimagepolicy_types.go | 72 ++++++++++++++++-- .../kubernetes/webhook/validator_test.go | 6 +- .../clusterimagepolicy/clusterimagepolicy.go | 73 +------------------ .../clusterimagepolicy_test.go | 23 +----- 5 files changed, 74 insertions(+), 101 deletions(-) diff --git a/pkg/apis/config/image_policies_test.go b/pkg/apis/config/image_policies_test.go index 7ecea12521d..7c9c4015c44 100644 --- a/pkg/apis/config/image_policies_test.go +++ b/pkg/apis/config/image_policies_test.go @@ -158,7 +158,6 @@ func checkPublicKey(t *testing.T, gotKey *ecdsa.PublicKey) { // pem.EncodeToMemory has an extra newline at the end got := strings.TrimSuffix(string(pemBytes), "\n") - if got != inlineKeyData { t.Errorf("Did not get what I wanted %s, got %s", inlineKeyData, string(pemBytes)) } diff --git a/pkg/cosign/kubernetes/webhook/clusterimagepolicy/clusterimagepolicy_types.go b/pkg/cosign/kubernetes/webhook/clusterimagepolicy/clusterimagepolicy_types.go index 9ac247c04c8..41706d53fc1 100644 --- a/pkg/cosign/kubernetes/webhook/clusterimagepolicy/clusterimagepolicy_types.go +++ b/pkg/cosign/kubernetes/webhook/clusterimagepolicy/clusterimagepolicy_types.go @@ -21,6 +21,8 @@ import ( "encoding/pem" "github.com/sigstore/cosign/pkg/apis/cosigned/v1alpha1" + + "knative.dev/pkg/apis" ) // ClusterImagePolicy defines the images that go through verification @@ -37,7 +39,7 @@ type Authority struct { // +optional Key *KeyRef `json:"key,omitempty"` // +optional - Keyless *v1alpha1.KeylessRef `json:"keyless,omitempty"` + Keyless *KeylessRef `json:"keyless,omitempty"` // +optional Sources []v1alpha1.Source `json:"source,omitempty"` // +optional @@ -50,15 +52,21 @@ type KeyRef struct { // Data contains the inline public key // +optional Data string `json:"data,omitempty"` - // KMS contains the KMS url of the public key - // +optional - KMS string `json:"kms,omitempty"` // PublicKeys are not marshalled because JSON unmarshalling // errors for *big.Int // +optional PublicKeys []*ecdsa.PublicKey `json:"-"` } +type KeylessRef struct { + // +optional + URL *apis.URL `json:"url,omitempty"` + // +optional + Identities []v1alpha1.Identity `json:"identities,omitempty"` + // +optional + CACert *KeyRef `json:"ca-cert,omitempty"` +} + // UnmarshalJSON populates the PublicKeys using Data because // JSON unmashalling errors for *big.Int func (k *KeyRef) UnmarshalJSON(data []byte) error { @@ -73,7 +81,7 @@ func (k *KeyRef) UnmarshalJSON(data []byte) error { k.Data = ret["data"] if ret["data"] != "" { - publicKeys, err = convertKeyDataToPublicKeys(ret["data"]) + publicKeys, err = ConvertKeyDataToPublicKeys(ret["data"]) if err != nil { return err } @@ -84,9 +92,59 @@ func (k *KeyRef) UnmarshalJSON(data []byte) error { return nil } -func convertKeyDataToPublicKeys(pubKey string) ([]*ecdsa.PublicKey, error) { - keys := []*ecdsa.PublicKey{} +func ConvertClusterImagePolicyV1alpha1ToWebhook(in *v1alpha1.ClusterImagePolicy) *ClusterImagePolicy { + copyIn := in.DeepCopy() + outAuthorities := make([]Authority, 0) + for _, authority := range copyIn.Spec.Authorities { + outAuthority := convertAuthorityV1Alpha1ToWebhook(authority) + outAuthorities = append(outAuthorities, *outAuthority) + } + + return &ClusterImagePolicy{ + Images: copyIn.Spec.Images, + Authorities: outAuthorities, + } +} + +func convertAuthorityV1Alpha1ToWebhook(in v1alpha1.Authority) *Authority { + keyRef := convertKeyRefV1Alpha1ToWebhook(in.Key) + keylessRef := convertKeylessRefV1Alpha1ToWebhook(in.Keyless) + + return &Authority{ + Key: keyRef, + Keyless: keylessRef, + Sources: in.Sources, + CTLog: in.CTLog, + } +} + +func convertKeyRefV1Alpha1ToWebhook(in *v1alpha1.KeyRef) *KeyRef { + if in == nil { + return nil + } + + return &KeyRef{ + Data: in.Data, + } +} + +func convertKeylessRefV1Alpha1ToWebhook(in *v1alpha1.KeylessRef) *KeylessRef { + if in == nil { + return nil + } + + CACertRef := convertKeyRefV1Alpha1ToWebhook(in.CACert) + + return &KeylessRef{ + URL: in.URL, + Identities: in.Identities, + CACert: CACertRef, + } +} + +func ConvertKeyDataToPublicKeys(pubKey string) ([]*ecdsa.PublicKey, error) { + keys := []*ecdsa.PublicKey{} pems := parsePems([]byte(pubKey)) for _, p := range pems { key, err := x509.ParsePKIXPublicKey(p.Bytes) diff --git a/pkg/cosign/kubernetes/webhook/validator_test.go b/pkg/cosign/kubernetes/webhook/validator_test.go index ad9b3ac56d8..f66772f4766 100644 --- a/pkg/cosign/kubernetes/webhook/validator_test.go +++ b/pkg/cosign/kubernetes/webhook/validator_test.go @@ -272,7 +272,7 @@ UoJou2P8sbDxpLiE/v3yLw1/jyOrCPWYHWFXnyyeGlkgSVefG54tNoK7Uw== }}, Authorities: []webhookcip.Authority{ { - Keyless: &v1alpha1.KeylessRef{ + Keyless: &webhookcip.KeylessRef{ URL: badURL, }, }, @@ -315,7 +315,7 @@ UoJou2P8sbDxpLiE/v3yLw1/jyOrCPWYHWFXnyyeGlkgSVefG54tNoK7Uw== }}, Authorities: []webhookcip.Authority{ { - Keyless: &v1alpha1.KeylessRef{ + Keyless: &webhookcip.KeylessRef{ URL: fulcioURL, }, }, @@ -358,7 +358,7 @@ UoJou2P8sbDxpLiE/v3yLw1/jyOrCPWYHWFXnyyeGlkgSVefG54tNoK7Uw== }}, Authorities: []webhookcip.Authority{ { - Keyless: &v1alpha1.KeylessRef{ + Keyless: &webhookcip.KeylessRef{ URL: fulcioURL, }, CTLog: &v1alpha1.TLog{ diff --git a/pkg/reconciler/clusterimagepolicy/clusterimagepolicy.go b/pkg/reconciler/clusterimagepolicy/clusterimagepolicy.go index 5ccfd51cb8e..4fef03bfd3c 100644 --- a/pkg/reconciler/clusterimagepolicy/clusterimagepolicy.go +++ b/pkg/reconciler/clusterimagepolicy/clusterimagepolicy.go @@ -17,10 +17,6 @@ package clusterimagepolicy import ( "context" "crypto" - "crypto/ecdsa" - "crypto/x509" - "encoding/json" - "encoding/pem" "fmt" "strings" @@ -30,12 +26,14 @@ import ( clusterimagepolicyreconciler "github.com/sigstore/cosign/pkg/client/injection/reconciler/cosigned/v1alpha1/clusterimagepolicy" webhookcip "github.com/sigstore/cosign/pkg/cosign/kubernetes/webhook/clusterimagepolicy" "github.com/sigstore/cosign/pkg/reconciler/clusterimagepolicy/resources" + corev1 "k8s.io/api/core/v1" apierrs "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes" corev1listers "k8s.io/client-go/listers/core/v1" + "knative.dev/pkg/logging" "knative.dev/pkg/reconciler" "knative.dev/pkg/system" @@ -66,24 +64,6 @@ var _ clusterimagepolicyreconciler.Finalizer = (*Reconciler)(nil) // ReconcileKind implements Interface.ReconcileKind. func (r *Reconciler) ReconcileKind(ctx context.Context, cip *v1alpha1.ClusterImagePolicy) reconciler.Event { cipCopy, cipErr := r.inlinePublicKeys(ctx, cip) - if cipErr != nil { - // Handle error here - r.handleCIPError(ctx, cip.Name) - return cipErr - } - - // Converting external CIP to webhook CIP - bytes, err := json.Marshal(&cipCopy.Spec) - if err != nil { - return err - } - - var webhookCIP *webhookcip.ClusterImagePolicy - if err := json.Unmarshal(bytes, &webhookCIP); err != nil { - return err - } - - webhookCIP, cipErr = r.convertKeyData(ctx, webhookCIP) if cipErr != nil { r.handleCIPError(ctx, cip.Name) // Note that we return the error about the Invalid cip here to make @@ -91,6 +71,8 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, cip *v1alpha1.ClusterIma return cipErr } + webhookCIP := webhookcip.ConvertClusterImagePolicyV1alpha1ToWebhook(cipCopy) + // See if the CM holding configs exists existing, err := r.configmaplister.ConfigMaps(system.Namespace()).Get(config.ImagePoliciesConfigName) if err != nil { @@ -142,24 +124,6 @@ func (r *Reconciler) FinalizeKind(ctx context.Context, cip *v1alpha1.ClusterImag return r.removeCIPEntry(ctx, existing, cip.Name) } -// convertKeyData will go through the CIP and try to convert key data -// to ecdsa.PublicKey and store it in the returned CIP -// When PublicKeys are successfully set, the authority key's data will be -// cleared out -func (r *Reconciler) convertKeyData(ctx context.Context, cip *webhookcip.ClusterImagePolicy) (*webhookcip.ClusterImagePolicy, error) { - for _, authority := range cip.Authorities { - if authority.Key != nil && authority.Key.Data != "" { - keys, err := convertAuthorityKeys(ctx, authority.Key.Data) - if err != nil { - return nil, err - } - // When publicKeys are successfully converted, clear out Data - authority.Key.PublicKeys = keys - } - } - return cip, nil -} - func (r *Reconciler) handleCIPError(ctx context.Context, cipName string) { // The CIP is invalid, try to remove CIP from the configmap existing, err := r.configmaplister.ConfigMaps(system.Namespace()).Get(config.ImagePoliciesConfigName) @@ -172,35 +136,6 @@ func (r *Reconciler) handleCIPError(ctx context.Context, cipName string) { } } -func convertAuthorityKeys(ctx context.Context, pubKey string) ([]*ecdsa.PublicKey, error) { - keys := []*ecdsa.PublicKey{} - - logging.FromContext(ctx).Debugf("Got public key: %v", pubKey) - - pems := parsePems([]byte(pubKey)) - for _, p := range pems { - key, err := x509.ParsePKIXPublicKey(p.Bytes) - if err != nil { - return nil, err - } - keys = append(keys, key.(*ecdsa.PublicKey)) - } - return keys, nil -} - -func parsePems(b []byte) []*pem.Block { - p, rest := pem.Decode(b) - if p == nil { - return nil - } - pems := []*pem.Block{p} - - if rest != nil { - return append(pems, parsePems(rest)...) - } - return pems -} - // inlinePublicKeys will go through the CIP and try to read the referenced // secrets, KMS keys and convert them into inlined data. Makes a copy of the CIP // before modifying it and returns the copy. diff --git a/pkg/reconciler/clusterimagepolicy/clusterimagepolicy_test.go b/pkg/reconciler/clusterimagepolicy/clusterimagepolicy_test.go index 61e496cf3ad..1220f8304bc 100644 --- a/pkg/reconciler/clusterimagepolicy/clusterimagepolicy_test.go +++ b/pkg/reconciler/clusterimagepolicy/clusterimagepolicy_test.go @@ -81,9 +81,6 @@ RCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ== // two entries but only one is being removed. For keyless entry. removeSingleEntryKeylessPatch = `[{"op":"remove","path":"/data/test-cip-2"}]` - // This is the patch for inlined secret for key ref data - inlinedSecretKeyPatch = `[{"op":"replace","path":"/data/test-cip","value":"{\"images\":[{\"glob\":\"ghcr.io/example/*\"}],\"authorities\":[{\"key\":{\"data\":\"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\\n-----END PUBLIC KEY-----\"}}]}"}]` - // This is the patch for inlined secret for keyless cakey ref data inlinedSecretKeylessPatch = `[{"op":"replace","path":"/data/test-cip-2","value":"{\"images\":[{\"glob\":\"ghcr.io/example/*\"}],\"authorities\":[{\"keyless\":{\"ca-cert\":{\"data\":\"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\\n-----END PUBLIC KEY-----\"}}}]}"}]` ) @@ -475,11 +472,10 @@ func TestReconcile(t *testing.T) { }, }}), ), - makeConfigMapWithTwoEntriesNotPublicKeyFromSecret(), makeSecret(keySecretName, validPublicKeyData), }, - WantPatches: []clientgotesting.PatchActionImpl{ - makePatch(inlinedSecretKeyPatch), + WantCreates: []runtime.Object{ + makeConfigMap(), }, PostConditions: []func(*testing.T, *TableRow){ AssertTrackingSecret(system.Namespace(), keySecretName), @@ -629,21 +625,6 @@ func makeConfigMapWithTwoEntries() *corev1.ConfigMap { } } -// Same as MakeConfigMapWithTwoEntries but the inline data is not the secret -// so we will replace it with the secret data -func makeConfigMapWithTwoEntriesNotPublicKeyFromSecret() *corev1.ConfigMap { - return &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: system.Namespace(), - Name: config.ImagePoliciesConfigName, - }, - Data: map[string]string{ - cipName: `{"images":[{"glob":"ghcr.io/example/*"}],"authorities":[{"key":{"data":"NOT A REAL PUBLIC KEY"}}]}`, - cipName2: "remove me please", - }, - } -} - func makePatch(patch string) clientgotesting.PatchActionImpl { return clientgotesting.PatchActionImpl{ ActionImpl: clientgotesting.ActionImpl{ From cf03ef22b2ec4d7302793764d47a738df565a7d5 Mon Sep 17 00:00:00 2001 From: Ville Aikas <11279988+vaikas@users.noreply.github.com> Date: Tue, 12 Apr 2022 16:06:12 -0700 Subject: [PATCH 06/53] Refactor policy related code, add support for vuln verify (#1747) * Refactor policy related code, add support for vuln verify Signed-off-by: Ville Aikas * Thanks @hectorj2f for catching a bad upstream rebase. Signed-off-by: Ville Aikas * Fix typo. Signed-off-by: Ville Aikas --- .../workflows/kind-verify-attestation.yaml | 51 ++++- cmd/cosign/cli/verify/verify_attestation.go | 79 +------- pkg/cosign/attestation/attestation.go | 9 + pkg/policy/attestation.go | 130 ++++++++++++ pkg/policy/attestation_test.go | 188 ++++++++++++++++++ pkg/policy/testdata/valid/custom | 1 + pkg/policy/testdata/valid/vuln | 1 + .../testdata/attestations/vuln-predicate.json | 21 ++ test/testdata/policies/cue-vuln-fails.cue | 25 +++ test/testdata/policies/cue-vuln-works.cue | 22 ++ 10 files changed, 443 insertions(+), 84 deletions(-) create mode 100644 pkg/policy/attestation.go create mode 100644 pkg/policy/attestation_test.go create mode 100644 pkg/policy/testdata/valid/custom create mode 100644 pkg/policy/testdata/valid/vuln create mode 100644 test/testdata/attestations/vuln-predicate.json create mode 100644 test/testdata/policies/cue-vuln-fails.cue create mode 100644 test/testdata/policies/cue-vuln-works.cue diff --git a/.github/workflows/kind-verify-attestation.yaml b/.github/workflows/kind-verify-attestation.yaml index db5b4c41260..558a9d156a9 100644 --- a/.github/workflows/kind-verify-attestation.yaml +++ b/.github/workflows/kind-verify-attestation.yaml @@ -45,7 +45,10 @@ jobs: GO111MODULE: on GOFLAGS: -ldflags=-s -ldflags=-w KOCACHE: ~/ko - COSIGN_EXPERIMENTAL: true + # Trust the custom Rekor API endpoint for fetching the Public Key from it. + SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY: "true" + # We are only testing keyless here, so set it. + COSIGN_EXPERIMENTAL: "true" steps: - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 @@ -89,28 +92,28 @@ jobs: - name: Create attestation for it run: | echo -n 'foobar e2e test' > ./predicate-file - SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY=1 COSIGN_EXPERIMENTAL=1 ./cosign attest --predicate ./predicate-file --fulcio-url ${{ env.FULCIO_URL }} --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry --force ${{ env.demoimage }} --identity-token ${{ env.OIDC_TOKEN }} + ./cosign attest --predicate ./predicate-file --fulcio-url ${{ env.FULCIO_URL }} --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry --force ${{ env.demoimage }} --identity-token ${{ env.OIDC_TOKEN }} - name: Verify with cosign run: | - SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY=1 COSIGN_EXPERIMENTAL=1 ./cosign verify --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry ${{ env.demoimage }} + ./cosign verify --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry ${{ env.demoimage }} - - name: Verify attestation with cosign, works + - name: Verify custom attestation with cosign, works run: | - echo '::group:: test verify-attestation success' - if ! SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY=1 COSIGN_EXPERIMENTAL=1 ./cosign verify-attestation --policy ./test/testdata/policies/cue-works.cue --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry ${{ env.demoimage }} ; then + echo '::group:: test custom verify-attestation success' + if ! ./cosign verify-attestation --policy ./test/testdata/policies/cue-works.cue --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry ${{ env.demoimage }} ; then echo Failed to verify attestation with a valid policy exit 1 else - echo Successfully validated attestation with a valid policy + echo Successfully validated custom attestation with a valid policy fi echo '::endgroup::' - - name: Verify attestation with cosign, fails + - name: Verify custom attestation with cosign, fails run: | - echo '::group:: test verify-attestation success' - if SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY=1 COSIGN_EXPERIMENTAL=1 ./cosign verify-attestation --policy ./test/testdata/policies/cue-fails.cue --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry ${{ env.demoimage }} ; then - echo verify-attestation succeeded with cue policy that should not work + echo '::group:: test custom verify-attestation success' + if ./cosign verify-attestation --policy ./test/testdata/policies/cue-fails.cue --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry ${{ env.demoimage }} ; then + echo custom verify-attestation succeeded with cue policy that should not work exit 1 else echo Successfully failed a policy that should not work @@ -120,3 +123,29 @@ jobs: - name: Collect diagnostics if: ${{ failure() }} uses: chainguard-dev/actions/kind-diag@84c993eaf02da1c325854fb272a4df9184bd80fc # main + + - name: Create vuln attestation for it + run: | + ./cosign attest --predicate ./test/testdata/attestations/vuln-predicate.json --type vuln --fulcio-url ${{ env.FULCIO_URL }} --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry --force ${{ env.demoimage }} --identity-token ${{ env.OIDC_TOKEN }} + + - name: Verify vuln attestation with cosign, works + run: | + echo '::group:: test vuln verify-attestation success' + if ! ./cosign verify-attestation --type vuln --policy ./test/testdata/policies/cue-vuln-works.cue --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry ${{ env.demoimage }} ; then + echo Failed to verify attestation with a valid policy + exit 1 + else + echo Successfully validated vuln attestation with a valid policy + fi + echo '::endgroup::' + + - name: Verify vuln attestation with cosign, fails + run: | + echo '::group:: test vuln verify-attestation success' + if ./cosign verify-attestation --type vuln --policy ./test/testdata/policies/cue-vuln-fails.cue --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry ${{ env.demoimage }} ; then + echo verify-attestation succeeded with cue policy that should not work + exit 1 + else + echo Successfully failed a policy that should not work + fi + echo '::endgroup::' diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go index e868eb8fb11..fb4cb7713b0 100644 --- a/cmd/cosign/cli/verify/verify_attestation.go +++ b/cmd/cosign/cli/verify/verify_attestation.go @@ -18,15 +18,12 @@ package verify import ( "context" "crypto" - "encoding/base64" - "encoding/json" "flag" "fmt" "os" "path/filepath" "github.com/google/go-containerregistry/pkg/name" - "github.com/in-toto/in-toto-golang/in_toto" "github.com/pkg/errors" "github.com/sigstore/cosign/pkg/cosign/pkcs11key" "github.com/sigstore/cosign/pkg/cosign/rego" @@ -39,6 +36,7 @@ import ( "github.com/sigstore/cosign/pkg/cosign" "github.com/sigstore/cosign/pkg/cosign/cue" "github.com/sigstore/cosign/pkg/cosign/pivkey" + "github.com/sigstore/cosign/pkg/policy" sigs "github.com/sigstore/cosign/pkg/signature" ) @@ -47,11 +45,11 @@ import ( type VerifyAttestationCommand struct { options.RegistryOptions CheckClaims bool + KeyRef string CertRef string CertEmail string CertOidcIssuer string CertChain string - KeyRef string Sk bool Slot string Output string @@ -182,80 +180,15 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e var validationErrors []error for _, vp := range verified { - var payloadData map[string]interface{} - - p, err := vp.Payload() - if err != nil { - return errors.Wrap(err, "could not get payload") - } - - err = json.Unmarshal(p, &payloadData) + payload, err := policy.AttestationToPayloadJSON(ctx, c.PredicateType, vp) if err != nil { - return errors.Wrap(err, "unmarshal payload data") + return errors.Wrap(err, "converting to consumable policy validation") } - - var decodedPayload []byte - if val, ok := payloadData["payload"]; ok { - decodedPayload, err = base64.StdEncoding.DecodeString(val.(string)) - if err != nil { - return fmt.Errorf("could not decode 'payload': %w", err) - } - } else { - return fmt.Errorf("could not find 'payload' in payload data") - } - - predicateURI, ok := options.PredicateTypeMap[c.PredicateType] - if !ok { - return fmt.Errorf("invalid predicate type: %s", c.PredicateType) - } - - // Only apply the policy against the requested predicate type - var statement in_toto.Statement - if err := json.Unmarshal(decodedPayload, &statement); err != nil { - return fmt.Errorf("unmarshal in-toto statement: %w", err) - } - if statement.PredicateType != predicateURI { + if len(payload) == 0 { + // This is not the predicate type we're looking for. continue } - var payload []byte - switch c.PredicateType { - case options.PredicateCustom: - payload, err = json.Marshal(statement) - if err != nil { - return fmt.Errorf("error when generating CosignStatement: %w", err) - } - case options.PredicateLink: - var linkStatement in_toto.LinkStatement - if err := json.Unmarshal(decodedPayload, &linkStatement); err != nil { - return fmt.Errorf("unmarshal LinkStatement: %w", err) - } - payload, err = json.Marshal(linkStatement) - if err != nil { - return fmt.Errorf("error when generating LinkStatement: %w", err) - } - case options.PredicateSLSA: - var slsaProvenanceStatement in_toto.ProvenanceStatement - if err := json.Unmarshal(decodedPayload, &slsaProvenanceStatement); err != nil { - return fmt.Errorf("unmarshal ProvenanceStatement: %w", err) - } - payload, err = json.Marshal(slsaProvenanceStatement) - if err != nil { - return fmt.Errorf("error when generating ProvenanceStatement: %w", err) - } - case options.PredicateSPDX: - var spdxStatement in_toto.SPDXStatement - if err := json.Unmarshal(decodedPayload, &spdxStatement); err != nil { - return fmt.Errorf("unmarshal SPDXStatement: %w", err) - } - payload, err = json.Marshal(spdxStatement) - if err != nil { - return fmt.Errorf("error when generating SPDXStatement: %w", err) - } - default: - return fmt.Errorf("unsupported predicate type: %s", c.PredicateType) - } - if len(cuePolicies) > 0 { fmt.Fprintf(os.Stderr, "will be validating against CUE policies: %v\n", cuePolicies) cueValidationErr := cue.ValidateJSON(payload, cuePolicies) diff --git a/pkg/cosign/attestation/attestation.go b/pkg/cosign/attestation/attestation.go index ce113220695..3215c32ca87 100644 --- a/pkg/cosign/attestation/attestation.go +++ b/pkg/cosign/attestation/attestation.go @@ -50,6 +50,15 @@ type CosignVulnPredicate struct { Metadata Metadata `json:"metadata"` } +// I think this will be moving to upstream in-toto in the fullness of time +// but creating it here for now so that we have a way to deserialize it +// as a InToto Statement +// https://github.com/in-toto/attestation/issues/58 +type CosignVulnStatement struct { + in_toto.StatementHeader + Predicate CosignVulnPredicate `json:"predicate"` +} + type Invocation struct { Parameters interface{} `json:"parameters"` URI string `json:"uri"` diff --git a/pkg/policy/attestation.go b/pkg/policy/attestation.go new file mode 100644 index 00000000000..44808201283 --- /dev/null +++ b/pkg/policy/attestation.go @@ -0,0 +1,130 @@ +// +// 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 policy + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + + "github.com/in-toto/in-toto-golang/in_toto" + "github.com/pkg/errors" + "github.com/sigstore/cosign/pkg/oci" + + "github.com/sigstore/cosign/cmd/cosign/cli/options" + "github.com/sigstore/cosign/pkg/cosign/attestation" +) + +// AttestationToPayloadJSON takes in a verified Attestation (oci.Signature) and +// marshals it into a JSON depending on the payload that's then consumable +// by policy engine like cue, rego, etc. +// +// Anything fed here must have been validated with either +// `VerifyLocalImageAttestations` or `VerifyImageAttestations` +// +// If there's no error, and payload is empty means the predicateType did not +// match the attestation. +func AttestationToPayloadJSON(ctx context.Context, predicateType string, verifiedAttestation oci.Signature) ([]byte, error) { + // Check the predicate up front, no point in wasting time if it's invalid. + predicateURI, ok := options.PredicateTypeMap[predicateType] + if !ok { + return nil, fmt.Errorf("invalid predicate type: %s", predicateType) + } + + var payloadData map[string]interface{} + + p, err := verifiedAttestation.Payload() + if err != nil { + return nil, errors.Wrap(err, "getting payload") + } + + err = json.Unmarshal(p, &payloadData) + if err != nil { + return nil, errors.Wrap(err, "unmarshaling payload data") + } + + var decodedPayload []byte + if val, ok := payloadData["payload"]; ok { + decodedPayload, err = base64.StdEncoding.DecodeString(val.(string)) + if err != nil { + return nil, errors.Wrap(err, "decoding payload") + } + } else { + return nil, fmt.Errorf("could not find payload in payload data") + } + + // Only apply the policy against the requested predicate type + var statement in_toto.Statement + if err := json.Unmarshal(decodedPayload, &statement); err != nil { + return nil, fmt.Errorf("unmarshal in-toto statement: %w", err) + } + if statement.PredicateType != predicateURI { + // This is not the predicate we're looking for, so skip it. + return nil, nil + } + + // NB: In many (all?) of these cases, we could just return the + // 'json.Marshal', but we check for errors here to decorate them + // with more meaningful error message. + var payload []byte + switch predicateType { + case options.PredicateCustom: + payload, err = json.Marshal(statement) + if err != nil { + return nil, errors.Wrap(err, "generating CosignStatement") + } + case options.PredicateLink: + var linkStatement in_toto.LinkStatement + if err := json.Unmarshal(decodedPayload, &linkStatement); err != nil { + return nil, errors.Wrap(err, "unmarshaling LinkStatement") + } + payload, err = json.Marshal(linkStatement) + if err != nil { + return nil, errors.Wrap(err, "marshaling LinkStatement") + } + case options.PredicateSLSA: + var slsaProvenanceStatement in_toto.ProvenanceStatement + if err := json.Unmarshal(decodedPayload, &slsaProvenanceStatement); err != nil { + return nil, errors.Wrap(err, "unmarshaling ProvenanceStatement") + } + payload, err = json.Marshal(slsaProvenanceStatement) + if err != nil { + return nil, errors.Wrap(err, "marshaling ProvenanceStatement") + } + case options.PredicateSPDX: + var spdxStatement in_toto.SPDXStatement + if err := json.Unmarshal(decodedPayload, &spdxStatement); err != nil { + return nil, errors.Wrap(err, "unmarshaling SPDXStatement") + } + payload, err = json.Marshal(spdxStatement) + if err != nil { + return nil, errors.Wrap(err, "marshaling SPDXStatement") + } + case options.PredicateVuln: + var vulnStatement attestation.CosignVulnStatement + if err := json.Unmarshal(decodedPayload, &vulnStatement); err != nil { + return nil, errors.Wrap(err, "unmarshaling CosignVulnStatement") + } + payload, err = json.Marshal(vulnStatement) + if err != nil { + return nil, errors.Wrap(err, "marshaling CosignVulnStatement") + } + default: + return nil, fmt.Errorf("unsupported predicate type: %s", predicateType) + } + return payload, nil +} diff --git a/pkg/policy/attestation_test.go b/pkg/policy/attestation_test.go new file mode 100644 index 00000000000..c79fb4babda --- /dev/null +++ b/pkg/policy/attestation_test.go @@ -0,0 +1,188 @@ +// +// 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 policy + +import ( + "context" + "crypto/x509" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "strings" + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/in-toto/in-toto-golang/in_toto" + "github.com/sigstore/cosign/pkg/cosign/attestation" + "github.com/sigstore/cosign/pkg/cosign/bundle" + "github.com/sigstore/cosign/pkg/oci" + "github.com/sigstore/cosign/pkg/oci/static" +) + +type failingAttestation struct { +} + +func (fa *failingAttestation) Payload() ([]byte, error) { + return nil, fmt.Errorf("inducing test failure") +} +func (fa *failingAttestation) Annotations() (map[string]string, error) { + return nil, fmt.Errorf("unimplemented") +} +func (fa *failingAttestation) Base64Signature() (string, error) { + return "", fmt.Errorf("unimplemented") +} +func (fa *failingAttestation) Cert() (*x509.Certificate, error) { + return nil, fmt.Errorf("unimplemented") +} +func (fa *failingAttestation) Chain() ([]*x509.Certificate, error) { + return nil, fmt.Errorf("unimplemented") +} +func (fa *failingAttestation) Bundle() (*bundle.RekorBundle, error) { + return nil, fmt.Errorf("unimplemented") +} +func (fa *failingAttestation) Digest() (v1.Hash, error) { + return v1.Hash{}, fmt.Errorf("unimplemented") +} +func (fa *failingAttestation) DiffID() (v1.Hash, error) { + return v1.Hash{}, fmt.Errorf("unimplemented") +} +func (fa *failingAttestation) Compressed() (io.ReadCloser, error) { + return nil, fmt.Errorf("unimplemented") +} +func (fa *failingAttestation) Uncompressed() (io.ReadCloser, error) { + return nil, fmt.Errorf("unimplemented") +} +func (fa *failingAttestation) Size() (int64, error) { + return 0, fmt.Errorf("unimplemented") +} +func (fa *failingAttestation) MediaType() (types.MediaType, error) { + return types.DockerConfigJSON, fmt.Errorf("unimplemented") +} + +var _ oci.Signature = (*failingAttestation)(nil) + +const ( + // Result of "echo 'nottotostatement' | base64" + // invalidTotoStatement = "bm90dG90b3N0YXRlbWVudAo=" + invalidTotoStatement = `{"payloadType":"application/vnd.in-toto+json","payload":"bm90dG90b3N0YXRlbWVudAo"}` +) + +func checkFailure(t *testing.T, want string, err error) { + t.Helper() + if err == nil { + t.Fatalf("Expected error, got none") + } + if !strings.Contains(err.Error(), want) { + t.Errorf("Failed to get the expected error of %q, got: %s", want, err) + } +} + +func TestFailures(t *testing.T) { + tests := []struct { + payload string + predicateType string + wantErrSubstring string + }{{payload: "", predicateType: "notvalidpredicate", wantErrSubstring: "invalid predicate type"}, + {payload: "", wantErrSubstring: "unmarshaling payload data"}, {payload: "{badness", wantErrSubstring: "unmarshaling payload data"}, + {payload: `{"payloadType":"notmarshallable}`, wantErrSubstring: "unmarshaling payload data"}, + {payload: `{"payload":"shou!ln'twork"}`, wantErrSubstring: "decoding payload"}, + {payload: `{"payloadType":"finebutnopayload"}`, wantErrSubstring: "could not find payload"}, + {payload: invalidTotoStatement, wantErrSubstring: "decoding payload: illegal base64"}, + } + for _, tc := range tests { + att, err := static.NewSignature([]byte(tc.payload), "") + if err != nil { + t.Fatal("Failed to create static.NewSignature: ", err) + } + predicateType := tc.predicateType + if predicateType == "" { + predicateType = "custom" + } + _, err = AttestationToPayloadJSON(context.TODO(), predicateType, att) + checkFailure(t, tc.wantErrSubstring, err) + } +} + +// TestMalformedPayload tests various non-predicate specific failures that +// are done even before we start processing the payload. +// This just stands alone since didn't want to complicate above tests with +// constructing different attestations there. +func TestErroringPayload(t *testing.T) { + // Payload() call fails + _, err := AttestationToPayloadJSON(context.TODO(), "custom", &failingAttestation{}) + checkFailure(t, "inducing test failure", err) +} +func TestAttestationToPayloadJson(t *testing.T) { + dir := "valid" + files := getDirFiles(t, dir) + for _, fileName := range files { + bytes := readAttestationFromTestFile(t, dir, fileName) + ociSig, err := static.NewSignature(bytes, "") + if err != nil { + t.Fatal("Failed to create static.NewSignature: ", err) + } + jsonBytes, err := AttestationToPayloadJSON(context.TODO(), fileName, ociSig) + if err != nil { + t.Fatalf("Failed to convert : %s", err) + } + switch fileName { + case "custom": + var intoto in_toto.Statement + if err := json.Unmarshal(jsonBytes, &intoto); err != nil { + t.Fatal("Wanted custom statement, can't unmarshal to it: ", err) + } + checkPredicateType(t, attestation.CosignCustomProvenanceV01, intoto.PredicateType) + case "vuln": + var vulnStatement attestation.CosignVulnStatement + if err := json.Unmarshal(jsonBytes, &vulnStatement); err != nil { + t.Fatal("Wanted vuln statement, can't unmarshal to it: ", err) + } + checkPredicateType(t, attestation.CosignVulnProvenanceV01, vulnStatement.PredicateType) + case "default": + t.Fatal("non supported predicate file") + } + } +} + +func checkPredicateType(t *testing.T, want, got string) { + t.Helper() + if want != got { + t.Errorf("Did not get expected predicateType, want: %s got: %s", want, got) + } +} + +func readAttestationFromTestFile(t *testing.T, dir, name string) []byte { + t.Helper() + b, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s/%s", dir, name)) + if err != nil { + t.Fatalf("Failed to read file : %s ReadFile() = %s", name, err) + } + return b +} + +func getDirFiles(t *testing.T, dir string) []string { + files, err := ioutil.ReadDir(fmt.Sprintf("testdata/%s", dir)) + if err != nil { + t.Fatalf("Failed to read dir : %s ReadFile() = %s", dir, err) + } + ret := []string{} + for _, file := range files { + ret = append(ret, file.Name()) + } + return ret +} diff --git a/pkg/policy/testdata/valid/custom b/pkg/policy/testdata/valid/custom new file mode 100644 index 00000000000..d05cabaa931 --- /dev/null +++ b/pkg/policy/testdata/valid/custom @@ -0,0 +1 @@ +{"payloadType":"application/vnd.in-toto+json","payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJjb3NpZ24uc2lnc3RvcmUuZGV2L2F0dGVzdGF0aW9uL3YxIiwic3ViamVjdCI6W3sibmFtZSI6InJlZ2lzdHJ5LmxvY2FsOjUwMDAva25hdGl2ZS9kZW1vIiwiZGlnZXN0Ijp7InNoYTI1NiI6IjZjNmZkNmE0MTE1YzZlOTk4ZmYzNTdjZDkxNDY4MDkzMWJiOWE2YzFhN2NkNWY1Y2IyZjVlMWMwOTMyYWI2ZWQifX1dLCJwcmVkaWNhdGUiOnsiRGF0YSI6ImZvb2JhciB0ZXN0IGF0dGVzdGF0aW9uIiwiVGltZXN0YW1wIjoiMjAyMi0wNC0wN1QxOToyMjoyNVoifX0=","signatures":[{"keyid":"","sig":"MEUCIQC/slGQVpRKgw4Jo8tcbgo85WNG/FOJfxcvQFvTEnG9swIgP4LeOmID+biUNwLLeylBQpAEgeV6GVcEpyG6r8LVnfY="}]} diff --git a/pkg/policy/testdata/valid/vuln b/pkg/policy/testdata/valid/vuln new file mode 100644 index 00000000000..2a38ba9d81c --- /dev/null +++ b/pkg/policy/testdata/valid/vuln @@ -0,0 +1 @@ +{"payloadType":"application/vnd.in-toto+json","payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJjb3NpZ24uc2lnc3RvcmUuZGV2L2F0dGVzdGF0aW9uL3Z1bG4vdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoicmVnaXN0cnkubG9jYWw6NTAwMC9rbmF0aXZlL2RlbW8iLCJkaWdlc3QiOnsic2hhMjU2IjoiM2MxOWFhOTgwYTljNTcwOWEyYzk2YzJkMDc3OWZlYmY2ZTVlNDUzYjkyYjE3MmNlODRjYjg1ZmRhZjY5NTM3MyJ9fV0sInByZWRpY2F0ZSI6eyJpbnZvY2F0aW9uIjp7InBhcmFtZXRlcnMiOm51bGwsInVyaSI6IiIsImV2ZW50X2lkIjoiIiwiYnVpbGRlci5pZCI6IiJ9LCJzY2FubmVyIjp7InVyaSI6IiIsInZlcnNpb24iOiIiLCJkYiI6eyJ1cmkiOiIiLCJ2ZXJzaW9uIjoiIn0sInJlc3VsdCI6bnVsbH0sIm1ldGFkYXRhIjp7InNjYW5TdGFydGVkT24iOiIwMDAxLTAxLTAxVDAwOjAwOjAwWiIsInNjYW5GaW5pc2hlZE9uIjoiMDAwMS0wMS0wMVQwMDowMDowMFoifX19","signatures":[{"keyid":"","sig":"MEUCIHE9QkUy+d6uFwae0LSH2Fgy99na3jQvaYMU6qj5dzbFAiEA0uKmqGY1ZHoQZsd0BR4Ug0c8d+sHT0hPcxA61o4DKlM="}]} diff --git a/test/testdata/attestations/vuln-predicate.json b/test/testdata/attestations/vuln-predicate.json new file mode 100644 index 00000000000..a6b351cb0ab --- /dev/null +++ b/test/testdata/attestations/vuln-predicate.json @@ -0,0 +1,21 @@ +{ + "invocation": { + "parameters": null, + "uri": "invocation.example.com/cosign-testing", + "event_id": "", + "builder.id": "" + }, + "scanner": { + "uri": "fakescanner.example.com/cosign-testing", + "version": "", + "db": { + "uri": "", + "version": "" + }, + "result": null + }, + "metadata": { + "scanStartedOn": "2022-04-12T00:00:00Z", + "scanFinishedOn": "2022-04-12T00:10:00Z" + } +} diff --git a/test/testdata/policies/cue-vuln-fails.cue b/test/testdata/policies/cue-vuln-fails.cue new file mode 100644 index 00000000000..57d741abe00 --- /dev/null +++ b/test/testdata/policies/cue-vuln-fails.cue @@ -0,0 +1,25 @@ +import "time" + +// This is after our scan happened +before: time.Parse(time.RFC3339, "2022-04-01T17:10:27Z") +after: time.Parse(time.RFC3339, "2022-03-09T17:10:27Z") + +// The predicateType field must match this string +predicateType: "cosign.sigstore.dev/attestation/vuln/v1" + +predicate: { + invocation: { + // This is the wrong invocation uri + uri: "invocation.example.com/cosign-testing-invalid" + } + scanner: { + // This is the wrong scanner uri + uri: "fakescanner.example.com/cosign-testing-invalid" + } + metadata: { + scanStartedOn: after + scanFinishedOn: after + } +} diff --git a/test/testdata/policies/cue-vuln-works.cue b/test/testdata/policies/cue-vuln-works.cue new file mode 100644 index 00000000000..79ea71080d9 --- /dev/null +++ b/test/testdata/policies/cue-vuln-works.cue @@ -0,0 +1,22 @@ +import "time" + +before: time.Parse(time.RFC3339, "2022-04-15T17:10:27Z") +after: time.Parse(time.RFC3339, "2022-03-09T17:10:27Z") + +// The predicateType field must match this string +predicateType: "cosign.sigstore.dev/attestation/vuln/v1" + +predicate: { + invocation: { + uri: "invocation.example.com/cosign-testing" + } + scanner: { + uri: "fakescanner.example.com/cosign-testing" + } + metadata: { + scanStartedOn: after + scanFinishedOn: after + } +} From 80fe5a31a398c45395d09d23e237c676ddad709f Mon Sep 17 00:00:00 2001 From: Hayden B Date: Tue, 12 Apr 2022 18:56:25 -0700 Subject: [PATCH 07/53] Use bundle log ID to find verification key (#1748) Instead of iterating over all log verification keys, we use the log ID in the bundle to find the correct key. This avoids iterating over all trusted keys. Note that the log ID is not signed. If the offline bundle is manipulated so that the log ID does not resolve to a valid key, this will not result in a denial of service attack, because cosign will fall back to fetching the entry from the online log. The equivalent change for CT entry verification has been made in another PR. Signed-off-by: Hayden Blauzvern --- cmd/cosign/cli/verify/verify_blob.go | 21 +++++------ pkg/cosign/tlog.go | 56 ++++++++++++++++++++-------- pkg/cosign/tlog_test.go | 10 +++++ pkg/cosign/verify.go | 21 +++++------ 4 files changed, 68 insertions(+), 40 deletions(-) diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index 8cc570be31f..40dc63c3b3d 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -351,19 +351,16 @@ func verifyRekorBundle(ctx context.Context, bundlePath string, cert *x509.Certif return errors.Wrap(err, "retrieving rekor public key") } - var entryVerError error - for _, pubKey := range publicKeys { - entryVerError = cosign.VerifySET(b.Bundle.Payload, b.Bundle.SignedEntryTimestamp, pubKey.PubKey) - // Exit early with successful verification - if entryVerError == nil { - if pubKey.Status != tuf.Active { - fmt.Fprintf(os.Stderr, "**Info** Successfully verified Rekor entry using an expired verification key\n") - } - break - } + pubKey, ok := publicKeys[b.Bundle.Payload.LogID] + if !ok { + return errors.New("rekor log public key not found for payload") + } + err = cosign.VerifySET(b.Bundle.Payload, b.Bundle.SignedEntryTimestamp, pubKey.PubKey) + if err != nil { + return err } - if entryVerError != nil { - return entryVerError + if pubKey.Status != tuf.Active { + fmt.Fprintf(os.Stderr, "**Info** Successfully verified Rekor entry using an expired verification key\n") } if cert == nil { diff --git a/pkg/cosign/tlog.go b/pkg/cosign/tlog.go index ed0e6e31356..21970b280e8 100644 --- a/pkg/cosign/tlog.go +++ b/pkg/cosign/tlog.go @@ -17,8 +17,10 @@ package cosign import ( "bytes" "context" + "crypto" "crypto/ecdsa" "crypto/sha256" + "crypto/x509" "encoding/base64" "encoding/hex" "fmt" @@ -63,9 +65,19 @@ const ( addRekorPublicKeyFromRekor = "SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY" ) +// getLogID generates a SHA256 hash of a DER-encoded public key. +func getLogID(pub crypto.PublicKey) (string, error) { + pubBytes, err := x509.MarshalPKIXPublicKey(pub) + if err != nil { + return "", err + } + digest := sha256.Sum256(pubBytes) + return hex.EncodeToString(digest[:]), nil +} + // GetRekorPubs retrieves trusted Rekor public keys from the embedded or cached // TUF root. If expired, makes a network call to retrieve the updated targets. -func GetRekorPubs(ctx context.Context) ([]RekorPubKey, error) { +func GetRekorPubs(ctx context.Context) (map[string]RekorPubKey, error) { tufClient, err := tuf.NewFromEnv(ctx) if err != nil { return nil, err @@ -75,7 +87,7 @@ func GetRekorPubs(ctx context.Context) ([]RekorPubKey, error) { if err != nil { return nil, err } - publicKeys := make([]RekorPubKey, 0, len(targets)) + publicKeys := make(map[string]RekorPubKey) altRekorPub := os.Getenv(altRekorPublicKey) if altRekorPub != "" { fmt.Fprintf(os.Stderr, "**Warning** Using a non-standard public key for Rekor: %s\n", altRekorPub) @@ -87,14 +99,22 @@ func GetRekorPubs(ctx context.Context) ([]RekorPubKey, error) { if err != nil { return nil, errors.Wrap(err, "error converting PEM to ECDSAKey") } - publicKeys = append(publicKeys, RekorPubKey{PubKey: extra, Status: tuf.Active}) + keyID, err := getLogID(extra) + if err != nil { + return nil, errors.Wrap(err, "error generating log ID") + } + publicKeys[keyID] = RekorPubKey{PubKey: extra, Status: tuf.Active} } else { for _, t := range targets { rekorPubKey, err := PemToECDSAKey(t.Target) if err != nil { return nil, errors.Wrap(err, "pem to ecdsa") } - publicKeys = append(publicKeys, RekorPubKey{PubKey: rekorPubKey, Status: t.Status}) + keyID, err := getLogID(rekorPubKey) + if err != nil { + return nil, errors.Wrap(err, "error generating log ID") + } + publicKeys[keyID] = RekorPubKey{PubKey: rekorPubKey, Status: t.Status} } } if len(publicKeys) == 0 { @@ -353,19 +373,23 @@ func VerifyTLogEntry(ctx context.Context, rekorClient *client.Rekor, e *models.L if err != nil { return errors.Wrap(err, "error converting rekor PEM public key from rekor to ECDSAKey") } - rekorPubKeys = append(rekorPubKeys, RekorPubKey{PubKey: pubFromAPI, Status: tuf.Active}) + keyID, err := getLogID(pubFromAPI) + if err != nil { + return errors.Wrap(err, "error generating log ID") + } + rekorPubKeys[keyID] = RekorPubKey{PubKey: pubFromAPI, Status: tuf.Active} } - var entryVerError error - for _, pubKey := range rekorPubKeys { - entryVerError = VerifySET(payload, []byte(e.Verification.SignedEntryTimestamp), pubKey.PubKey) - // Return once the SET is verified successfully. - if entryVerError == nil { - if pubKey.Status != tuf.Active { - fmt.Fprintf(os.Stderr, "**Info** Successfully verified Rekor entry using an expired verification key\n") - } - return nil - } + pubKey, ok := rekorPubKeys[payload.LogID] + if !ok { + return errors.New("rekor log public key not found for payload") } - return errors.Wrap(entryVerError, "verifying signedEntryTimestamp") + err = VerifySET(payload, []byte(e.Verification.SignedEntryTimestamp), pubKey.PubKey) + if err != nil { + return errors.Wrap(err, "verifying signedEntryTimestamp") + } + if pubKey.Status != tuf.Active { + fmt.Fprintf(os.Stderr, "**Info** Successfully verified Rekor entry using an expired verification key\n") + } + return nil } diff --git a/pkg/cosign/tlog_test.go b/pkg/cosign/tlog_test.go index 9b773271f67..03b54709796 100644 --- a/pkg/cosign/tlog_test.go +++ b/pkg/cosign/tlog_test.go @@ -27,4 +27,14 @@ func TestGetRekorPubKeys(t *testing.T) { if len(keys) == 0 { t.Errorf("expected 1 or more keys, got 0") } + // check that the mapping of key digest to key is correct + for logID, key := range keys { + expectedLogID, err := getLogID(key.PubKey) + if err != nil { + t.Fatalf("unexpected error generated log ID: %v", err) + } + if logID != expectedLogID { + t.Fatalf("key digests are not equal") + } + } } diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index 10d7aedd119..356aeed0ecf 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -645,19 +645,16 @@ func VerifyBundle(ctx context.Context, sig oci.Signature) (bool, error) { return false, errors.Wrap(err, "retrieving rekor public key") } - var entryVerError error - for _, pubKey := range publicKeys { - entryVerError = VerifySET(bundle.Payload, bundle.SignedEntryTimestamp, pubKey.PubKey) - // Exit early with successful verification - if entryVerError == nil { - if pubKey.Status != tuf.Active { - fmt.Fprintf(os.Stderr, "**Info** Successfully verified Rekor entry using an expired verification key\n") - } - break - } + pubKey, ok := publicKeys[bundle.Payload.LogID] + if !ok { + return false, errors.New("rekor log public key not found for payload") + } + err = VerifySET(bundle.Payload, bundle.SignedEntryTimestamp, pubKey.PubKey) + if err != nil { + return false, err } - if entryVerError != nil { - return false, entryVerError + if pubKey.Status != tuf.Active { + fmt.Fprintf(os.Stderr, "**Info** Successfully verified Rekor entry using an expired verification key\n") } cert, err := sig.Cert() From 36afb67a1343d7c9f5ade8a118d01545f76ba31e Mon Sep 17 00:00:00 2001 From: Vladimir Nachev Date: Wed, 13 Apr 2022 16:27:00 +0300 Subject: [PATCH 08/53] [cosigned] The webhook name is now configurable via --webhook-name flag (#1726) * [cosigned] The webhook name is now configurable via --webhook-name flag Signed-off-by: Vladimir Nachev * [cosigned] Update doc string for webhookName Signed-off-by: Vladimir Nachev --- cmd/cosign/webhook/main.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/cmd/cosign/webhook/main.go b/cmd/cosign/webhook/main.go index 5c99513f5ee..e75f8d75de0 100644 --- a/cmd/cosign/webhook/main.go +++ b/cmd/cosign/webhook/main.go @@ -44,10 +44,15 @@ import ( var secretName = flag.String("secret-name", "", "The name of the secret in the webhook's namespace that holds the public key for verification.") -// webhookName holds the name of the validating webhook to set up with the -// types we are watching. If this changes, you must also change: +// webhookName holds the name of the validating and mutating webhook +// configuration resources dispatching admission requests to cosigned. +// It is also the name of the webhook which is injected by the controller +// with the resource types, namespace selectors, CABindle and service path. +// If this changes, you must also change: // ./config/500-webhook-configuration.yaml -const webhookName = "cosigned.sigstore.dev" +// https://github.com/sigstore/helm-charts/blob/main/charts/cosigned/templates/webhook/webhook_mutating.yaml +// https://github.com/sigstore/helm-charts/blob/main/charts/cosigned/templates/webhook/webhook_validating.yaml +var webhookName = flag.String("webhook-name", "cosigned.sigstore.dev", "The name of the validating and mutating webhook configurations as well as the webhook name that is automatically configured, if exists, with different rules and client settings setting how the admission requests to be dispatched to cosigned.") func main() { opts := webhook.Options{ @@ -92,7 +97,7 @@ func NewValidatingAdmissionController(ctx context.Context, cmw configmap.Watcher return validation.NewAdmissionController(ctx, // Name of the resource webhook. - webhookName, + *webhookName, // The path on which to serve the webhook. "/validations", @@ -123,7 +128,7 @@ func NewMutatingAdmissionController(ctx context.Context, cmw configmap.Watcher) return defaulting.NewAdmissionController(ctx, // Name of the resource webhook. - webhookName, + *webhookName, // The path on which to serve the webhook. "/mutations", From 3047220e49dcddfcff5aa9c507e584bf53e1461b Mon Sep 17 00:00:00 2001 From: Hayden B Date: Wed, 13 Apr 2022 08:08:52 -0700 Subject: [PATCH 09/53] Add intermediate CA certificate pool for Fulcio (#1749) This separates roots and intermediates from the TUF targets. This will be used to configure the default intermediate certificates when none are found. In particular, this will be used by verify-blob when fetching an entry from Rekor. An intermediate CA certificate will be added to the v3 TUF root. Signed-off-by: Hayden Blauzvern --- cmd/cosign/cli/fulcio/fulcio.go | 4 ++ .../cli/fulcio/fulcioroots/fulcioroots.go | 62 ++++++++++++++----- .../fulcio/fulcioroots/fulcioroots_test.go | 24 +++++-- cmd/cosign/cli/verify/verify_blob.go | 7 ++- 4 files changed, 74 insertions(+), 23 deletions(-) diff --git a/cmd/cosign/cli/fulcio/fulcio.go b/cmd/cosign/cli/fulcio/fulcio.go index 338cdd4d1ed..d7eedabafb8 100644 --- a/cmd/cosign/cli/fulcio/fulcio.go +++ b/cmd/cosign/cli/fulcio/fulcio.go @@ -157,6 +157,10 @@ func GetRoots() *x509.CertPool { return fulcioroots.Get() } +func GetIntermediates() *x509.CertPool { + return fulcioroots.GetIntermediates() +} + func NewClient(fulcioURL string) (api.Client, error) { fulcioServer, err := url.Parse(fulcioURL) if err != nil { diff --git a/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots.go b/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots.go index 0485721b379..9b48f2b2d74 100644 --- a/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots.go +++ b/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots.go @@ -16,6 +16,7 @@ package fulcioroots import ( + "bytes" "context" "crypto/x509" "os" @@ -23,11 +24,13 @@ import ( "github.com/pkg/errors" "github.com/sigstore/cosign/pkg/cosign/tuf" + "github.com/sigstore/sigstore/pkg/cryptoutils" ) var ( - rootsOnce sync.Once - roots *x509.CertPool + rootsOnce sync.Once + roots *x509.CertPool + intermediates *x509.CertPool ) // This is the root in the fulcio project. @@ -43,7 +46,7 @@ const ( func Get() *x509.CertPool { rootsOnce.Do(func() { var err error - roots, err = initRoots() + roots, intermediates, err = initRoots() if err != nil { panic(err) } @@ -51,37 +54,68 @@ func Get() *x509.CertPool { return roots } -func initRoots() (*x509.CertPool, error) { - cp := x509.NewCertPool() +func GetIntermediates() *x509.CertPool { + rootsOnce.Do(func() { + var err error + roots, intermediates, err = initRoots() + if err != nil { + panic(err) + } + }) + return intermediates +} + +func initRoots() (*x509.CertPool, *x509.CertPool, error) { + rootPool := x509.NewCertPool() + intermediatePool := x509.NewCertPool() + rootEnv := os.Getenv(altRoot) if rootEnv != "" { raw, err := os.ReadFile(rootEnv) if err != nil { - return nil, errors.Wrap(err, "error reading root PEM file") + return nil, nil, errors.Wrap(err, "error reading root PEM file") + } + certs, err := cryptoutils.UnmarshalCertificatesFromPEM(raw) + if err != nil { + return nil, nil, errors.Wrap(err, "error unmarshalling certificates") } - if !cp.AppendCertsFromPEM(raw) { - return nil, errors.New("error creating root cert pool") + for _, cert := range certs { + // root certificates are self-signed + if bytes.Equal(cert.RawSubject, cert.RawIssuer) { + rootPool.AddCert(cert) + } else { + intermediatePool.AddCert(cert) + } } } else { tufClient, err := tuf.NewFromEnv(context.Background()) if err != nil { - return nil, errors.Wrap(err, "initializing tuf") + return nil, nil, errors.Wrap(err, "initializing tuf") } defer tufClient.Close() // Retrieve from the embedded or cached TUF root. If expired, a network // call is made to update the root. targets, err := tufClient.GetTargetsByMeta(tuf.Fulcio, []string{fulcioTargetStr, fulcioV1TargetStr}) if err != nil { - return nil, errors.New("error getting targets") + return nil, nil, errors.New("error getting targets") } if len(targets) == 0 { - return nil, errors.New("none of the Fulcio roots have been found") + return nil, nil, errors.New("none of the Fulcio roots have been found") } for _, t := range targets { - if !cp.AppendCertsFromPEM(t.Target) { - return nil, errors.New("error creating root cert pool") + certs, err := cryptoutils.UnmarshalCertificatesFromPEM(t.Target) + if err != nil { + return nil, nil, errors.Wrap(err, "error unmarshalling certificates") + } + for _, cert := range certs { + // root certificates are self-signed + if bytes.Equal(cert.RawSubject, cert.RawIssuer) { + rootPool.AddCert(cert) + } else { + intermediatePool.AddCert(cert) + } } } } - return cp, nil + return rootPool, intermediatePool, nil } diff --git a/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots_test.go b/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots_test.go index fba782e2d45..9071db8cd4a 100644 --- a/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots_test.go +++ b/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots_test.go @@ -23,23 +23,35 @@ import ( ) func TestGetFulcioRoots(t *testing.T) { - rootCert, _, _ := test.GenerateRootCa() - pemCert, _ := cryptoutils.MarshalCertificateToPEM(rootCert) + rootCert, rootPriv, _ := test.GenerateRootCa() + rootPemCert, _ := cryptoutils.MarshalCertificateToPEM(rootCert) + subCert, _, _ := test.GenerateSubordinateCa(rootCert, rootPriv) + subPemCert, _ := cryptoutils.MarshalCertificateToPEM(subCert) + + var chain []byte + chain = append(chain, subPemCert...) + chain = append(chain, rootPemCert...) tmpCertFile, err := os.CreateTemp(t.TempDir(), "cosign_fulcio_root_*.cert") if err != nil { t.Fatalf("failed to create temp cert file: %v", err) } defer tmpCertFile.Close() - if _, err := tmpCertFile.Write(pemCert); err != nil { + if _, err := tmpCertFile.Write(chain); err != nil { t.Fatalf("failed to write cert file: %v", err) } os.Setenv("SIGSTORE_ROOT_FILE", tmpCertFile.Name()) defer os.Unsetenv("SIGSTORE_ROOT_FILE") - certPool := Get() + rootCertPool := Get() + // ignore deprecation error because certificates do not contain from SystemCertPool + if len(rootCertPool.Subjects()) != 1 { // nolint:staticcheck + t.Errorf("expected 1 root certificate, got 0") + } + + subCertPool := GetIntermediates() // ignore deprecation error because certificates do not contain from SystemCertPool - if len(certPool.Subjects()) == 0 { // nolint:staticcheck - t.Errorf("expected 1 or more certificates, got 0") + if len(subCertPool.Subjects()) != 1 { // nolint:staticcheck + t.Errorf("expected 1 intermediate certificate, got 0") } } diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index 40dc63c3b3d..d0741f13044 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -198,9 +198,10 @@ func verifySigByUUID(ctx context.Context, ko sign.KeyOpts, rClient *client.Rekor } co := &cosign.CheckOpts{ - RootCerts: fulcio.GetRoots(), - CertEmail: certEmail, - CertOidcIssuer: certOidcIssuer, + RootCerts: fulcio.GetRoots(), + IntermediateCerts: fulcio.GetIntermediates(), + CertEmail: certEmail, + CertOidcIssuer: certOidcIssuer, } cert := certs[0] verifier, err := cosign.ValidateAndUnpackCert(cert, co) From ea74133f49a19e719894a9a08dc72c3723dba0df Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Apr 2022 11:34:23 -0500 Subject: [PATCH 10/53] Bump github.com/spf13/viper from 1.10.1 to 1.11.0 (#1751) Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.10.1 to 1.11.0. - [Release notes](https://github.com/spf13/viper/releases) - [Commits](https://github.com/spf13/viper/compare/v1.10.1...v1.11.0) --- updated-dependencies: - dependency-name: github.com/spf13/viper dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 29 +++++++++++++++-------------- go.sum | 47 ++++++++++++++++++++++++++++++++--------------- 2 files changed, 47 insertions(+), 29 deletions(-) diff --git a/go.mod b/go.mod index 54817b28b04..9d72519daf1 100644 --- a/go.mod +++ b/go.mod @@ -40,15 +40,15 @@ require ( github.com/sigstore/rekor v0.4.1-0.20220114213500-23f583409af3 github.com/sigstore/sigstore v1.2.1-0.20220401110139-0e610e39782f github.com/spf13/cobra v1.4.0 - github.com/spf13/viper v1.10.1 + github.com/spf13/viper v1.11.0 github.com/spiffe/go-spiffe/v2 v2.0.0 github.com/stretchr/testify v1.7.1 github.com/theupdateframework/go-tuf v0.0.0-20220211205608-f0c3294f63b9 github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 github.com/xanzy/go-gitlab v0.63.0 - golang.org/x/net v0.0.0-20220325170049-de3da57026de - golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a - golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886 + golang.org/x/net v0.0.0-20220412020605-290c469a71a5 + golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 + golang.org/x/sys v0.0.0-20220412211240-33da011f77ad golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 google.golang.org/api v0.74.0 @@ -89,14 +89,14 @@ require ( github.com/mitchellh/mapstructure v1.4.3 github.com/pierrec/lz4 v2.6.1+incompatible github.com/prometheus/procfs v0.7.3 // indirect - github.com/spf13/afero v1.8.0 // indirect + github.com/spf13/afero v1.8.2 // indirect github.com/urfave/cli v1.22.5 // indirect github.com/withfig/autocomplete-tools/packages/cobra v0.0.0-20220122124547-31d3821a6898 go.opentelemetry.io/contrib v1.3.0 // indirect go.opentelemetry.io/proto/otlp v0.12.0 // indirect go.uber.org/atomic v1.9.0 go.uber.org/zap v1.21.0 - golang.org/x/crypto v0.0.0-20220214200702-86341886e292 + golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 google.golang.org/grpc v1.45.0 google.golang.org/protobuf v1.28.0 k8s.io/code-generator v0.23.5 @@ -207,7 +207,7 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.3.0 // indirect - github.com/googleapis/gax-go/v2 v2.2.0 // indirect + github.com/googleapis/gax-go/v2 v2.3.0 // indirect github.com/googleapis/gnostic v0.5.5 // indirect github.com/googleapis/go-type-adapters v1.0.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect @@ -225,7 +225,7 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.14.2 // indirect github.com/leodido/go-urn v1.2.1 // indirect - github.com/magiconair/properties v1.8.5 // indirect + github.com/magiconair/properties v1.8.6 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect @@ -241,6 +241,7 @@ require ( github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pelletier/go-toml v1.9.4 // indirect + github.com/pelletier/go-toml/v2 v2.0.0-beta.8 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.12.1 // indirect github.com/prometheus/client_model v0.2.0 // indirect @@ -271,9 +272,9 @@ require ( github.com/yashtewari/glob-intersection v0.0.0-20180916065949-5c77d914dd0b // indirect github.com/zeebo/errs v1.2.2 // indirect go.etcd.io/bbolt v1.3.6 // indirect - go.etcd.io/etcd/api/v3 v3.5.1 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.5.1 // indirect - go.etcd.io/etcd/client/v2 v2.305.1 // indirect + go.etcd.io/etcd/api/v3 v3.5.2 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.2 // indirect + go.etcd.io/etcd/client/v2 v2.305.2 // indirect go.etcd.io/etcd/client/v3 v3.5.0 // indirect go.etcd.io/etcd/etcdctl/v3 v3.5.0 // indirect go.etcd.io/etcd/etcdutl/v3 v3.5.0 // indirect @@ -298,13 +299,13 @@ require ( golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/tools v0.1.10 // indirect - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf // indirect + google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac // indirect gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/ini.v1 v1.66.2 // indirect + gopkg.in/ini.v1 v1.66.4 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect k8s.io/apiextensions-apiserver v0.23.4 // indirect diff --git a/go.sum b/go.sum index eac01859be7..209d61d5652 100644 --- a/go.sum +++ b/go.sum @@ -1224,8 +1224,9 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= -github.com/googleapis/gax-go/v2 v2.2.0 h1:s7jOdKSaksJVOxE0Y/S32otcfiP+UQ0cL8/GTKaONwE= github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0 h1:nRJtk3y8Fm770D42QV6T90ZnvFZyk7agSo3Q+Z9p3WI= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= @@ -1373,6 +1374,7 @@ github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hashicorp/vault/api v1.3.0/go.mod h1:EabNQLI0VWbWoGlA+oBLC8PXmR9D60aUVgQGvangFWQ= github.com/hashicorp/vault/api v1.3.1/go.mod h1:QeJoWxMFt+MsuWcYhmwRLwKEXrjwAFFywzhptMsTIUw= github.com/hashicorp/vault/api v1.5.0 h1:Bp6yc2bn7CWkOrVIzFT/Qurzx528bdavF3nz590eu28= @@ -1582,8 +1584,9 @@ github.com/magefile/mage v1.11.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXq github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -1848,6 +1851,8 @@ github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrap github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.0.0-beta.8 h1:dy81yyLYJDwMTifq24Oi/IslOslRrDSb3jwDggjz3Z0= +github.com/pelletier/go-toml/v2 v2.0.0-beta.8/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/peterh/liner v0.0.0-20170211195444-bf27d3ba8e1d/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= @@ -1990,6 +1995,7 @@ github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiB github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYIR88KRMEuODE= github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= github.com/sagikazarmark/crypt v0.4.0/go.mod h1:ALv2SRj7GxYV4HO9elxH9nS6M9gW+xDNxqmyJ6RfDFM= +github.com/sagikazarmark/crypt v0.5.0/go.mod h1:l+nzl7KWh51rpzp2h7t4MZWyiEWdhNpOAnclKvg+mdA= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/sanposhiho/wastedassign/v2 v2.0.6/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI= github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:am+Fp8Bt506lA3Rk3QCmSqmYmLMnPDhdDUcosQCAx+I= @@ -2064,8 +2070,8 @@ github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTd github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.4.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/afero v1.8.0 h1:5MmtuhAgYeU6qpa7w7bP0dv6MBYuup0vekhSpSkoq60= -github.com/spf13/afero v1.8.0/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= +github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= +github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= @@ -2097,8 +2103,9 @@ github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5q github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/spf13/viper v1.9.0/go.mod h1:+i6ajR7OX2XaiBkrcZJFK21htRk7eDeLg7+O6bhUPP4= github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= -github.com/spf13/viper v1.10.1 h1:nuJZuYpG7gTj/XqiUwg8bA0cp1+M2mC3J4g5luUYBKk= github.com/spf13/viper v1.10.1/go.mod h1:IGlFPqhNAPKRxohIzWpI5QEy4kuI7tcl5WvR+8qy1rU= +github.com/spf13/viper v1.11.0 h1:7OX/1FS6n7jHD1zGrZTM7WtY13ZELRyosK4k93oPr44= +github.com/spf13/viper v1.11.0/go.mod h1:djo0X/bA5+tYVoCn+C7cAYJGcVn/qYLFTG8gdUsX7Zk= github.com/spiffe/go-spiffe/v2 v2.0.0 h1:y6N7BZAxgaFZYELyrIdxSMm2e2tWpzgQewUts9h1hfM= github.com/spiffe/go-spiffe/v2 v2.0.0/go.mod h1:TEfgrEcyFhuSuvqohJt6IxENUNeHfndWCCV1EX7UaVk= github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= @@ -2291,15 +2298,18 @@ go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489 h1:1JFLBqwIgdyHN1Zt go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= go.etcd.io/etcd/api/v3 v3.5.0-alpha.0/go.mod h1:mPcW6aZJukV6Aa81LSKpBjQXTWlXB5r74ymPoSWa3Sw= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/api/v3 v3.5.1 h1:v28cktvBq+7vGyJXF8G+rWJmj+1XUmMtqcLnH8hDocM= go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/api/v3 v3.5.2 h1:tXok5yLlKyuQ/SXSjtqHc4uzNaMqZi2XsoSPr/LlJXI= +go.etcd.io/etcd/api/v3 v3.5.2/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/pkg/v3 v3.5.1 h1:XIQcHCFSG53bJETYeRJtIxdLv2EWRGxcfzR8lSnTH4E= go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/pkg/v3 v3.5.2 h1:4hzqQ6hIb3blLyQ8usCU4h3NghkqcsohEQ3o3VetYxE= +go.etcd.io/etcd/client/pkg/v3 v3.5.2/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0-alpha.0/go.mod h1:kdV+xzCJ3luEBSIeQyB/OEKkWKd8Zkux4sbDeANrosU= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.etcd.io/etcd/client/v2 v2.305.1 h1:vtxYCKWA9x31w0WJj7DdqsHFNjhkigdAnziDtkZb/l4= go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= +go.etcd.io/etcd/client/v2 v2.305.2 h1:ymrVwTkefuqA/rPkSW7/B4ApijbPVefRumkY+stNfS0= +go.etcd.io/etcd/client/v2 v2.305.2/go.mod h1:2D7ZejHVMIfog1221iLSYlQRzrtECw3kz4I4VAQm3qI= go.etcd.io/etcd/client/v3 v3.5.0-alpha.0/go.mod h1:wKt7jgDgf/OfKiYmCq5WFGxOFAkVMLxiiXgLDFhECr8= go.etcd.io/etcd/client/v3 v3.5.0 h1:62Eh0XOro+rDwkrypAGDfgmNh5Joq+z+W9HZdlXMzek= go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= @@ -2470,8 +2480,9 @@ golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -2605,8 +2616,9 @@ golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220127074510-2fabfed7e28f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220325170049-de3da57026de h1:pZB1TWnKi+o4bENlbzAgLrEbY4RMYmUIRobMcSmfeYc= golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5 h1:bRb386wvrE+oBNdF1d/Xh9mQrfQ4ecYhW5qJ5GvTGT4= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -2631,8 +2643,9 @@ golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a h1:qfl7ob3DIEs3Ml9oLuPwY2N04gymzAW04WsUQHIClgM= golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -2794,8 +2807,9 @@ golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886 h1:eJv7u3ksNXoLbGSKuv2s/SIO4tJVxc/A+MTpzxDgz/Q= golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -2959,8 +2973,9 @@ golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= @@ -3133,8 +3148,9 @@ google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2 google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= -google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf h1:JTjwKJX9erVpsw17w+OIPP7iAgEkN/r8urhWSunEDTs= google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac h1:qSNTkEN+L2mvWcLgJOR+8bdHX9rN/IdU3A1Ghpfb1Rg= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -3221,8 +3237,9 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.66.2 h1:XfR1dOYubytKy4Shzc2LHrrGhU0lDCfDGG1yLPmpgsI= gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= +gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= From 650dc80a062639f1735022495873e7d8d694f20c Mon Sep 17 00:00:00 2001 From: asraa Date: Wed, 13 Apr 2022 16:28:23 -0500 Subject: [PATCH 11/53] test: create fake TUF test root and create test SETs for verification (#1750) * wip Signed-off-by: Asra Ali add fake SET test Signed-off-by: Asra Ali fix Signed-off-by: Asra Ali fix test Signed-off-by: Asra Ali fix Signed-off-by: Asra Ali * address haydentherapper comments Signed-off-by: Asra Ali --- pkg/cosign/tuf/testutils.go | 126 ++++++++++++++++++++++++++++++++++++ pkg/cosign/verify_test.go | 70 ++++++++++++++++++-- 2 files changed, 192 insertions(+), 4 deletions(-) create mode 100644 pkg/cosign/tuf/testutils.go diff --git a/pkg/cosign/tuf/testutils.go b/pkg/cosign/tuf/testutils.go new file mode 100644 index 00000000000..729af872db6 --- /dev/null +++ b/pkg/cosign/tuf/testutils.go @@ -0,0 +1,126 @@ +// +// 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 tuf + +import ( + "context" + "crypto/x509" + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/sigstore/sigstore/pkg/signature" + "github.com/sigstore/sigstore/pkg/signature/options" + "github.com/theupdateframework/go-tuf" +) + +type TestSigstoreRoot struct { + Rekor signature.Verifier + FulcioCertificate *x509.Certificate + // TODO: Include a CTFE key if/when cosign verifies SCT. +} + +// This creates a new sigstore TUF repo whose signers can be used to create dynamic +// signed Rekor entries. +func NewSigstoreTufRepo(t *testing.T, root TestSigstoreRoot) (tuf.LocalStore, *tuf.Repo) { + td := t.TempDir() + ctx := context.Background() + remote := tuf.FileSystemStore(td, nil) + r, err := tuf.NewRepo(remote) + if err != nil { + t.Error(err) + } + if err := r.Init(false); err != nil { + t.Error(err) + } + + for _, role := range []string{"root", "targets", "snapshot", "timestamp"} { + if _, err := r.GenKey(role); err != nil { + t.Error(err) + } + } + targetsPath := filepath.Join(td, "staged", "targets") + if err := os.MkdirAll(filepath.Dir(targetsPath), 0755); err != nil { + t.Error(err) + } + // Add the rekor key target + pk, err := root.Rekor.PublicKey(options.WithContext(ctx)) + if err != nil { + t.Error(err) + } + b, err := x509.MarshalPKIXPublicKey(pk) + if err != nil { + t.Error(err) + } + rekorPath := "rekor.pub" + rekorData := cryptoutils.PEMEncode(cryptoutils.PublicKeyPEMType, b) + if err := ioutil.WriteFile(filepath.Join(targetsPath, rekorPath), rekorData, 0600); err != nil { + t.Error(err) + } + scmRekor, err := json.Marshal(&sigstoreCustomMetadata{Sigstore: customMetadata{Usage: Rekor, Status: Active}}) + if err != nil { + t.Error(err) + } + if err := r.AddTarget("rekor.pub", scmRekor); err != nil { + t.Error(err) + } + // Add Fulcio Certificate information. + fulcioPath := "fulcio.crt.pem" + fulcioData := cryptoutils.PEMEncode(cryptoutils.CertificatePEMType, root.FulcioCertificate.Raw) + if err := ioutil.WriteFile(filepath.Join(targetsPath, fulcioPath), fulcioData, 0600); err != nil { + t.Error(err) + } + scmFulcio, err := json.Marshal(&sigstoreCustomMetadata{Sigstore: customMetadata{Usage: Fulcio, Status: Active}}) + if err != nil { + t.Error(err) + } + if err := r.AddTarget(fulcioPath, scmFulcio); err != nil { + t.Error(err) + } + if err := r.Snapshot(); err != nil { + t.Error(err) + } + if err := r.Timestamp(); err != nil { + t.Error(err) + } + if err := r.Commit(); err != nil { + t.Error(err) + } + // Serve remote repository. + s := httptest.NewServer(http.FileServer(http.Dir(filepath.Join(td, "repository")))) + defer s.Close() + + // Initialize with custom root. + tufRoot := t.TempDir() + t.Setenv("TUF_ROOT", tufRoot) + meta, err := remote.GetMeta() + if err != nil { + t.Error(err) + } + rootBytes, ok := meta["root.json"] + if !ok { + t.Error(err) + } + if err := Initialize(ctx, s.URL, rootBytes); err != nil { + t.Error(err) + } + return remote, r +} diff --git a/pkg/cosign/verify_test.go b/pkg/cosign/verify_test.go index 0ff80e7def6..bba5dfd06a9 100644 --- a/pkg/cosign/verify_test.go +++ b/pkg/cosign/verify_test.go @@ -15,8 +15,10 @@ package cosign import ( + "bytes" "context" "crypto" + "crypto/elliptic" "crypto/rand" "crypto/sha256" "crypto/x509" @@ -26,15 +28,22 @@ import ( "io" "strings" "testing" + "time" + "github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer" + "github.com/go-openapi/strfmt" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/in-toto/in-toto-golang/in_toto" "github.com/pkg/errors" "github.com/secure-systems-lab/go-securesystemslib/dsse" + "github.com/sigstore/cosign/pkg/cosign/bundle" + ctuf "github.com/sigstore/cosign/pkg/cosign/tuf" "github.com/sigstore/cosign/pkg/oci/static" "github.com/sigstore/cosign/pkg/types" "github.com/sigstore/cosign/test" + rtypes "github.com/sigstore/rekor/pkg/types" "github.com/sigstore/sigstore/pkg/signature" + "github.com/sigstore/sigstore/pkg/signature/options" "github.com/stretchr/testify/require" ) @@ -167,8 +176,54 @@ func TestVerifyImageSignatureMultipleSubs(t *testing.T) { } } +func signEntry(ctx context.Context, t *testing.T, signer signature.Signer, entry bundle.RekorPayload) []byte { + payload, err := json.Marshal(entry) + if err != nil { + t.Fatalf("marshalling error: %v", err) + } + canonicalized, err := jsoncanonicalizer.Transform(payload) + if err != nil { + t.Fatalf("canonicalizing error: %v", err) + } + signature, err := signer.SignMessage(bytes.NewReader(canonicalized), options.WithContext(ctx)) + if err != nil { + t.Fatalf("signing error: %v", err) + } + return signature +} + +func CreateTestBundle(ctx context.Context, t *testing.T, rekor signature.Signer, leaf []byte) *bundle.RekorBundle { + // generate log ID according to rekor public key + pk, _ := rekor.PublicKey(nil) + keyID, _ := getLogID(pk) + pyld := bundle.RekorPayload{ + Body: base64.StdEncoding.EncodeToString(leaf), + IntegratedTime: time.Now().Unix(), + LogIndex: 693591, + LogID: keyID, + } + // Sign with root. + signature := signEntry(ctx, t, rekor, pyld) + b := &bundle.RekorBundle{ + SignedEntryTimestamp: strfmt.Base64(signature), + Payload: pyld, + } + return b +} + func TestVerifyImageSignatureWithNoChain(t *testing.T) { + ctx := context.Background() rootCert, rootKey, _ := test.GenerateRootCa() + sv, _, err := signature.NewECDSASignerVerifier(elliptic.P256(), rand.Reader, crypto.SHA256) + if err != nil { + t.Fatalf("creating signer: %v", err) + } + testSigstoreRoot := ctuf.TestSigstoreRoot{ + Rekor: sv, + FulcioCertificate: rootCert, + } + _, _ = ctuf.NewSigstoreTufRepo(t, testSigstoreRoot) + leafCert, privKey, _ := test.GenerateLeafCert("subject", "oidc-issuer", rootCert, rootKey) pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) @@ -179,14 +234,21 @@ func TestVerifyImageSignatureWithNoChain(t *testing.T) { h := sha256.Sum256(payload) signature, _ := privKey.Sign(rand.Reader, h[:], crypto.SHA256) - ociSig, _ := static.NewSignature(payload, base64.StdEncoding.EncodeToString(signature), static.WithCertChain(pemLeaf, []byte{})) + // Create a fake bundle + pe, _ := proposedEntry(base64.StdEncoding.EncodeToString(signature), payload, pemLeaf) + entry, _ := rtypes.NewEntry(pe[0]) + leaf, _ := entry.Canonicalize(ctx) + rekorBundle := CreateTestBundle(ctx, t, sv, leaf) + + opts := []static.Option{static.WithCertChain(pemLeaf, []byte{}), static.WithBundle(rekorBundle)} + ociSig, _ := static.NewSignature(payload, base64.StdEncoding.EncodeToString(signature), opts...) + verified, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, &CheckOpts{RootCerts: rootPool}) if err != nil { t.Fatalf("unexpected error while verifying signature, expected no error, got %v", err) } - // TODO: Create fake bundle and test verification - if verified == true { - t.Fatalf("expected verified=false, got verified=true") + if verified == false { + t.Fatalf("expected verified=true, got verified=false") } } From 380b68a0ce263c460e10c84267c71342ba92655e Mon Sep 17 00:00:00 2001 From: Carlos Tadeu Panato Junior Date: Thu, 14 Apr 2022 14:08:10 +0200 Subject: [PATCH 12/53] update go builder and cosign images (#1755) Signed-off-by: cpanato --- .github/workflows/validate-release.yml | 4 ++-- release/cloudbuild.yaml | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/validate-release.yml b/.github/workflows/validate-release.yml index e5077a03f42..93f8a93507b 100644 --- a/.github/workflows/validate-release.yml +++ b/.github/workflows/validate-release.yml @@ -39,8 +39,8 @@ jobs: statuses: none env: - CROSS_BUILDER_IMAGE: ghcr.io/gythialy/golang-cross:v1.17.8-1@sha256:38effe76e69a728f6c2e76b290c0d5e09fdff439926e3bbe7e69978c84c185f3 - COSIGN_IMAGE: gcr.io/projectsigstore/cosign:v1.6.0@sha256:b667002156c4bf9fedd9273f689b800bb5c341660e710e3bbac981c9795423d9 + CROSS_BUILDER_IMAGE: ghcr.io/gythialy/golang-cross:v1.17.9-0@sha256:62c64ee6c74285839db86ae0814d2411bfe4bc2cdc025b10122e4bb8d27b1418 + COSIGN_IMAGE: gcr.io/projectsigstore/cosign:v1.7.2@sha256:ad2985a87622d5934a4bc06a61faadff772e377937e42519af4f506e1b019d1e steps: - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 #v2.4.0 diff --git a/release/cloudbuild.yaml b/release/cloudbuild.yaml index d303345dcab..bbd5bad0332 100644 --- a/release/cloudbuild.yaml +++ b/release/cloudbuild.yaml @@ -32,17 +32,17 @@ steps: echo "Checking out ${_GIT_TAG}" git checkout ${_GIT_TAG} -- name: 'gcr.io/projectsigstore/cosign:v1.7.1@sha256:7d735456ae0c6489d088981a228b944e8a729c2aa979d824a74e44ab843d6ad2' +- name: 'gcr.io/projectsigstore/cosign:v1.7.2@sha256:ad2985a87622d5934a4bc06a61faadff772e377937e42519af4f506e1b019d1e' dir: "go/src/sigstore/cosign" env: - COSIGN_EXPERIMENTAL=true - TUF_ROOT=/tmp args: - 'verify' - - 'ghcr.io/gythialy/golang-cross:v1.17.8-1@sha256:38effe76e69a728f6c2e76b290c0d5e09fdff439926e3bbe7e69978c84c185f3' + - 'ghcr.io/gythialy/golang-cross:v1.17.9-0@sha256:62c64ee6c74285839db86ae0814d2411bfe4bc2cdc025b10122e4bb8d27b1418' # maybe we can build our own image and use that to be more in a safe side -- name: ghcr.io/gythialy/golang-cross:v1.17.8-1@sha256:38effe76e69a728f6c2e76b290c0d5e09fdff439926e3bbe7e69978c84c185f3 +- name: ghcr.io/gythialy/golang-cross:v1.17.9-0@sha256:62c64ee6c74285839db86ae0814d2411bfe4bc2cdc025b10122e4bb8d27b1418 entrypoint: /bin/sh dir: "go/src/sigstore/cosign" env: @@ -65,7 +65,7 @@ steps: gcloud auth configure-docker \ && make release -- name: ghcr.io/gythialy/golang-cross:v1.17.8-1@sha256:38effe76e69a728f6c2e76b290c0d5e09fdff439926e3bbe7e69978c84c185f3 +- name: ghcr.io/gythialy/golang-cross:v1.17.9-0@sha256:62c64ee6c74285839db86ae0814d2411bfe4bc2cdc025b10122e4bb8d27b1418 entrypoint: 'bash' dir: "go/src/sigstore/cosign" env: From 72060a14d547197d872cb2737989ad8046b1f581 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Apr 2022 10:00:09 -0500 Subject: [PATCH 13/53] Bump sigstore/cosign-installer from 2.2.0 to 2.2.1 (#1752) Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 2.2.0 to 2.2.1. - [Release notes](https://github.com/sigstore/cosign-installer/releases) - [Commits](https://github.com/sigstore/cosign-installer/compare/d6a3abf1bdea83574e28d40543793018b6035605...bb61838e7ee5bf314f85f2e219b3706835fa6306) --- updated-dependencies: - dependency-name: sigstore/cosign-installer dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yaml | 2 +- .github/workflows/github-oidc.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 38c6a62739d..4a617c200cd 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -35,7 +35,7 @@ jobs: steps: - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 - - uses: sigstore/cosign-installer@d6a3abf1bdea83574e28d40543793018b6035605 # v2.0.1 + - uses: sigstore/cosign-installer@bb61838e7ee5bf314f85f2e219b3706835fa6306 # v2.0.1 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 with: diff --git a/.github/workflows/github-oidc.yaml b/.github/workflows/github-oidc.yaml index d6820f155a1..72c571eb5f5 100644 --- a/.github/workflows/github-oidc.yaml +++ b/.github/workflows/github-oidc.yaml @@ -41,7 +41,7 @@ jobs: go-version: '1.17.x' # Install tools. - - uses: sigstore/cosign-installer@d6a3abf1bdea83574e28d40543793018b6035605 # v2.0.1 + - uses: sigstore/cosign-installer@bb61838e7ee5bf314f85f2e219b3706835fa6306 # v2.0.1 - uses: imjasonh/setup-ko@2c3450ca27f6e6f2b02e72a40f2163c281a1f675 # v0.4 - name: Build and sign a container image From 0c232dac0fb424e21e97745a188b840b4ad11911 Mon Sep 17 00:00:00 2001 From: Ville Aikas <11279988+vaikas@users.noreply.github.com> Date: Thu, 14 Apr 2022 11:12:17 -0700 Subject: [PATCH 14/53] Implement identities, fix bug in webhook validation. (#1759) * Implement identites, fix bug in webhook validation. Signed-off-by: Ville Aikas * fix lint. Signed-off-by: Ville Aikas * Fix the invalid test that was incorrectly assuming identities can't be specified. Fix the actions yaml file typo. missing actual run, doh. Signed-off-by: Ville Aikas --- .../workflows/kind-cluster-image-policy.yaml | 42 ++++++++- .../v1alpha1/clusterimagepolicy_validation.go | 9 +- .../clusterimagepolicy_validation_test.go | 2 +- pkg/cosign/kubernetes/webhook/validation.go | 6 +- pkg/cosign/kubernetes/webhook/validator.go | 2 +- pkg/cosign/verify.go | 93 ++++++++++++++++--- pkg/cosign/verify_test.go | 92 ++++++++++++++++++ test/cert_utils.go | 35 +++++++ .../cip-keyless-with-identities-mismatch.yaml | 29 ++++++ .../e2e/cip-keyless-with-identities.yaml | 29 ++++++ .../keylessref-with-multiple-properties.yaml | 4 +- 11 files changed, 318 insertions(+), 25 deletions(-) create mode 100644 test/testdata/cosigned/e2e/cip-keyless-with-identities-mismatch.yaml create mode 100644 test/testdata/cosigned/e2e/cip-keyless-with-identities.yaml diff --git a/.github/workflows/kind-cluster-image-policy.yaml b/.github/workflows/kind-cluster-image-policy.yaml index 01666ed65af..963f5f6a4be 100644 --- a/.github/workflows/kind-cluster-image-policy.yaml +++ b/.github/workflows/kind-cluster-image-policy.yaml @@ -140,7 +140,7 @@ jobs: echo '::group:: test job success' # We signed this above, this should work if ! kubectl create -n demo-keyless-signing job demo --image=${{ env.demoimage }} ; then - echo Failed to create Job in namespace without label! + echo Failed to create Job in namespace with matching signature! exit 1 else echo Succcessfully created Job with signed image @@ -157,6 +157,46 @@ jobs: fi echo '::endgroup::' + - name: Add ClusterImagePolicy with identities that match issuer and subject + run: | + kubectl apply -f ./test/testdata/cosigned/e2e/cip-keyless-with-identities.yaml + # make sure the reconciler has enough time to update the configmap + sleep 5 + + - name: Verify the job still works with additional constraints + run: | + echo '::group:: test job success' + # This has correct issuer/subject, so should work + if ! kubectl create -n demo-keyless-signing job demo-identities-works --image=${{ env.demoimage }} ; then + echo Failed to create Job in namespace without label! + exit 1 + else + echo Succcessfully created Job with signed image + fi + echo '::endgroup:: test job success' + + - name: Add ClusterImagePolicy with identities that do not match issuer and subject + run: | + kubectl apply -f ./test/testdata/cosigned/e2e/cip-keyless-with-identities-mismatch.yaml + # make sure the reconciler has enough time to update the configmap + sleep 5 + + - name: Verify the job now fails because subject and issuer do not match + run: | + echo '::group:: test job block' + if kubectl create -n demo-keyless-signing job demo-identities-works --image=${{ env.demoimage }} ; then + echo Failed to block Job in namespace with non matching issuer and subject! + exit 1 + else + echo Succcessfully blocked Job with mismatching issuer and subject + fi + echo '::endgroup:: test job block' + + - name: Remove the mismatching cip + run: | + kubectl delete cip image-policy-keyless-with-identities-mismatch + sleep 5 + - name: Generate New Signing Key run: | COSIGN_PASSWORD="" ./cosign generate-key-pair diff --git a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go index 0c8300fbd13..edc12b907ac 100644 --- a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go +++ b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go @@ -115,12 +115,9 @@ func (keyless *KeylessRef) Validate(ctx context.Context) *apis.FieldError { errs = errs.Also(apis.ErrMissingOneOf("url", "identities", "ca-cert")) } - if keyless.URL != nil { - if keyless.CACert != nil || keyless.Identities != nil { - errs = errs.Also(apis.ErrMultipleOneOf("url", "identities", "ca-cert")) - } - } else if keyless.CACert != nil && keyless.Identities != nil { - errs = errs.Also(apis.ErrMultipleOneOf("url", "identities", "ca-cert")) + // TODO: Are these really mutually exclusive? + if keyless.URL != nil && keyless.CACert != nil { + errs = errs.Also(apis.ErrMultipleOneOf("url", "ca-cert")) } if keyless.Identities != nil && len(keyless.Identities) == 0 { diff --git a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go index ba7edfd8645..5d04662e836 100644 --- a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go +++ b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go @@ -295,7 +295,7 @@ func TestKeylessValidation(t *testing.T) { { name: "Should fail when keyless has multiple properties", expectErr: true, - errorString: "expected exactly one, got both: spec.authorities[0].keyless.ca-cert, spec.authorities[0].keyless.identities, spec.authorities[0].keyless.url", + errorString: "expected exactly one, got both: spec.authorities[0].keyless.ca-cert, spec.authorities[0].keyless.url", policy: ClusterImagePolicy{ Spec: ClusterImagePolicySpec{ Images: []ImagePattern{ diff --git a/pkg/cosign/kubernetes/webhook/validation.go b/pkg/cosign/kubernetes/webhook/validation.go index 72dc704341e..6a54ade1faa 100644 --- a/pkg/cosign/kubernetes/webhook/validation.go +++ b/pkg/cosign/kubernetes/webhook/validation.go @@ -29,6 +29,7 @@ import ( "knative.dev/pkg/logging" "github.com/sigstore/cosign/cmd/cosign/cli/fulcio/fulcioroots" + v1alpha1 "github.com/sigstore/cosign/pkg/apis/cosigned/v1alpha1" "github.com/sigstore/cosign/pkg/cosign" "github.com/sigstore/cosign/pkg/oci" ociremote "github.com/sigstore/cosign/pkg/oci/remote" @@ -39,7 +40,7 @@ import ( func valid(ctx context.Context, ref name.Reference, keys []*ecdsa.PublicKey, opts ...ociremote.Option) ([]oci.Signature, error) { if len(keys) == 0 { // If there are no keys, then verify against the fulcio root. - sps, err := validSignaturesWithFulcio(ctx, ref, fulcioroots.Get(), nil /* rekor */, opts...) + sps, err := validSignaturesWithFulcio(ctx, ref, fulcioroots.Get(), nil /* rekor */, nil /* no identities */, opts...) if err != nil { return nil, err } @@ -86,12 +87,13 @@ func validSignatures(ctx context.Context, ref name.Reference, verifier signature // validSignaturesWithFulcio expects a Fulcio Cert to verify against. An // optional rekorClient can also be given, if nil passed, default is assumed. -func validSignaturesWithFulcio(ctx context.Context, ref name.Reference, fulcioRoots *x509.CertPool, rekorClient *client.Rekor, opts ...ociremote.Option) ([]oci.Signature, error) { +func validSignaturesWithFulcio(ctx context.Context, ref name.Reference, fulcioRoots *x509.CertPool, rekorClient *client.Rekor, identities []v1alpha1.Identity, opts ...ociremote.Option) ([]oci.Signature, error) { sigs, _, err := cosignVerifySignatures(ctx, ref, &cosign.CheckOpts{ RegistryClientOpts: opts, RootCerts: fulcioRoots, RekorClient: rekorClient, ClaimVerifier: cosign.SimpleClaimVerifier, + Identities: identities, }) return sigs, err } diff --git a/pkg/cosign/kubernetes/webhook/validator.go b/pkg/cosign/kubernetes/webhook/validator.go index 474d81f35a2..bd95f87623f 100644 --- a/pkg/cosign/kubernetes/webhook/validator.go +++ b/pkg/cosign/kubernetes/webhook/validator.go @@ -303,7 +303,7 @@ func ValidatePolicy(ctx context.Context, ref name.Reference, kc authn.Keychain, continue } } - sps, err := validSignaturesWithFulcio(ctx, ref, fulcioroot, rekorClient, remoteOpts...) + sps, err := validSignaturesWithFulcio(ctx, ref, fulcioroot, rekorClient, authority.Keyless.Identities, remoteOpts...) if err != nil { logging.FromContext(ctx).Errorf("failed validSignatures with fulcio for %s: %v", ref.Name(), err) authorityErrors = append(authorityErrors, errors.Wrap(err, "validate signatures with fulcio")) diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index 356aeed0ecf..cdcc05074d5 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -27,9 +27,11 @@ import ( "encoding/json" "fmt" "os" + "regexp" "strings" "time" + "github.com/sigstore/cosign/pkg/apis/cosigned/v1alpha1" cbundle "github.com/sigstore/cosign/pkg/cosign/bundle" "github.com/sigstore/cosign/pkg/cosign/tuf" @@ -84,6 +86,11 @@ type CheckOpts struct { // SignatureRef is the reference to the signature file SignatureRef string + + // Identities is an array of Identity (Subject, Issuer) matchers that have + // to be met for the signature to ve valid. + // Supercedes CertEmail / CertOidcIssuer + Identities []v1alpha1.Identity } func getSignedEntity(signedImgRef name.Reference, regClientOpts []ociremote.Option) (oci.SignedEntity, v1.Hash, error) { @@ -143,7 +150,7 @@ func verifyOCIAttestation(_ context.Context, verifier signature.Verifier, att pa } // ValidateAndUnpackCert creates a Verifier from a certificate. Veries that the certificate -// chains up to a trusted root. Optionally verifies the subject of the certificate. +// chains up to a trusted root. Optionally verifies the subject and issuer of the certificate. func ValidateAndUnpackCert(cert *x509.Certificate, co *CheckOpts) (signature.Verifier, error) { verifier, err := signature.LoadVerifier(cert.PublicKey, crypto.SHA256) if err != nil { @@ -167,23 +174,87 @@ func ValidateAndUnpackCert(cert *x509.Certificate, co *CheckOpts) (signature.Ver } } if co.CertOidcIssuer != "" { - issuer := "" - for _, ext := range cert.Extensions { - if ext.Id.Equal(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1}) { - issuer = string(ext.Value) - break - } - } - if issuer != co.CertOidcIssuer { + if getIssuer(cert) != co.CertOidcIssuer { return nil, errors.New("expected oidc issuer not found in certificate") } } + // If there are identities given, go through them and if one of them + // matches, call that good, otherwise, return an error. + if len(co.Identities) > 0 { + for _, identity := range co.Identities { + issuerMatches := false + // Check the issuer first + fmt.Fprintf(os.Stderr, "Checking identity: %+v", identity) + if identity.Issuer != "" { + issuer := getIssuer(cert) + if regex, err := regexp.Compile(identity.Issuer); err != nil { + return nil, fmt.Errorf("malformed issuer in identity: %s : %w", identity.Issuer, err) + } else if regex.MatchString(issuer) { + issuerMatches = true + } + } else { + // No issuer constraint on this identity, so checks out + issuerMatches = true + } + + // Then the subject + subjectMatches := false + if identity.Subject != "" { + regex, err := regexp.Compile(identity.Subject) + if err != nil { + return nil, fmt.Errorf("malformed subject in identity: %s : %w", identity.Subject, err) + } + for _, san := range getSubjectAlternateNames(cert) { + if regex.MatchString(san) { + subjectMatches = true + break + } + } + } else { + // No subject constraint on this identity, so checks out + subjectMatches = true + } + if subjectMatches && issuerMatches { + // If both issuer / subject match, return verifier + return verifier, nil + } + } + return nil, errors.New("none of the expected identities matched what was in the certificate") + } return verifier, nil } -// ValidateAndUnpackCertWithChain creates a Verifier from a certificate. Veries that the certificate +// getSubjectAlternateNames returns all of the following for a Certificate. +// DNSNames +// EmailAddresses +// IPAddresses +// URIs +func getSubjectAlternateNames(cert *x509.Certificate) []string { + sans := []string{} + sans = append(sans, cert.DNSNames...) + sans = append(sans, cert.EmailAddresses...) + for _, ip := range cert.IPAddresses { + sans = append(sans, ip.String()) + } + for _, uri := range cert.URIs { + sans = append(sans, uri.String()) + } + return sans +} + +// getIssuer returns the issuer for a Certificate +func getIssuer(cert *x509.Certificate) string { + for _, ext := range cert.Extensions { + if ext.Id.Equal(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1}) { + return string(ext.Value) + } + } + return "" +} + +// ValidateAndUnpackCertWithChain creates a Verifier from a certificate. Verifies that the certificate // chains up to the provided root. Chain should start with the parent of the certificate and end with the root. -// Optionally verifies the subject of the certificate. +// Optionally verifies the subject and issuer of the certificate. func ValidateAndUnpackCertWithChain(cert *x509.Certificate, chain []*x509.Certificate, co *CheckOpts) (signature.Verifier, error) { if len(chain) == 0 { return nil, errors.New("no chain provided to validate certificate") diff --git a/pkg/cosign/verify_test.go b/pkg/cosign/verify_test.go index bba5dfd06a9..83e91a3ee5c 100644 --- a/pkg/cosign/verify_test.go +++ b/pkg/cosign/verify_test.go @@ -26,6 +26,8 @@ import ( "encoding/json" "encoding/pem" "io" + "net" + "net/url" "strings" "testing" "time" @@ -36,6 +38,7 @@ import ( "github.com/in-toto/in-toto-golang/in_toto" "github.com/pkg/errors" "github.com/secure-systems-lab/go-securesystemslib/dsse" + "github.com/sigstore/cosign/pkg/apis/cosigned/v1alpha1" "github.com/sigstore/cosign/pkg/cosign/bundle" ctuf "github.com/sigstore/cosign/pkg/cosign/tuf" "github.com/sigstore/cosign/pkg/oci/static" @@ -482,6 +485,95 @@ func TestValidateAndUnpackCertWithChainFailsWithInvalidChain(t *testing.T) { } } +func TestValidateAndUnpackCertWithIdentities(t *testing.T) { + u, err := url.Parse("http://url.example.com") + if err != nil { + t.Fatal("failed to parse url", err) + } + emailSubject := "email@example.com" + dnsSubjects := []string{"dnssubject.example.com"} + ipSubjects := []net.IP{net.ParseIP("1.2.3.4")} + uriSubjects := []*url.URL{u} + oidcIssuer := "https://accounts.google.com" + + tests := []struct { + identities []v1alpha1.Identity + wantErrSubstring string + dnsNames []string + emailAddresses []string + ipAddresses []net.IP + uris []*url.URL + }{ + {identities: nil /* No matches required, checks out */}, + {identities: []v1alpha1.Identity{ // Strict match on both + {Subject: emailSubject, Issuer: oidcIssuer}}, + emailAddresses: []string{emailSubject}, + wantErrSubstring: ""}, + {identities: []v1alpha1.Identity{ // just issuer + {Issuer: oidcIssuer}}, + emailAddresses: []string{emailSubject}, + wantErrSubstring: ""}, + {identities: []v1alpha1.Identity{ // just subject + {Subject: emailSubject}}, + emailAddresses: []string{emailSubject}, + wantErrSubstring: ""}, + {identities: []v1alpha1.Identity{ // mis-match + {Subject: "wrongsubject", Issuer: oidcIssuer}, + {Subject: emailSubject, Issuer: "wrongissuer"}}, + emailAddresses: []string{emailSubject}, + wantErrSubstring: "none of the expected identities matched"}, + {identities: []v1alpha1.Identity{ // one good identity, other does not match + {Subject: "wrongsubject", Issuer: "wrongissuer"}, + {Subject: emailSubject, Issuer: oidcIssuer}}, + emailAddresses: []string{emailSubject}, + wantErrSubstring: ""}, + {identities: []v1alpha1.Identity{ // illegal regex for subject + {Subject: "****", Issuer: oidcIssuer}}, + emailAddresses: []string{emailSubject}, + wantErrSubstring: "malformed subject in identity"}, + {identities: []v1alpha1.Identity{ // illegal regex for issuer + {Subject: emailSubject, Issuer: "****"}}, + wantErrSubstring: "malformed issuer in identity"}, + {identities: []v1alpha1.Identity{ // regex matches + {Subject: ".*example.com", Issuer: ".*accounts.google.*"}}, + emailAddresses: []string{emailSubject}, + wantErrSubstring: ""}, + {identities: []v1alpha1.Identity{ // regex matches dnsNames + {Subject: ".*ubject.example.com", Issuer: ".*accounts.google.*"}}, + dnsNames: dnsSubjects, + wantErrSubstring: ""}, + {identities: []v1alpha1.Identity{ // regex matches ip + {Subject: "1.2.3.*", Issuer: ".*accounts.google.*"}}, + ipAddresses: ipSubjects, + wantErrSubstring: ""}, + {identities: []v1alpha1.Identity{ // regex matches urls + {Subject: ".*url.examp.*", Issuer: ".*accounts.google.*"}}, + uris: uriSubjects, + wantErrSubstring: ""}, + } + for _, tc := range tests { + rootCert, rootKey, _ := test.GenerateRootCa() + leafCert, _, _ := test.GenerateLeafCertWithSubjectAlternateNames(tc.dnsNames, tc.emailAddresses, tc.ipAddresses, tc.uris, oidcIssuer, rootCert, rootKey) + + rootPool := x509.NewCertPool() + rootPool.AddCert(rootCert) + + co := &CheckOpts{ + RootCerts: rootPool, + Identities: tc.identities, + } + _, err := ValidateAndUnpackCert(leafCert, co) + if err == nil && tc.wantErrSubstring != "" { + t.Errorf("Expected error %s got none", tc.wantErrSubstring) + } else if err != nil { + if tc.wantErrSubstring == "" { + t.Errorf("Did not expect an error, got err = %v", err) + } else if !strings.Contains(err.Error(), tc.wantErrSubstring) { + t.Errorf("Did not get the expected error %s, got err = %v", tc.wantErrSubstring, err) + } + } + } +} func TestCompareSigs(t *testing.T) { // TODO(nsmith5): Add test cases for invalid signature, missing signature etc tests := []struct { diff --git a/test/cert_utils.go b/test/cert_utils.go index 25e444ba95a..e39e84707c5 100644 --- a/test/cert_utils.go +++ b/test/cert_utils.go @@ -23,6 +23,8 @@ import ( "crypto/x509/pkix" "encoding/asn1" "math/big" + "net" + "net/url" "time" ) @@ -144,3 +146,36 @@ func GenerateLeafCert(subject string, oidcIssuer string, parentTemplate *x509.Ce return cert, priv, nil } + +func GenerateLeafCertWithSubjectAlternateNames(dnsNames []string, emailAddresses []string, ipAddresses []net.IP, uris []*url.URL, oidcIssuer string, parentTemplate *x509.Certificate, parentPriv crypto.Signer) (*x509.Certificate, *ecdsa.PrivateKey, error) { + certTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(1), + EmailAddresses: emailAddresses, + DNSNames: dnsNames, + IPAddresses: ipAddresses, + URIs: uris, + NotBefore: time.Now().Add(-1 * time.Minute), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, + IsCA: false, + ExtraExtensions: []pkix.Extension{{ + // OID for OIDC Issuer extension + Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1}, + Critical: false, + Value: []byte(oidcIssuer), + }}, + } + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, err + } + + cert, err := createCertificate(certTemplate, parentTemplate, &priv.PublicKey, parentPriv) + if err != nil { + return nil, nil, err + } + + return cert, priv, nil +} diff --git a/test/testdata/cosigned/e2e/cip-keyless-with-identities-mismatch.yaml b/test/testdata/cosigned/e2e/cip-keyless-with-identities-mismatch.yaml new file mode 100644 index 00000000000..15e21dfad22 --- /dev/null +++ b/test/testdata/cosigned/e2e/cip-keyless-with-identities-mismatch.yaml @@ -0,0 +1,29 @@ +# 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 +# +# https://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. + +apiVersion: cosigned.sigstore.dev/v1alpha1 +kind: ClusterImagePolicy +metadata: + name: image-policy-keyless-with-identities-mismatch +spec: + images: + - glob: registry.local:5000/cosigned/demo* + authorities: + - keyless: + url: http://fulcio.fulcio-system.svc + identities: + - issuer: .*kubernetes.securenamespace.* + subject: .*kubernetes.io/namespaces/securenamespace/serviceaccounts/default + ctlog: + url: http://rekor.rekor-system.svc diff --git a/test/testdata/cosigned/e2e/cip-keyless-with-identities.yaml b/test/testdata/cosigned/e2e/cip-keyless-with-identities.yaml new file mode 100644 index 00000000000..5e67eb42b85 --- /dev/null +++ b/test/testdata/cosigned/e2e/cip-keyless-with-identities.yaml @@ -0,0 +1,29 @@ +# 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 +# +# https://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. + +apiVersion: cosigned.sigstore.dev/v1alpha1 +kind: ClusterImagePolicy +metadata: + name: image-policy-keyless-with-identities +spec: + images: + - glob: registry.local:5000/cosigned/demo* + authorities: + - keyless: + url: http://fulcio.fulcio-system.svc + identities: + - issuer: .*kubernetes.default.* + subject: .*kubernetes.io/namespaces/default/serviceaccounts/default + ctlog: + url: http://rekor.rekor-system.svc diff --git a/test/testdata/cosigned/invalid/keylessref-with-multiple-properties.yaml b/test/testdata/cosigned/invalid/keylessref-with-multiple-properties.yaml index 1d3af5bcf24..eff58f34b06 100644 --- a/test/testdata/cosigned/invalid/keylessref-with-multiple-properties.yaml +++ b/test/testdata/cosigned/invalid/keylessref-with-multiple-properties.yaml @@ -25,6 +25,4 @@ spec: secretRef: name: ca-cert-secret namespace: some-namespace - identities: - - issuer: "issue-details" - subject: "subject-details" + url: http://example.com From 95169988771b3b303884bca6fd1a63f54c387721 Mon Sep 17 00:00:00 2001 From: Ville Aikas <11279988+vaikas@users.noreply.github.com> Date: Thu, 14 Apr 2022 12:29:05 -0700 Subject: [PATCH 15/53] Validate issuer/subject regexp in validate webhook. (#1761) Signed-off-by: Ville Aikas --- .../v1alpha1/clusterimagepolicy_validation.go | 6 ++ .../clusterimagepolicy_validation_test.go | 61 +++++++++++++++++++ .../keylessref-with-malformed-issuer.yaml | 25 ++++++++ .../keylessref-with-malformed-subject.yaml | 25 ++++++++ .../cosigned/valid/valid-policy-regex.yaml | 4 ++ 5 files changed, 121 insertions(+) create mode 100644 test/testdata/cosigned/invalid/keylessref-with-malformed-issuer.yaml create mode 100644 test/testdata/cosigned/invalid/keylessref-with-malformed-subject.yaml diff --git a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go index edc12b907ac..c16db8736a7 100644 --- a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go +++ b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go @@ -135,6 +135,12 @@ func (identity *Identity) Validate(ctx context.Context) *apis.FieldError { if identity.Issuer == "" && identity.Subject == "" { errs = errs.Also(apis.ErrMissingOneOf("issuer", "subject")) } + if identity.Issuer != "" { + errs = errs.Also(ValidateRegex(identity.Issuer).ViaField("issuer")) + } + if identity.Subject != "" { + errs = errs.Also(ValidateRegex(identity.Subject).ViaField("subject")) + } return errs } diff --git a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go index 5d04662e836..b3213cc67a8 100644 --- a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go +++ b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go @@ -439,6 +439,67 @@ func TestIdentitiesValidation(t *testing.T) { }, }, }, + { + name: "Should fail when issuer has invalid regex", + expectErr: true, + errorString: "invalid value: ****: spec.authorities[0].keyless.identities[0].issuer\nregex is invalid: error parsing regexp: missing argument to repetition operator: `*`", + policy: ClusterImagePolicy{ + Spec: ClusterImagePolicySpec{ + Images: []ImagePattern{ + { + Glob: "globbityglob", + }, + }, + Authorities: []Authority{ + { + Keyless: &KeylessRef{ + Identities: []Identity{{Issuer: "****"}}, + }, + }, + }, + }, + }, + }, + { + name: "Should fail when subject has invalid regex", + expectErr: true, + errorString: "invalid value: ****: spec.authorities[0].keyless.identities[0].subject\nregex is invalid: error parsing regexp: missing argument to repetition operator: `*`", + policy: ClusterImagePolicy{ + Spec: ClusterImagePolicySpec{ + Images: []ImagePattern{ + { + Glob: "globbityglob", + }, + }, + Authorities: []Authority{ + { + Keyless: &KeylessRef{ + Identities: []Identity{{Subject: "****"}}, + }, + }, + }, + }, + }, + }, + { + name: "Should pass when subject and issuer have valid regex", + policy: ClusterImagePolicy{ + Spec: ClusterImagePolicySpec{ + Images: []ImagePattern{ + { + Glob: "globbityglob", + }, + }, + Authorities: []Authority{ + { + Keyless: &KeylessRef{ + Identities: []Identity{{Subject: ".*subject.*", Issuer: ".*issuer.*"}}, + }, + }, + }, + }, + }, + }, { name: "Should pass when identities is valid", expectErr: false, diff --git a/test/testdata/cosigned/invalid/keylessref-with-malformed-issuer.yaml b/test/testdata/cosigned/invalid/keylessref-with-malformed-issuer.yaml new file mode 100644 index 00000000000..7b04359ee99 --- /dev/null +++ b/test/testdata/cosigned/invalid/keylessref-with-malformed-issuer.yaml @@ -0,0 +1,25 @@ +# 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. +--- +apiVersion: cosigned.sigstore.dev/v1alpha1 +kind: ClusterImagePolicy +metadata: + name: image-policy +spec: + images: + - glob: image* + authorities: + - keyless: + identities: + - issuer: **** diff --git a/test/testdata/cosigned/invalid/keylessref-with-malformed-subject.yaml b/test/testdata/cosigned/invalid/keylessref-with-malformed-subject.yaml new file mode 100644 index 00000000000..fbfd5f57288 --- /dev/null +++ b/test/testdata/cosigned/invalid/keylessref-with-malformed-subject.yaml @@ -0,0 +1,25 @@ +# 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. +--- +apiVersion: cosigned.sigstore.dev/v1alpha1 +kind: ClusterImagePolicy +metadata: + name: image-policy +spec: + images: + - glob: image* + authorities: + - keyless: + identities: + - subject: **** diff --git a/test/testdata/cosigned/valid/valid-policy-regex.yaml b/test/testdata/cosigned/valid/valid-policy-regex.yaml index a82ae479d84..804ff4127ec 100644 --- a/test/testdata/cosigned/valid/valid-policy-regex.yaml +++ b/test/testdata/cosigned/valid/valid-policy-regex.yaml @@ -33,6 +33,10 @@ spec: - keyless: identities: - issuer: "issue-details1" + subject: ".*subject.*" + - keyless: + identities: + - issuer: "issue.*" - key: data: | -----BEGIN PUBLIC KEY----- From 70a3d8c68ad9156d6a81b8a0082dde0e8b91ccbf Mon Sep 17 00:00:00 2001 From: Hector Fernandez Date: Thu, 14 Apr 2022 22:40:04 +0200 Subject: [PATCH 16/53] chore: add warn when attaching sBOM (#1756) Signed-off-by: hectorj2f --- cmd/cosign/cli/attach.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cmd/cosign/cli/attach.go b/cmd/cosign/cli/attach.go index fbea7b0dc85..99281af7a61 100644 --- a/cmd/cosign/cli/attach.go +++ b/cmd/cosign/cli/attach.go @@ -16,6 +16,9 @@ package cli import ( + "fmt" + "os" + "github.com/sigstore/cosign/cmd/cosign/cli/attach" "github.com/sigstore/cosign/cmd/cosign/cli/options" "github.com/spf13/cobra" @@ -67,6 +70,7 @@ func attachSBOM() *cobra.Command { if err != nil { return err } + fmt.Fprintf(os.Stderr, "WARNING: Attaching SBOMs this way does not sign them. If you want to sign them, use 'cosign attest -predicate %s -key ' or 'cosign sign -key '.\n", o.SBOM) return attach.SBOMCmd(cmd.Context(), o.Registry, o.SBOM, mediaType, args[0]) }, } From 920dcfadc29a3b0a04ee3f4dc503fb08ab96437a Mon Sep 17 00:00:00 2001 From: Hayden B Date: Thu, 14 Apr 2022 17:19:34 -0700 Subject: [PATCH 17/53] Verify embedded SCTs (#1731) * Add verification for embedded SCTs This adds support for verifying SCTs embedded in certificates in addition to being detached. Embedded SCTs will be verified while parsing the certificate on verification (for verify, verify-blob, and verify-attestation), and on signing if a certificate chain is provided. Signed-off-by: Hayden Blauzvern * Add policy flag to enforce SCT Signed-off-by: Hayden Blauzvern * Added comment for imported package Signed-off-by: Hayden Blauzvern --- cmd/cosign/cli/dockerfile.go | 1 + cmd/cosign/cli/fulcio/depcheck_test.go | 1 - .../testdata/garbage-there-are-limits | 0 .../fulcioverifier/{ => ctl}/testdata/google | 0 .../testdata/letsencrypt-testflume-2021 | Bin .../fulcioverifier/{ => ctl}/testdata/rsa | 0 .../cli/fulcio/fulcioverifier/ctl/verify.go | 229 +++++++++++ .../fulcio/fulcioverifier/ctl/verify_test.go | 250 ++++++++++++ .../fulcio/fulcioverifier/ctutil/ctutil.go | 224 ++++++++++ .../fulcioverifier/ctutil/ctutil_test.go | 385 ++++++++++++++++++ .../fulcio/fulcioverifier/fulcioverifier.go | 116 +----- .../fulcioverifier/fulcioverifier_test.go | 73 ---- cmd/cosign/cli/manifest.go | 1 + cmd/cosign/cli/options/certificate.go | 5 + cmd/cosign/cli/sign/sign.go | 16 +- cmd/cosign/cli/verify.go | 5 +- cmd/cosign/cli/verify/verify.go | 2 + cmd/cosign/cli/verify/verify_attestation.go | 2 + cmd/cosign/cli/verify/verify_blob.go | 10 +- doc/cosign_dockerfile_verify.md | 1 + doc/cosign_manifest_verify.md | 1 + doc/cosign_verify-attestation.md | 1 + doc/cosign_verify-blob.md | 1 + doc/cosign_verify.md | 1 + go.mod | 2 + go.sum | 2 - pkg/cosign/verify.go | 34 +- pkg/cosign/verify_test.go | 73 +++- test/e2e_test.go | 14 +- .../google/trillian/{ => merkle}/LICENSE | 0 30 files changed, 1239 insertions(+), 211 deletions(-) rename cmd/cosign/cli/fulcio/fulcioverifier/{ => ctl}/testdata/garbage-there-are-limits (100%) rename cmd/cosign/cli/fulcio/fulcioverifier/{ => ctl}/testdata/google (100%) rename cmd/cosign/cli/fulcio/fulcioverifier/{ => ctl}/testdata/letsencrypt-testflume-2021 (100%) rename cmd/cosign/cli/fulcio/fulcioverifier/{ => ctl}/testdata/rsa (100%) create mode 100644 cmd/cosign/cli/fulcio/fulcioverifier/ctl/verify.go create mode 100644 cmd/cosign/cli/fulcio/fulcioverifier/ctl/verify_test.go create mode 100644 cmd/cosign/cli/fulcio/fulcioverifier/ctutil/ctutil.go create mode 100644 cmd/cosign/cli/fulcio/fulcioverifier/ctutil/ctutil_test.go delete mode 100644 cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier_test.go rename third_party/VENDOR-LICENSE/github.com/google/trillian/{ => merkle}/LICENSE (100%) diff --git a/cmd/cosign/cli/dockerfile.go b/cmd/cosign/cli/dockerfile.go index fdcf524c49e..27e37b2b778 100644 --- a/cmd/cosign/cli/dockerfile.go +++ b/cmd/cosign/cli/dockerfile.go @@ -92,6 +92,7 @@ Shell-like variables in the Dockerfile's FROM lines will be substituted with val CertEmail: o.CertVerify.CertEmail, CertOidcIssuer: o.CertVerify.CertOidcIssuer, CertChain: o.CertVerify.CertChain, + EnforceSCT: o.CertVerify.EnforceSCT, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, Output: o.Output, diff --git a/cmd/cosign/cli/fulcio/depcheck_test.go b/cmd/cosign/cli/fulcio/depcheck_test.go index 39493e5a86c..bc7d45f2f26 100644 --- a/cmd/cosign/cli/fulcio/depcheck_test.go +++ b/cmd/cosign/cli/fulcio/depcheck_test.go @@ -25,7 +25,6 @@ func TestNoDeps(t *testing.T) { depcheck.AssertNoDependency(t, map[string][]string{ "github.com/sigstore/cosign/cmd/cosign/cli/fulcio": { // Avoid pulling in a variety of things that are massive dependencies. - "github.com/google/certificate-transparency-go", "github.com/google/trillian", "github.com/envoyproxy/go-control-plane", "github.com/gogo/protobuf/protoc-gen-gogo", diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/testdata/garbage-there-are-limits b/cmd/cosign/cli/fulcio/fulcioverifier/ctl/testdata/garbage-there-are-limits similarity index 100% rename from cmd/cosign/cli/fulcio/fulcioverifier/testdata/garbage-there-are-limits rename to cmd/cosign/cli/fulcio/fulcioverifier/ctl/testdata/garbage-there-are-limits diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/testdata/google b/cmd/cosign/cli/fulcio/fulcioverifier/ctl/testdata/google similarity index 100% rename from cmd/cosign/cli/fulcio/fulcioverifier/testdata/google rename to cmd/cosign/cli/fulcio/fulcioverifier/ctl/testdata/google diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/testdata/letsencrypt-testflume-2021 b/cmd/cosign/cli/fulcio/fulcioverifier/ctl/testdata/letsencrypt-testflume-2021 similarity index 100% rename from cmd/cosign/cli/fulcio/fulcioverifier/testdata/letsencrypt-testflume-2021 rename to cmd/cosign/cli/fulcio/fulcioverifier/ctl/testdata/letsencrypt-testflume-2021 diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/testdata/rsa b/cmd/cosign/cli/fulcio/fulcioverifier/ctl/testdata/rsa similarity index 100% rename from cmd/cosign/cli/fulcio/fulcioverifier/testdata/rsa rename to cmd/cosign/cli/fulcio/fulcioverifier/ctl/testdata/rsa diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/ctl/verify.go b/cmd/cosign/cli/fulcio/fulcioverifier/ctl/verify.go new file mode 100644 index 00000000000..49ca970fb82 --- /dev/null +++ b/cmd/cosign/cli/fulcio/fulcioverifier/ctl/verify.go @@ -0,0 +1,229 @@ +// 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 ctl + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/sha256" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + "os" + + ct "github.com/google/certificate-transparency-go" + ctx509 "github.com/google/certificate-transparency-go/x509" + "github.com/google/certificate-transparency-go/x509util" + "github.com/pkg/errors" + "github.com/sigstore/cosign/cmd/cosign/cli/fulcio/fulcioverifier/ctutil" + + "github.com/sigstore/cosign/pkg/cosign/tuf" + "github.com/sigstore/sigstore/pkg/cryptoutils" +) + +// This is the CT log public key target name +var ctPublicKeyStr = `ctfe.pub` + +// Setting this env variable will over ride what is used to validate +// the SCT coming back from Fulcio. +const altCTLogPublicKeyLocation = "SIGSTORE_CT_LOG_PUBLIC_KEY_FILE" + +// logIDMetadata holds information for mapping a key ID hash (log ID) to associated data. +type logIDMetadata struct { + pubKey crypto.PublicKey + status tuf.StatusKind +} + +// ContainsSCT checks if the certificate contains embedded SCTs. cert can either be +// DER or PEM encoded. +func ContainsSCT(cert []byte) (bool, error) { + embeddedSCTs, err := x509util.ParseSCTsFromCertificate(cert) + if err != nil { + return false, err + } + if len(embeddedSCTs) != 0 { + return true, nil + } + return false, nil +} + +// VerifySCT verifies SCTs against the Fulcio CT log public key. +// +// The SCT is a `Signed Certificate Timestamp`, which promises that +// the certificate issued by Fulcio was also added to the public CT log within +// some defined time period. +// +// VerifySCT can verify an SCT list embedded in the certificate, or a detached +// SCT provided by Fulcio. +// +// By default the public keys comes from TUF, but you can override this for test +// purposes by using an env variable `SIGSTORE_CT_LOG_PUBLIC_KEY_FILE`. If using +// an alternate, the file can be PEM, or DER format. +func VerifySCT(ctx context.Context, certPEM, chainPEM, rawSCT []byte) error { + // fetch SCT verification key + pubKeys := make(map[[sha256.Size]byte]logIDMetadata) + rootEnv := os.Getenv(altCTLogPublicKeyLocation) + if rootEnv == "" { + tufClient, err := tuf.NewFromEnv(ctx) + if err != nil { + return err + } + defer tufClient.Close() + + targets, err := tufClient.GetTargetsByMeta(tuf.CTFE, []string{ctPublicKeyStr}) + if err != nil { + return err + } + for _, t := range targets { + pub, err := cryptoutils.UnmarshalPEMToPublicKey(t.Target) + if err != nil { + return err + } + ctPub, ok := pub.(*ecdsa.PublicKey) + if !ok { + return fmt.Errorf("invalid public key: was %T, require *ecdsa.PublicKey", pub) + } + keyID, err := ctutil.GetCTLogID(ctPub) + if err != nil { + return errors.Wrap(err, "error getting CTFE public key hash") + } + pubKeys[keyID] = logIDMetadata{ctPub, t.Status} + } + } else { + fmt.Fprintf(os.Stderr, "**Warning** Using a non-standard public key for verifying SCT: %s\n", rootEnv) + raw, err := os.ReadFile(rootEnv) + if err != nil { + return errors.Wrap(err, "error reading alternate public key file") + } + pubKey, err := getAlternatePublicKey(raw) + if err != nil { + return errors.Wrap(err, "error parsing alternate public key from the file") + } + keyID, err := ctutil.GetCTLogID(pubKey) + if err != nil { + return errors.Wrap(err, "error getting CTFE public key hash") + } + pubKeys[keyID] = logIDMetadata{pubKey, tuf.Active} + } + if len(pubKeys) == 0 { + return errors.New("none of the CTFE keys have been found") + } + + // parse certificate and chain + cert, err := x509util.CertificateFromPEM(certPEM) + if err != nil { + return err + } + certChain, err := x509util.CertificatesFromPEM(chainPEM) + if err != nil { + return err + } + if len(certChain) == 0 { + return errors.New("no certificate chain found") + } + + // fetch embedded SCT if present + embeddedSCTs, err := x509util.ParseSCTsFromCertificate(certPEM) + if err != nil { + return err + } + // SCT must be either embedded or in header + if len(embeddedSCTs) == 0 && len(rawSCT) == 0 { + return errors.New("no SCT found") + } + + // check SCT embedded in certificate + if len(embeddedSCTs) != 0 { + for _, sct := range embeddedSCTs { + pubKeyMetadata, ok := pubKeys[sct.LogID.KeyID] + if !ok { + return errors.New("ctfe public key not found for embedded SCT") + } + err := ctutil.VerifySCT(pubKeyMetadata.pubKey, []*ctx509.Certificate{cert, certChain[0]}, sct, true) + if err != nil { + return errors.Wrap(err, "error verifying embedded SCT") + } + if pubKeyMetadata.status != tuf.Active { + fmt.Fprintf(os.Stderr, "**Info** Successfully verified embedded SCT using an expired verification key\n") + } + } + return nil + } + + // check SCT in response header + var addChainResp ct.AddChainResponse + if err := json.Unmarshal(rawSCT, &addChainResp); err != nil { + return errors.Wrap(err, "unmarshal") + } + sct, err := addChainResp.ToSignedCertificateTimestamp() + if err != nil { + return err + } + pubKeyMetadata, ok := pubKeys[sct.LogID.KeyID] + if !ok { + return errors.New("ctfe public key not found") + } + err = ctutil.VerifySCT(pubKeyMetadata.pubKey, []*ctx509.Certificate{cert}, sct, false) + if err != nil { + return errors.Wrap(err, "error verifying SCT") + } + if pubKeyMetadata.status != tuf.Active { + fmt.Fprintf(os.Stderr, "**Info** Successfully verified SCT using an expired verification key\n") + } + return nil +} + +// VerifyEmbeddedSCT verifies an embedded SCT in a certificate. +func VerifyEmbeddedSCT(ctx context.Context, chain []*x509.Certificate) error { + if len(chain) < 2 { + return errors.New("certificate chain must contain at least a certificate and its issuer") + } + certPEM, err := cryptoutils.MarshalCertificateToPEM(chain[0]) + if err != nil { + return err + } + chainPEM, err := cryptoutils.MarshalCertificatesToPEM(chain[1:]) + if err != nil { + return err + } + return VerifySCT(ctx, certPEM, chainPEM, []byte{}) +} + +// Given a byte array, try to construct a public key from it. +// Will try first to see if it's PEM formatted, if not, then it will +// try to parse it as der publics, and failing that +func getAlternatePublicKey(in []byte) (crypto.PublicKey, error) { + var pubKey crypto.PublicKey + var err error + var derBytes []byte + pemBlock, _ := pem.Decode(in) + if pemBlock == nil { + fmt.Fprintf(os.Stderr, "Failed to decode non-standard public key for verifying SCT using PEM decode, trying as DER") + derBytes = in + } else { + derBytes = pemBlock.Bytes + } + pubKey, err = x509.ParsePKIXPublicKey(derBytes) + if err != nil { + // Try using the PKCS1 before giving up. + pubKey, err = x509.ParsePKCS1PublicKey(derBytes) + if err != nil { + return nil, errors.Wrap(err, "failed to parse alternate public key") + } + } + return pubKey, nil +} diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/ctl/verify_test.go b/cmd/cosign/cli/fulcio/fulcioverifier/ctl/verify_test.go new file mode 100644 index 00000000000..751e1423551 --- /dev/null +++ b/cmd/cosign/cli/fulcio/fulcioverifier/ctl/verify_test.go @@ -0,0 +1,250 @@ +// 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 ctl + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "encoding/base64" + "encoding/json" + "fmt" + "os" + "reflect" + "strings" + "testing" + + ct "github.com/google/certificate-transparency-go" + "github.com/google/certificate-transparency-go/testdata" + "github.com/google/certificate-transparency-go/tls" + "github.com/sigstore/sigstore/pkg/cryptoutils" +) + +func TestGetAlternatePublicKey(t *testing.T) { + wd, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get cwd: %v", err) + } + tests := []struct { + file string + wantErrSub string + wantType string + }{ + {file: "garbage-there-are-limits", wantErrSub: "failed to parse"}, + // Testflume 2021 from here, https://letsencrypt.org/docs/ct-logs/ + {file: "letsencrypt-testflume-2021", wantType: "*ecdsa.PublicKey"}, + // This needs to be parsed with the pkcs1, pkix won't do. + {file: "rsa", wantType: "*rsa.PublicKey"}, + // This works with pkix, from: + // https://www.gstatic.com/ct/log_list/v2/log_list_pubkey.pem + {file: "google", wantType: "*rsa.PublicKey"}, + } + for _, tc := range tests { + filepath := fmt.Sprintf("%s/testdata/%s", wd, tc.file) + bytes, err := os.ReadFile(filepath) + if err != nil { + t.Fatalf("Failed to read testfile %s : %v", tc.file, err) + } + got, err := getAlternatePublicKey(bytes) + switch { + case err == nil && tc.wantErrSub != "": + t.Errorf("Wanted Error for %s but got none", tc.file) + case err != nil && tc.wantErrSub == "": + t.Errorf("Did not want error for %s but got: %v", tc.file, err) + case err != nil && tc.wantErrSub != "": + if !strings.Contains(err.Error(), tc.wantErrSub) { + t.Errorf("Unexpected error for %s: %s wanted to contain: %s", tc.file, err.Error(), tc.wantErrSub) + } + } + switch { + case got == nil && tc.wantType != "": + t.Errorf("Wanted public key for %s but got none", tc.file) + case got != nil && tc.wantType == "": + t.Errorf("Did not want error for %s but got: %v", tc.file, err) + case got != nil && tc.wantType != "": + if reflect.TypeOf(got).String() != tc.wantType { + t.Errorf("Unexpected type for %s: %+T wanted: %s", tc.file, got, tc.wantType) + } + } + } +} + +func TestContainsSCT(t *testing.T) { + // test certificate without embedded SCT + contains, err := ContainsSCT([]byte(testdata.TestCertPEM)) + if err != nil { + t.Fatalf("unexpected error in ContainsSCT: %v", err) + } + if contains { + t.Fatalf("certificate unexpectedly contained SCT") + } + + // test certificate with embedded SCT + contains, err = ContainsSCT([]byte(testdata.TestEmbeddedCertPEM)) + if err != nil { + t.Fatalf("unexpected error in ContainsSCT: %v", err) + } + if !contains { + t.Fatalf("certificate unexpectedly did not contain SCT") + } +} + +// From https://github.com/google/certificate-transparency-go/blob/e76f3f637053b90c8168d29b01ca162cd235ace5/ctutil/ctutil_test.go +func TestVerifySCT(t *testing.T) { + tests := []struct { + desc string + certPEM string + chainPEM string + sct []byte + embedded bool + wantErr bool + errMsg string + }{ + { + desc: "cert", + certPEM: testdata.TestCertPEM, + chainPEM: testdata.CACertPEM, + sct: testdata.TestCertProof, + }, + { + desc: "invalid SCT", + certPEM: testdata.TestPreCertPEM, + chainPEM: testdata.CACertPEM, + sct: testdata.TestCertProof, + wantErr: true, + }, + { + desc: "cert with embedded SCT", + certPEM: testdata.TestEmbeddedCertPEM, + chainPEM: testdata.CACertPEM, + sct: testdata.TestPreCertProof, + embedded: true, + }, + { + desc: "cert with invalid embedded SCT", + certPEM: testdata.TestInvalidEmbeddedCertPEM, + chainPEM: testdata.CACertPEM, + sct: testdata.TestInvalidProof, + embedded: true, + wantErr: true, + errMsg: "failed to verify ECDSA signature", + }, + } + + writePubKey(t, testdata.LogPublicKeyPEM) + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + // convert SCT to response struct if detached + var sctBytes []byte + if !test.embedded { + var sct ct.SignedCertificateTimestamp + if _, err := tls.Unmarshal(test.sct, &sct); err != nil { + t.Fatalf("error tls-unmarshalling sct: %s", err) + } + chainResp, err := toAddChainResponse(&sct) + if err != nil { + t.Fatalf("error generating chain response: %v", err) + } + sctBytes, err = json.Marshal(chainResp) + if err != nil { + t.Fatalf("error marshalling chain: %v", err) + } + } + + err := VerifySCT(context.Background(), []byte(test.certPEM), []byte(test.chainPEM), sctBytes) + if gotErr := err != nil; gotErr != test.wantErr && !strings.Contains(err.Error(), test.errMsg) { + t.Errorf("VerifySCT(_,_,_, %t) = %v, want error? %t", test.embedded, err, test.wantErr) + } + }) + } +} + +func TestVerifySCTError(t *testing.T) { + // verify fails with mismatched verifcation key + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("unexpected error generating ECDSA key: %v", err) + } + pemKey, err := cryptoutils.MarshalPublicKeyToPEM(key.Public()) + if err != nil { + t.Fatalf("unexpected error marshalling ECDSA key: %v", err) + } + writePubKey(t, string(pemKey)) + err = VerifySCT(context.Background(), []byte(testdata.TestEmbeddedCertPEM), []byte(testdata.CACertPEM), []byte{}) + if err == nil || !strings.Contains(err.Error(), "ctfe public key not found") { + t.Fatalf("expected error verifying SCT with mismatched key: %v", err) + } + + // verify fails without either a detached SCT or embedded SCT + err = VerifySCT(context.Background(), []byte(testdata.TestCertPEM), []byte(testdata.CACertPEM), []byte{}) + if err == nil || !strings.Contains(err.Error(), "no SCT found") { + t.Fatalf("expected error verifying SCT without SCT: %v", err) + } +} + +func TestVerifyEmbeddedSCT(t *testing.T) { + chain, err := cryptoutils.UnmarshalCertificatesFromPEM([]byte(testdata.TestEmbeddedCertPEM + testdata.CACertPEM)) + if err != nil { + t.Fatalf("error unmarshalling certificate chain: %v", err) + } + + // verify fails without a certificate chain + err = VerifyEmbeddedSCT(context.Background(), chain[:1]) + if err == nil || err.Error() != "certificate chain must contain at least a certificate and its issuer" { + t.Fatalf("expected error verifying SCT without chain: %v", err) + } + + writePubKey(t, testdata.LogPublicKeyPEM) + err = VerifyEmbeddedSCT(context.Background(), chain) + if err != nil { + t.Fatalf("unexpected error verifying embedded SCT: %v", err) + } +} + +// toAddChainResponse converts an SCT to a response struct, the expected structure for detached SCTs +func toAddChainResponse(sct *ct.SignedCertificateTimestamp) (*ct.AddChainResponse, error) { + sig, err := tls.Marshal(sct.Signature) + if err != nil { + return nil, fmt.Errorf("failed to marshal signature: %w", err) + } + addChainResp := &ct.AddChainResponse{ + SCTVersion: sct.SCTVersion, + Timestamp: sct.Timestamp, + Extensions: base64.StdEncoding.EncodeToString(sct.Extensions), + ID: sct.LogID.KeyID[:], + Signature: sig, + } + + return addChainResp, nil +} + +// writePubKey writes the SCT verification key to disk, since there is not a TUF +// test setup +func writePubKey(t *testing.T, keyPEM string) { + t.Helper() + + tmpPrivFile, err := os.CreateTemp(t.TempDir(), "cosign_verify_sct_*.key") + if err != nil { + t.Fatalf("failed to create temp key file: %v", err) + } + t.Cleanup(func() { tmpPrivFile.Close() }) + if _, err := tmpPrivFile.Write([]byte(keyPEM)); err != nil { + t.Fatalf("failed to write key file: %v", err) + } + os.Setenv("SIGSTORE_CT_LOG_PUBLIC_KEY_FILE", tmpPrivFile.Name()) + t.Cleanup(func() { os.Unsetenv("SIGSTORE_CT_LOG_PUBLIC_KEY_FILE") }) +} diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/ctutil/ctutil.go b/cmd/cosign/cli/fulcio/fulcioverifier/ctutil/ctutil.go new file mode 100644 index 00000000000..a764b8e32f8 --- /dev/null +++ b/cmd/cosign/cli/fulcio/fulcioverifier/ctutil/ctutil.go @@ -0,0 +1,224 @@ +// Copyright 2018 Google LLC. All Rights Reserved. +// +// 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 copied from +// https://github.com/google/certificate-transparency-go/blob/master/ctutil/ctutil.go + +// Package ctutil contains utilities for Certificate Transparency. +package ctutil + +import ( + "bytes" + "crypto" + "crypto/sha256" + "encoding/base64" + "errors" + "fmt" + + ct "github.com/google/certificate-transparency-go" + "github.com/google/certificate-transparency-go/tls" + "github.com/google/certificate-transparency-go/x509" +) + +var emptyHash = [sha256.Size]byte{} + +// LeafHashB64 does as LeafHash does, but returns the leaf hash base64-encoded. +// The base64-encoded leaf hash returned by B64LeafHash can be used with the +// get-proof-by-hash API endpoint of Certificate Transparency Logs. +func LeafHashB64(chain []*x509.Certificate, sct *ct.SignedCertificateTimestamp, embedded bool) (string, error) { + hash, err := LeafHash(chain, sct, embedded) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(hash[:]), nil +} + +// LeafHash calculates the leaf hash of the certificate or precertificate at +// chain[0] that sct was issued for. +// +// sct is required because the SCT timestamp is used to calculate the leaf hash. +// Leaf hashes are unique to (pre)certificate-SCT pairs. +// +// This function can be used with three different types of leaf certificate: +// - X.509 Certificate: +// If using this function to calculate the leaf hash for a normal X.509 +// certificate then it is enough to just provide the end entity +// certificate in chain. This case assumes that the SCT being provided is +// not embedded within the leaf certificate provided, i.e. the certificate +// is what was submitted to the Certificate Transparency Log in order to +// obtain the SCT. For this case, set embedded to false. +// - Precertificate: +// If using this function to calculate the leaf hash for a precertificate +// then the issuing certificate must also be provided in chain. The +// precertificate should be at chain[0], and its issuer at chain[1]. For +// this case, set embedded to false. +// - X.509 Certificate containing the SCT embedded within it: +// If using this function to calculate the leaf hash for a certificate +// where the SCT provided is embedded within the certificate you +// are providing at chain[0], set embedded to true. LeafHash will +// calculate the leaf hash by building the corresponding precertificate. +// LeafHash will return an error if the provided SCT cannot be found +// embedded within chain[0]. As with the precertificate case, the issuing +// certificate must also be provided in chain. The certificate containing +// the embedded SCT should be at chain[0], and its issuer at chain[1]. +// +// Note: LeafHash doesn't check that the provided SCT verifies for the given +// chain. It simply calculates what the leaf hash would be for the given +// (pre)certificate-SCT pair. +func LeafHash(chain []*x509.Certificate, sct *ct.SignedCertificateTimestamp, embedded bool) ([sha256.Size]byte, error) { + leaf, err := createLeaf(chain, sct, embedded) + if err != nil { + return emptyHash, err + } + return ct.LeafHashForLeaf(leaf) +} + +// VerifySCT takes the public key of a Certificate Transparency Log, a +// certificate chain, and an SCT and verifies whether the SCT is a valid SCT for +// the certificate at chain[0], signed by the Log that the public key belongs +// to. If the SCT does not verify, an error will be returned. +// +// This function can be used with three different types of leaf certificate: +// - X.509 Certificate: +// If using this function to verify an SCT for a normal X.509 certificate +// then it is enough to just provide the end entity certificate in chain. +// This case assumes that the SCT being provided is not embedded within +// the leaf certificate provided, i.e. the certificate is what was +// submitted to the Certificate Transparency Log in order to obtain the +// SCT. For this case, set embedded to false. +// - Precertificate: +// If using this function to verify an SCT for a precertificate then the +// issuing certificate must also be provided in chain. The precertificate +// should be at chain[0], and its issuer at chain[1]. For this case, set +// embedded to false. +// - X.509 Certificate containing the SCT embedded within it: +// If the SCT you wish to verify is embedded within the certificate you +// are providing at chain[0], set embedded to true. VerifySCT will +// verify the provided SCT by building the corresponding precertificate. +// VerifySCT will return an error if the provided SCT cannot be found +// embedded within chain[0]. As with the precertificate case, the issuing +// certificate must also be provided in chain. The certificate containing +// the embedded SCT should be at chain[0], and its issuer at chain[1]. +func VerifySCT(pubKey crypto.PublicKey, chain []*x509.Certificate, sct *ct.SignedCertificateTimestamp, embedded bool) error { + s, err := ct.NewSignatureVerifier(pubKey) + if err != nil { + return fmt.Errorf("error creating signature verifier: %w", err) + } + + return VerifySCTWithVerifier(s, chain, sct, embedded) +} + +// VerifySCTWithVerifier takes a ct.SignatureVerifier, a certificate chain, and +// an SCT and verifies whether the SCT is a valid SCT for the certificate at +// chain[0], signed by the Log whose public key was used to set up the +// ct.SignatureVerifier. If the SCT does not verify, an error will be returned. +// +// This function can be used with three different types of leaf certificate: +// - X.509 Certificate: +// If using this function to verify an SCT for a normal X.509 certificate +// then it is enough to just provide the end entity certificate in chain. +// This case assumes that the SCT being provided is not embedded within +// the leaf certificate provided, i.e. the certificate is what was +// submitted to the Certificate Transparency Log in order to obtain the +// SCT. For this case, set embedded to false. +// - Precertificate: +// If using this function to verify an SCT for a precertificate then the +// issuing certificate must also be provided in chain. The precertificate +// should be at chain[0], and its issuer at chain[1]. For this case, set +// embedded to false. +// - X.509 Certificate containing the SCT embedded within it: +// If the SCT you wish to verify is embedded within the certificate you +// are providing at chain[0], set embedded to true. VerifySCT will +// verify the provided SCT by building the corresponding precertificate. +// VerifySCT will return an error if the provided SCT cannot be found +// embedded within chain[0]. As with the precertificate case, the issuing +// certificate must also be provided in chain. The certificate containing +// the embedded SCT should be at chain[0], and its issuer at chain[1]. +func VerifySCTWithVerifier(sv *ct.SignatureVerifier, chain []*x509.Certificate, sct *ct.SignedCertificateTimestamp, embedded bool) error { + if sv == nil { + return errors.New("ct.SignatureVerifier is nil") + } + + leaf, err := createLeaf(chain, sct, embedded) + if err != nil { + return err + } + + return sv.VerifySCTSignature(*sct, ct.LogEntry{Leaf: *leaf}) +} + +func createLeaf(chain []*x509.Certificate, sct *ct.SignedCertificateTimestamp, embedded bool) (*ct.MerkleTreeLeaf, error) { + if len(chain) == 0 { + return nil, errors.New("chain is empty") + } + if sct == nil { + return nil, errors.New("sct is nil") + } + + if embedded { + sctPresent, err := ContainsSCT(chain[0], sct) + if err != nil { + return nil, fmt.Errorf("error checking for SCT in leaf certificate: %w", err) + } + if !sctPresent { + return nil, errors.New("SCT provided is not embedded within leaf certificate") + } + } + + certType := ct.X509LogEntryType + if chain[0].IsPrecertificate() || embedded { + certType = ct.PrecertLogEntryType + } + + var leaf *ct.MerkleTreeLeaf + var err error + if embedded { + leaf, err = ct.MerkleTreeLeafForEmbeddedSCT(chain, sct.Timestamp) + } else { + leaf, err = ct.MerkleTreeLeafFromChain(chain, certType, sct.Timestamp) + } + if err != nil { + return nil, fmt.Errorf("error creating MerkleTreeLeaf: %w", err) + } + return leaf, nil +} + +// ContainsSCT checks to see whether the given SCT is embedded within the given +// certificate. +func ContainsSCT(cert *x509.Certificate, sct *ct.SignedCertificateTimestamp) (bool, error) { + if cert == nil || sct == nil { + return false, nil + } + + sctBytes, err := tls.Marshal(*sct) + if err != nil { + return false, fmt.Errorf("error tls.Marshalling SCT: %w", err) + } + for _, s := range cert.SCTList.SCTList { + if bytes.Equal(sctBytes, s.Val) { + return true, nil + } + } + return false, nil +} + +// GetCTLogID takes the key manager for a log and returns the LogID. (see RFC 6962 S3.2) +// In CT V1 the log id is a hash of the public key. +func GetCTLogID(pk crypto.PublicKey) ([sha256.Size]byte, error) { + pubBytes, err := x509.MarshalPKIXPublicKey(pk) + if err != nil { + return [sha256.Size]byte{}, err + } + return sha256.Sum256(pubBytes), nil +} diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/ctutil/ctutil_test.go b/cmd/cosign/cli/fulcio/fulcioverifier/ctutil/ctutil_test.go new file mode 100644 index 00000000000..8476d866d0c --- /dev/null +++ b/cmd/cosign/cli/fulcio/fulcioverifier/ctutil/ctutil_test.go @@ -0,0 +1,385 @@ +// Copyright 2018 Google LLC. All Rights Reserved. +// +// 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 ctutil + +import ( + "crypto/x509" + "encoding/base64" + "encoding/pem" + "testing" + + ct "github.com/google/certificate-transparency-go" + "github.com/google/certificate-transparency-go/testdata" + "github.com/google/certificate-transparency-go/tls" + ttestdata "github.com/google/certificate-transparency-go/trillian/testdata" + "github.com/google/certificate-transparency-go/x509util" +) + +var ( + demoLogID = [32]byte{19, 56, 222, 93, 229, 36, 102, 128, 227, 214, 3, 121, 93, 175, 126, 236, 97, 217, 34, 32, 40, 233, 98, 27, 46, 179, 164, 251, 84, 10, 60, 57} +) + +func TestLeafHash(t *testing.T) { + tests := []struct { + desc string + chainPEM string + sct []byte + embedded bool + want string + }{ + { + desc: "cert", + chainPEM: testdata.TestCertPEM + testdata.CACertPEM, + sct: testdata.TestCertProof, + want: testdata.TestCertB64LeafHash, + }, + { + desc: "precert", + chainPEM: testdata.TestPreCertPEM + testdata.CACertPEM, + sct: testdata.TestPreCertProof, + want: testdata.TestPreCertB64LeafHash, + }, + { + desc: "cert with embedded SCT", + chainPEM: testdata.TestEmbeddedCertPEM + testdata.CACertPEM, + sct: testdata.TestPreCertProof, + embedded: true, + want: testdata.TestPreCertB64LeafHash, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + // Parse chain + chain, err := x509util.CertificatesFromPEM([]byte(test.chainPEM)) + if err != nil { + t.Fatalf("error parsing certificate chain: %s", err) + } + + // Parse SCT + var sct ct.SignedCertificateTimestamp + if _, err = tls.Unmarshal(test.sct, &sct); err != nil { + t.Fatalf("error tls-unmarshalling sct: %s", err) + } + + // Test LeafHash() + wantSl, err := base64.StdEncoding.DecodeString(test.want) + if err != nil { + t.Fatalf("error base64-decoding leaf hash %q: %s", test.want, err) + } + var want [32]byte + copy(want[:], wantSl) + + got, err := LeafHash(chain, &sct, test.embedded) + if got != want || err != nil { + t.Errorf("LeafHash(_,_) = %v, %v, want %v, nil", got, err, want) + } + + // Test LeafHashB64() + gotB64, err := LeafHashB64(chain, &sct, test.embedded) + if gotB64 != test.want || err != nil { + t.Errorf("LeafHashB64(_,_) = %v, %v, want %v, nil", gotB64, err, test.want) + } + }) + } +} + +func TestLeafHashErrors(t *testing.T) { + tests := []struct { + desc string + chainPEM string + sct []byte + embedded bool + }{ + { + desc: "empty chain", + chainPEM: "", + sct: testdata.TestCertProof, + }, + { + desc: "nil SCT", + chainPEM: testdata.TestCertPEM + testdata.CACertPEM, + sct: nil, + }, + { + desc: "no SCTs embedded in cert, embedded true", + chainPEM: testdata.TestCertPEM + testdata.CACertPEM, + sct: testdata.TestInvalidProof, + embedded: true, + }, + { + desc: "cert contains embedded SCTs, but not the SCT provided", + chainPEM: testdata.TestEmbeddedCertPEM + testdata.CACertPEM, + sct: testdata.TestInvalidProof, + embedded: true, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + // Parse chain + chain, err := x509util.CertificatesFromPEM([]byte(test.chainPEM)) + if err != nil { + t.Fatalf("error parsing certificate chain: %s", err) + } + + // Parse SCT + var sct *ct.SignedCertificateTimestamp + if test.sct != nil { + sct = &ct.SignedCertificateTimestamp{} + if _, err = tls.Unmarshal(test.sct, sct); err != nil { + t.Fatalf("error tls-unmarshalling sct: %s", err) + } + } + + // Test LeafHash() + got, err := LeafHash(chain, sct, test.embedded) + if got != emptyHash || err == nil { + t.Errorf("LeafHash(_,_) = %s, %v, want %v, error", got, err, emptyHash) + } + + // Test LeafHashB64() + gotB64, err := LeafHashB64(chain, sct, test.embedded) + if gotB64 != "" || err == nil { + t.Errorf("LeafHashB64(_,_) = %s, %v, want \"\", error", gotB64, err) + } + }) + } +} + +func TestVerifySCT(t *testing.T) { + tests := []struct { + desc string + chainPEM string + sct []byte + embedded bool + wantErr bool + }{ + { + desc: "cert", + chainPEM: testdata.TestCertPEM + testdata.CACertPEM, + sct: testdata.TestCertProof, + }, + { + desc: "precert", + chainPEM: testdata.TestPreCertPEM + testdata.CACertPEM, + sct: testdata.TestPreCertProof, + }, + { + desc: "invalid SCT", + chainPEM: testdata.TestPreCertPEM + testdata.CACertPEM, + sct: testdata.TestCertProof, + wantErr: true, + }, + { + desc: "cert with embedded SCT", + chainPEM: testdata.TestEmbeddedCertPEM + testdata.CACertPEM, + sct: testdata.TestPreCertProof, + embedded: true, + }, + { + desc: "cert with invalid embedded SCT", + chainPEM: testdata.TestInvalidEmbeddedCertPEM + testdata.CACertPEM, + sct: testdata.TestInvalidProof, + embedded: true, + wantErr: true, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + // Parse chain + chain, err := x509util.CertificatesFromPEM([]byte(test.chainPEM)) + if err != nil { + t.Fatalf("error parsing certificate chain: %s", err) + } + + // Parse SCT + var sct ct.SignedCertificateTimestamp + if _, err = tls.Unmarshal(test.sct, &sct); err != nil { + t.Fatalf("error tls-unmarshalling sct: %s", err) + } + + // Test VerifySCT() + pk, err := ct.PublicKeyFromB64(testdata.LogPublicKeyB64) + if err != nil { + t.Errorf("error parsing public key: %s", err) + } + + err = VerifySCT(pk, chain, &sct, test.embedded) + if gotErr := err != nil; gotErr != test.wantErr { + t.Errorf("VerifySCT(_,_,_, %t) = %v, want error? %t", test.embedded, err, test.wantErr) + } + }) + } +} + +func TestVerifySCTWithVerifier(t *testing.T) { + // Parse public key + pk, err := ct.PublicKeyFromB64(testdata.LogPublicKeyB64) + if err != nil { + t.Errorf("error parsing public key: %s", err) + } + + // Create signature verifier + sv, err := ct.NewSignatureVerifier(pk) + if err != nil { + t.Errorf("couldn't create signature verifier: %s", err) + } + + tests := []struct { + desc string + sv *ct.SignatureVerifier + chainPEM string + sct []byte + embedded bool + wantErr bool + }{ + { + desc: "nil signature verifier", + sv: nil, + chainPEM: testdata.TestCertPEM + testdata.CACertPEM, + sct: testdata.TestCertProof, + wantErr: true, + }, + { + desc: "cert", + sv: sv, + chainPEM: testdata.TestCertPEM + testdata.CACertPEM, + sct: testdata.TestCertProof, + }, + { + desc: "precert", + sv: sv, + chainPEM: testdata.TestPreCertPEM + testdata.CACertPEM, + sct: testdata.TestPreCertProof, + }, + { + desc: "invalid SCT", + sv: sv, + chainPEM: testdata.TestPreCertPEM + testdata.CACertPEM, + sct: testdata.TestCertProof, + wantErr: true, + }, + { + desc: "cert with embedded SCT", + sv: sv, + chainPEM: testdata.TestEmbeddedCertPEM + testdata.CACertPEM, + sct: testdata.TestPreCertProof, + embedded: true, + }, + { + desc: "cert with invalid embedded SCT", + sv: sv, + chainPEM: testdata.TestInvalidEmbeddedCertPEM + testdata.CACertPEM, + sct: testdata.TestInvalidProof, + embedded: true, + wantErr: true, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + // Parse chain + chain, err := x509util.CertificatesFromPEM([]byte(test.chainPEM)) + if err != nil { + t.Fatalf("error parsing certificate chain: %s", err) + } + + // Parse SCT + var sct ct.SignedCertificateTimestamp + if _, err = tls.Unmarshal(test.sct, &sct); err != nil { + t.Fatalf("error tls-unmarshalling sct: %s", err) + } + + // Test VerifySCTWithVerifier() + err = VerifySCTWithVerifier(test.sv, chain, &sct, test.embedded) + if gotErr := err != nil; gotErr != test.wantErr { + t.Errorf("VerifySCT(_,_,_, %t) = %v, want error? %t", test.embedded, err, test.wantErr) + } + }) + } +} + +func TestContainsSCT(t *testing.T) { + tests := []struct { + desc string + certPEM string + sct []byte + want bool + }{ + { + desc: "cert doesn't contain any SCTs", + certPEM: testdata.TestCertPEM, + sct: testdata.TestPreCertProof, + want: false, + }, + { + desc: "cert contains SCT but not specified SCT", + certPEM: testdata.TestEmbeddedCertPEM, + sct: testdata.TestInvalidProof, + want: false, + }, + { + desc: "cert contains SCT", + certPEM: testdata.TestEmbeddedCertPEM, + sct: testdata.TestPreCertProof, + want: true, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + // Parse cert + cert, err := x509util.CertificateFromPEM([]byte(test.certPEM)) + if err != nil { + t.Fatalf("error parsing certificate: %s", err) + } + + // Parse SCT + var sct ct.SignedCertificateTimestamp + if _, err = tls.Unmarshal(test.sct, &sct); err != nil { + t.Fatalf("error tls-unmarshalling sct: %s", err) + } + + // Test ContainsSCT() + got, err := ContainsSCT(cert, &sct) + if err != nil { + t.Fatalf("ContainsSCT(_,_) = false, %s, want no error", err) + } + + if got != test.want { + t.Errorf("ContainsSCT(_,_) = %t, nil, want %t, nil", got, test.want) + } + }) + } +} + +func TestGetCTLogID(t *testing.T) { + block, _ := pem.Decode([]byte(ttestdata.DemoPublicKey)) + pk, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + t.Fatalf("unexpected error loading public key: %v", err) + } + + got, err := GetCTLogID(pk) + if err != nil { + t.Fatalf("error getting logid: %v", err) + } + + if want := demoLogID; got != want { + t.Errorf("logID: \n%v want \n%v", got, want) + } +} diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go b/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go index 819585a9985..3687f5db01f 100644 --- a/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go +++ b/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go @@ -17,103 +17,16 @@ package fulcioverifier import ( "context" - "crypto" - "crypto/x509" - "encoding/json" - "encoding/pem" "fmt" "os" - ct "github.com/google/certificate-transparency-go" - "github.com/google/certificate-transparency-go/ctutil" - ctx509 "github.com/google/certificate-transparency-go/x509" - "github.com/google/certificate-transparency-go/x509util" "github.com/pkg/errors" "github.com/sigstore/cosign/cmd/cosign/cli/fulcio" - "github.com/sigstore/cosign/pkg/cosign" - "github.com/sigstore/cosign/pkg/cosign/tuf" + "github.com/sigstore/cosign/cmd/cosign/cli/fulcio/fulcioverifier/ctl" "github.com/sigstore/fulcio/pkg/api" ) -// This is the CT log public key target name -var ctPublicKeyStr = `ctfe.pub` - -// Setting this env variable will over ride what is used to validate -// the SCT coming back from Fulcio. -const altCTLogPublicKeyLocation = "SIGSTORE_CT_LOG_PUBLIC_KEY_FILE" - -// verifySCT verifies the SCT against the Fulcio CT log public key. -// By default this comes from TUF, but you can override this (for test) -// purposes by using an env variable `SIGSTORE_CT_LOG_PUBLIC_KEY_FILE`. If using -// an alternate, the file can be PEM, or DER format. -// -// The SCT is a `Signed Certificate Timestamp`, which promises that -// the certificate issued by Fulcio was also added to the public CT log within -// some defined time period -func verifySCT(ctx context.Context, certPEM, rawSCT []byte) error { - pubKeys := make(map[crypto.PublicKey]tuf.StatusKind) - rootEnv := os.Getenv(altCTLogPublicKeyLocation) - if rootEnv == "" { - tufClient, err := tuf.NewFromEnv(ctx) - if err != nil { - return err - } - defer tufClient.Close() - - targets, err := tufClient.GetTargetsByMeta(tuf.CTFE, []string{ctPublicKeyStr}) - if err != nil { - return err - } - for _, t := range targets { - ctPub, err := cosign.PemToECDSAKey(t.Target) - if err != nil { - return errors.Wrap(err, "converting Public CT to ECDSAKey") - } - pubKeys[ctPub] = t.Status - } - } else { - fmt.Fprintf(os.Stderr, "**Warning** Using a non-standard public key for verifying SCT: %s\n", rootEnv) - raw, err := os.ReadFile(rootEnv) - if err != nil { - return errors.Wrap(err, "error reading alternate public key file") - } - pubKey, err := getAlternatePublicKey(raw) - if err != nil { - return errors.Wrap(err, "error parsing alternate public key from the file") - } - pubKeys[pubKey] = tuf.Active - } - if len(pubKeys) == 0 { - return errors.New("none of the CTFE keys have been found") - } - cert, err := x509util.CertificateFromPEM(certPEM) - if err != nil { - return err - } - var addChainResp ct.AddChainResponse - if err := json.Unmarshal(rawSCT, &addChainResp); err != nil { - return errors.Wrap(err, "unmarshal") - } - sct, err := addChainResp.ToSignedCertificateTimestamp() - if err != nil { - return err - } - var verifySctErr error - for pubKey, status := range pubKeys { - verifySctErr = ctutil.VerifySCT(pubKey, []*ctx509.Certificate{cert}, sct, false) - // Exit after successful verification of the SCT - if verifySctErr == nil { - if status != tuf.Active { - fmt.Fprintf(os.Stderr, "**Info** Successfully verified SCT using an expired verification key\n") - } - return nil - } - } - // Return the last error that occurred during verification. - return verifySctErr -} - func NewSigner(ctx context.Context, idToken, oidcIssuer, oidcClientID, oidcClientSecret, oidcRedirectURL string, fClient api.Client) (*fulcio.Signer, error) { fs, err := fulcio.NewSigner(ctx, idToken, oidcIssuer, oidcClientID, oidcClientSecret, oidcRedirectURL, fClient) if err != nil { @@ -121,35 +34,10 @@ func NewSigner(ctx context.Context, idToken, oidcIssuer, oidcClientID, oidcClien } // verify the sct - if err := verifySCT(ctx, fs.Cert, fs.SCT); err != nil { + if err := ctl.VerifySCT(ctx, fs.Cert, fs.Chain, fs.SCT); err != nil { return nil, errors.Wrap(err, "verifying SCT") } fmt.Fprintln(os.Stderr, "Successfully verified SCT...") return fs, nil } - -// Given a byte array, try to construct a public key from it. -// Will try first to see if it's PEM formatted, if not, then it will -// try to parse it as der publics, and failing that -func getAlternatePublicKey(in []byte) (crypto.PublicKey, error) { - var pubKey crypto.PublicKey - var err error - var derBytes []byte - pemBlock, _ := pem.Decode(in) - if pemBlock == nil { - fmt.Fprintf(os.Stderr, "Failed to decode non-standard public key for verifying SCT using PEM decode, trying as DER") - derBytes = in - } else { - derBytes = pemBlock.Bytes - } - pubKey, err = x509.ParsePKIXPublicKey(derBytes) - if err != nil { - // Try using the PKCS1 before giving up. - pubKey, err = x509.ParsePKCS1PublicKey(derBytes) - if err != nil { - return nil, errors.Wrap(err, "failed to parse alternate public key") - } - } - return pubKey, nil -} diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier_test.go b/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier_test.go deleted file mode 100644 index a4588e7ace5..00000000000 --- a/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier_test.go +++ /dev/null @@ -1,73 +0,0 @@ -// -// 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 fulcioverifier - -import ( - "fmt" - "os" - "reflect" - "strings" - "testing" -) - -func TestGetAlternatePublicKey(t *testing.T) { - wd, err := os.Getwd() - if err != nil { - t.Fatalf("Failed to get cwd: %v", err) - } - tests := []struct { - file string - wantErrSub string - wantType string - }{ - {file: "garbage-there-are-limits", wantErrSub: "failed to parse"}, - // Testflume 2021 from here, https://letsencrypt.org/docs/ct-logs/ - {file: "letsencrypt-testflume-2021", wantType: "*ecdsa.PublicKey"}, - // This needs to be parsed with the pkcs1, pkix won't do. - {file: "rsa", wantType: "*rsa.PublicKey"}, - // This works with pkix, from: - // https://www.gstatic.com/ct/log_list/v2/log_list_pubkey.pem - {file: "google", wantType: "*rsa.PublicKey"}, - } - for _, tc := range tests { - filepath := fmt.Sprintf("%s/testdata/%s", wd, tc.file) - bytes, err := os.ReadFile(filepath) - if err != nil { - t.Fatalf("Failed to read testfile %s : %v", tc.file, err) - } - got, err := getAlternatePublicKey(bytes) - switch { - case err == nil && tc.wantErrSub != "": - t.Errorf("Wanted Error for %s but got none", tc.file) - case err != nil && tc.wantErrSub == "": - t.Errorf("Did not want error for %s but got: %v", tc.file, err) - case err != nil && tc.wantErrSub != "": - if !strings.Contains(err.Error(), tc.wantErrSub) { - t.Errorf("Unexpected error for %s: %s wanted to contain: %s", tc.file, err.Error(), tc.wantErrSub) - } - } - switch { - case got == nil && tc.wantType != "": - t.Errorf("Wanted public key for %s but got none", tc.file) - case got != nil && tc.wantType == "": - t.Errorf("Did not want error for %s but got: %v", tc.file, err) - case got != nil && tc.wantType != "": - if reflect.TypeOf(got).String() != tc.wantType { - t.Errorf("Unexpected type for %s: %+T wanted: %s", tc.file, got, tc.wantType) - } - } - } -} diff --git a/cmd/cosign/cli/manifest.go b/cmd/cosign/cli/manifest.go index 07c213cbe58..5854065f43e 100644 --- a/cmd/cosign/cli/manifest.go +++ b/cmd/cosign/cli/manifest.go @@ -87,6 +87,7 @@ against the transparency log.`, CertEmail: o.CertVerify.CertEmail, CertOidcIssuer: o.CertVerify.CertOidcIssuer, CertChain: o.CertVerify.CertChain, + EnforceSCT: o.CertVerify.EnforceSCT, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, Output: o.Output, diff --git a/cmd/cosign/cli/options/certificate.go b/cmd/cosign/cli/options/certificate.go index f801f4f801a..615842c810e 100644 --- a/cmd/cosign/cli/options/certificate.go +++ b/cmd/cosign/cli/options/certificate.go @@ -24,6 +24,7 @@ type CertVerifyOptions struct { CertEmail string CertOidcIssuer string CertChain string + EnforceSCT bool } var _ Interface = (*RekorOptions)(nil) @@ -44,4 +45,8 @@ func (o *CertVerifyOptions) AddFlags(cmd *cobra.Command) { "when building the certificate chain for the signing certificate. "+ "Must start with the parent intermediate CA certificate of the "+ "signing certificate and end with the root certificate") + + cmd.Flags().BoolVar(&o.EnforceSCT, "enforce-sct", false, + "whether to enforce that a certificate contain an embedded SCT, a proof of "+ + "inclusion in a certificate transparency log") } diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go index 4f3a12843ed..44da104baf8 100644 --- a/cmd/cosign/cli/sign/sign.go +++ b/cmd/cosign/cli/sign/sign.go @@ -32,6 +32,7 @@ import ( "github.com/sigstore/cosign/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/cmd/cosign/cli/fulcio/fulcioverifier" + "github.com/sigstore/cosign/cmd/cosign/cli/fulcio/fulcioverifier/ctl" "github.com/sigstore/cosign/cmd/cosign/cli/options" "github.com/sigstore/cosign/cmd/cosign/cli/rekor" icos "github.com/sigstore/cosign/internal/pkg/cosign" @@ -414,9 +415,22 @@ func signerFromKeyRef(ctx context.Context, certPath, certChainPath, keyRef strin for _, c := range certChain[:len(certChain)-1] { subPool.AddCert(c) } - if err := cosign.TrustedCert(leafCert, rootPool, subPool); err != nil { + if _, err := cosign.TrustedCert(leafCert, rootPool, subPool); err != nil { return nil, errors.Wrap(err, "unable to validate certificate chain") } + // Verify SCT if present in the leaf certificate. + contains, err := ctl.ContainsSCT(leafCert.Raw) + if err != nil { + return nil, err + } + if contains { + var chain []*x509.Certificate + chain = append(chain, leafCert) + chain = append(chain, certChain...) + if err := ctl.VerifyEmbeddedSCT(context.Background(), chain); err != nil { + return nil, err + } + } certSigner.Chain = certChainBytes return certSigner, nil diff --git a/cmd/cosign/cli/verify.go b/cmd/cosign/cli/verify.go index 680e30239a1..5471da9a1b0 100644 --- a/cmd/cosign/cli/verify.go +++ b/cmd/cosign/cli/verify.go @@ -97,6 +97,7 @@ against the transparency log.`, CertEmail: o.CertVerify.CertEmail, CertOidcIssuer: o.CertVerify.CertOidcIssuer, CertChain: o.CertVerify.CertChain, + EnforceSCT: o.CertVerify.EnforceSCT, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, Output: o.Output, @@ -174,6 +175,7 @@ against the transparency log.`, CertEmail: o.CertVerify.CertEmail, CertOidcIssuer: o.CertVerify.CertOidcIssuer, CertChain: o.CertVerify.CertChain, + EnforceSCT: o.CertVerify.EnforceSCT, KeyRef: o.Key, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, @@ -252,7 +254,8 @@ The blob may be specified as a path to a file or - for stdin.`, BundlePath: o.BundlePath, } if err := verify.VerifyBlobCmd(cmd.Context(), ko, o.CertVerify.Cert, - o.CertVerify.CertEmail, o.CertVerify.CertOidcIssuer, o.CertVerify.CertChain, o.Signature, args[0]); err != nil { + o.CertVerify.CertEmail, o.CertVerify.CertOidcIssuer, o.CertVerify.CertChain, + o.Signature, args[0], o.CertVerify.EnforceSCT); err != nil { return errors.Wrapf(err, "verifying blob %s", args) } return nil diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index 79cc1325c6d..e55381e0b1d 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -54,6 +54,7 @@ type VerifyCommand struct { CertEmail string CertOidcIssuer string CertChain string + EnforceSCT bool Sk bool Slot string Output string @@ -95,6 +96,7 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { RegistryClientOpts: ociremoteOpts, CertEmail: c.CertEmail, CertOidcIssuer: c.CertOidcIssuer, + EnforceSCT: c.EnforceSCT, SignatureRef: c.SignatureRef, } if c.CheckClaims { diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go index fb4cb7713b0..8ab95fd7dac 100644 --- a/cmd/cosign/cli/verify/verify_attestation.go +++ b/cmd/cosign/cli/verify/verify_attestation.go @@ -50,6 +50,7 @@ type VerifyAttestationCommand struct { CertEmail string CertOidcIssuer string CertChain string + EnforceSCT bool Sk bool Slot string Output string @@ -77,6 +78,7 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e RegistryClientOpts: ociremoteOpts, CertEmail: c.CertEmail, CertOidcIssuer: c.CertOidcIssuer, + EnforceSCT: c.EnforceSCT, } if c.CheckClaims { co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index d0741f13044..0341ed0a025 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -61,7 +61,8 @@ func isb64(data []byte) bool { } // nolint -func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, certOidcIssuer, certChain, sigRef, blobRef string) error { +func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, + certOidcIssuer, certChain, sigRef, blobRef string, enforceSCT bool) error { var verifier signature.Verifier var cert *x509.Certificate @@ -119,6 +120,7 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, cer co := &cosign.CheckOpts{ CertEmail: certEmail, CertOidcIssuer: certOidcIssuer, + EnforceSCT: enforceSCT, } verifier, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co) if err != nil { @@ -162,7 +164,7 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, cer if len(uuids) == 0 { return errors.New("could not find a tlog entry for provided blob") } - return verifySigByUUID(ctx, ko, rClient, certEmail, certOidcIssuer, sig, b64sig, uuids, blobBytes) + return verifySigByUUID(ctx, ko, rClient, certEmail, certOidcIssuer, sig, b64sig, uuids, blobBytes, enforceSCT) } // Use the DSSE verifier if the payload is a DSSE with the In-Toto format. @@ -184,7 +186,8 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, cer return nil } -func verifySigByUUID(ctx context.Context, ko sign.KeyOpts, rClient *client.Rekor, certEmail, certOidcIssuer, sig, b64sig string, uuids []string, blobBytes []byte) error { +func verifySigByUUID(ctx context.Context, ko sign.KeyOpts, rClient *client.Rekor, certEmail, certOidcIssuer, sig, b64sig string, + uuids []string, blobBytes []byte, enforceSCT bool) error { var validSigExists bool for _, u := range uuids { tlogEntry, err := cosign.GetTlogEntry(ctx, rClient, u) @@ -202,6 +205,7 @@ func verifySigByUUID(ctx context.Context, ko sign.KeyOpts, rClient *client.Rekor IntermediateCerts: fulcio.GetIntermediates(), CertEmail: certEmail, CertOidcIssuer: certOidcIssuer, + EnforceSCT: enforceSCT, } cert := certs[0] verifier, err := cosign.ValidateAndUnpackCert(cert, co) diff --git a/doc/cosign_dockerfile_verify.md b/doc/cosign_dockerfile_verify.md index bf0ae43bf8d..fa6ec41bfe3 100644 --- a/doc/cosign_dockerfile_verify.md +++ b/doc/cosign_dockerfile_verify.md @@ -62,6 +62,7 @@ cosign dockerfile verify [flags] --cert-email string the email expected in a valid Fulcio certificate --cert-oidc-issuer string the OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth --check-claims whether to check the claims found (default true) + --enforce-sct whether to enforce that a certificate contain an embedded SCT, a proof of inclusion in a certificate transparency log -h, --help help for verify --k8s-keychain whether to use the kubernetes keychain instead of the default keychain (supports workload identity). --key string path to the public key file, KMS URI or Kubernetes Secret diff --git a/doc/cosign_manifest_verify.md b/doc/cosign_manifest_verify.md index a78e08bfb59..81f314d83f8 100644 --- a/doc/cosign_manifest_verify.md +++ b/doc/cosign_manifest_verify.md @@ -56,6 +56,7 @@ cosign manifest verify [flags] --cert-email string the email expected in a valid Fulcio certificate --cert-oidc-issuer string the OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth --check-claims whether to check the claims found (default true) + --enforce-sct whether to enforce that a certificate contain an embedded SCT, a proof of inclusion in a certificate transparency log -h, --help help for verify --k8s-keychain whether to use the kubernetes keychain instead of the default keychain (supports workload identity). --key string path to the public key file, KMS URI or Kubernetes Secret diff --git a/doc/cosign_verify-attestation.md b/doc/cosign_verify-attestation.md index 90c7e7a4f09..65ca6994699 100644 --- a/doc/cosign_verify-attestation.md +++ b/doc/cosign_verify-attestation.md @@ -66,6 +66,7 @@ cosign verify-attestation [flags] --cert-email string the email expected in a valid Fulcio certificate --cert-oidc-issuer string the OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth --check-claims whether to check the claims found (default true) + --enforce-sct whether to enforce that a certificate contain an embedded SCT, a proof of inclusion in a certificate transparency log -h, --help help for verify-attestation --k8s-keychain whether to use the kubernetes keychain instead of the default keychain (supports workload identity). --key string path to the public key file, KMS URI or Kubernetes Secret diff --git a/doc/cosign_verify-blob.md b/doc/cosign_verify-blob.md index cb02172985a..894e46afde6 100644 --- a/doc/cosign_verify-blob.md +++ b/doc/cosign_verify-blob.md @@ -68,6 +68,7 @@ cosign verify-blob [flags] --cert-chain string path to a list of CA certificates in PEM format which will be needed when building the certificate chain for the signing certificate. Must start with the parent intermediate CA certificate of the signing certificate and end with the root certificate --cert-email string the email expected in a valid Fulcio certificate --cert-oidc-issuer string the OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth + --enforce-sct whether to enforce that a certificate contain an embedded SCT, a proof of inclusion in a certificate transparency log -h, --help help for verify-blob --k8s-keychain whether to use the kubernetes keychain instead of the default keychain (supports workload identity). --key string path to the public key file, KMS URI or Kubernetes Secret diff --git a/doc/cosign_verify.md b/doc/cosign_verify.md index c27f4d50808..68bf94f6d90 100644 --- a/doc/cosign_verify.md +++ b/doc/cosign_verify.md @@ -72,6 +72,7 @@ cosign verify [flags] --cert-email string the email expected in a valid Fulcio certificate --cert-oidc-issuer string the OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth --check-claims whether to check the claims found (default true) + --enforce-sct whether to enforce that a certificate contain an embedded SCT, a proof of inclusion in a certificate transparency log -h, --help help for verify --k8s-keychain whether to use the kubernetes keychain instead of the default keychain (supports workload identity). --key string path to the public key file, KMS URI or Kubernetes Secret diff --git a/go.mod b/go.mod index 9d72519daf1..d622821d8cf 100644 --- a/go.mod +++ b/go.mod @@ -314,3 +314,5 @@ require ( sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect ) + +exclude github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b diff --git a/go.sum b/go.sum index 209d61d5652..538d3d7f87b 100644 --- a/go.sum +++ b/go.sum @@ -1052,7 +1052,6 @@ github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzw github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog= github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20210429001901-424d2337a529/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= @@ -1717,7 +1716,6 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/moricho/tparallel v0.2.1/go.mod h1:fXEIZxG2vdfl0ZF8b42f5a78EhjjD5mX8qUplsoSU4k= diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index cdcc05074d5..b00ed733c75 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -31,6 +31,7 @@ import ( "strings" "time" + "github.com/sigstore/cosign/cmd/cosign/cli/fulcio/fulcioverifier/ctl" "github.com/sigstore/cosign/pkg/apis/cosigned/v1alpha1" cbundle "github.com/sigstore/cosign/pkg/cosign/bundle" "github.com/sigstore/cosign/pkg/cosign/tuf" @@ -83,6 +84,9 @@ type CheckOpts struct { CertEmail string // CertOidcIssuer is the OIDC issuer expected for a certificate to be valid. The empty string means any certificate can be valid. CertOidcIssuer string + // EnforceSCT requires that a certificate contain an embedded SCT during verification. An SCT is proof of inclusion in a + // certificate transparency log. + EnforceSCT bool // SignatureRef is the reference to the signature file SignatureRef string @@ -158,7 +162,8 @@ func ValidateAndUnpackCert(cert *x509.Certificate, co *CheckOpts) (signature.Ver } // Now verify the cert, then the signature. - if err := TrustedCert(cert, co.RootCerts, co.IntermediateCerts); err != nil { + chains, err := TrustedCert(cert, co.RootCerts, co.IntermediateCerts) + if err != nil { return nil, err } if co.CertEmail != "" { @@ -221,6 +226,22 @@ func ValidateAndUnpackCert(cert *x509.Certificate, co *CheckOpts) (signature.Ver } return nil, errors.New("none of the expected identities matched what was in the certificate") } + contains, err := ctl.ContainsSCT(cert.Raw) + if err != nil { + return nil, err + } + if co.EnforceSCT && !contains { + return nil, errors.New("certificate does not include required embedded SCT") + } + if contains { + // handle if chains has more than one chain - grab first and print message + if len(chains) > 1 { + fmt.Fprintf(os.Stderr, "**Info** Multiple valid certificate chains found. Selecting the first to verify the SCT.\n") + } + if err := ctl.VerifyEmbeddedSCT(context.Background(), chains[0]); err != nil { + return nil, err + } + } return verifier, nil } @@ -901,8 +922,8 @@ func VerifySET(bundlePayload cbundle.RekorPayload, signature []byte, pub *ecdsa. return nil } -func TrustedCert(cert *x509.Certificate, roots *x509.CertPool, intermediates *x509.CertPool) error { - if _, err := cert.Verify(x509.VerifyOptions{ +func TrustedCert(cert *x509.Certificate, roots *x509.CertPool, intermediates *x509.CertPool) ([][]*x509.Certificate, error) { + chains, err := cert.Verify(x509.VerifyOptions{ // THIS IS IMPORTANT: WE DO NOT CHECK TIMES HERE // THE CERTIFICATE IS TREATED AS TRUSTED FOREVER // WE CHECK THAT THE SIGNATURES WERE CREATED DURING THIS WINDOW @@ -912,10 +933,11 @@ func TrustedCert(cert *x509.Certificate, roots *x509.CertPool, intermediates *x5 KeyUsages: []x509.ExtKeyUsage{ x509.ExtKeyUsageCodeSigning, }, - }); err != nil { - return err + }) + if err != nil { + return nil, err } - return nil + return chains, nil } func correctAnnotations(wanted, have map[string]interface{}) bool { diff --git a/pkg/cosign/verify_test.go b/pkg/cosign/verify_test.go index 83e91a3ee5c..3f735c6d3ec 100644 --- a/pkg/cosign/verify_test.go +++ b/pkg/cosign/verify_test.go @@ -28,12 +28,14 @@ import ( "io" "net" "net/url" + "os" "strings" "testing" "time" "github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer" "github.com/go-openapi/strfmt" + "github.com/google/certificate-transparency-go/testdata" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/in-toto/in-toto-golang/in_toto" "github.com/pkg/errors" @@ -45,6 +47,7 @@ import ( "github.com/sigstore/cosign/pkg/types" "github.com/sigstore/cosign/test" rtypes "github.com/sigstore/rekor/pkg/types" + "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/options" "github.com/stretchr/testify/require" @@ -349,6 +352,64 @@ func TestValidateAndUnpackCertSuccessAllowAllValues(t *testing.T) { } } +func TestValidateAndUnpackCertWithSCT(t *testing.T) { + chain, err := cryptoutils.UnmarshalCertificatesFromPEM([]byte(testdata.TestEmbeddedCertPEM + testdata.CACertPEM)) + if err != nil { + t.Fatalf("error unmarshalling certificate chain: %v", err) + } + + rootPool := x509.NewCertPool() + rootPool.AddCert(chain[1]) + co := &CheckOpts{ + RootCerts: rootPool, + } + + // write SCT verification key to disk + tmpPrivFile, err := os.CreateTemp(t.TempDir(), "cosign_verify_sct_*.key") + if err != nil { + t.Fatalf("failed to create temp key file: %v", err) + } + defer tmpPrivFile.Close() + if _, err := tmpPrivFile.Write([]byte(testdata.LogPublicKeyPEM)); err != nil { + t.Fatalf("failed to write key file: %v", err) + } + os.Setenv("SIGSTORE_CT_LOG_PUBLIC_KEY_FILE", tmpPrivFile.Name()) + defer os.Unsetenv("SIGSTORE_CT_LOG_PUBLIC_KEY_FILE") + + _, err = ValidateAndUnpackCert(chain[0], co) + if err != nil { + t.Errorf("ValidateAndUnpackCert expected no error, got err = %v", err) + } + + // validate again, explicitly setting enforce SCT + co.EnforceSCT = true + _, err = ValidateAndUnpackCert(chain[0], co) + if err != nil { + t.Errorf("ValidateAndUnpackCert expected no error, got err = %v", err) + } +} + +func TestValidateAndUnpackCertWithoutRequiredSCT(t *testing.T) { + subject := "email@email" + oidcIssuer := "https://accounts.google.com" + + rootCert, rootKey, _ := test.GenerateRootCa() + leafCert, _, _ := test.GenerateLeafCert(subject, oidcIssuer, rootCert, rootKey) + + rootPool := x509.NewCertPool() + rootPool.AddCert(rootCert) + + co := &CheckOpts{ + RootCerts: rootPool, + CertEmail: subject, + CertOidcIssuer: oidcIssuer, + EnforceSCT: true, + } + + _, err := ValidateAndUnpackCert(leafCert, co) + require.Contains(t, err.Error(), "certificate does not include required embedded SCT") +} + func TestValidateAndUnpackCertInvalidRoot(t *testing.T) { subject := "email@email" oidcIssuer := "https://accounts.google.com" @@ -621,10 +682,16 @@ func TestTrustedCertSuccess(t *testing.T) { subPool := x509.NewCertPool() subPool.AddCert(subCert) - err := TrustedCert(leafCert, rootPool, subPool) + chains, err := TrustedCert(leafCert, rootPool, subPool) if err != nil { t.Fatalf("expected no error verifying certificate, got %v", err) } + if len(chains) != 1 { + t.Fatalf("unexpected number of chains found, expected 1, got %v", len(chains)) + } + if len(chains[0]) != 3 { + t.Fatalf("unexpected number of certs in chain, expected 3, got %v", len(chains[0])) + } } func TestTrustedCertSuccessNoIntermediates(t *testing.T) { @@ -634,7 +701,7 @@ func TestTrustedCertSuccessNoIntermediates(t *testing.T) { rootPool := x509.NewCertPool() rootPool.AddCert(rootCert) - err := TrustedCert(leafCert, rootPool, nil) + _, err := TrustedCert(leafCert, rootPool, nil) if err != nil { t.Fatalf("expected no error verifying certificate, got %v", err) } @@ -652,7 +719,7 @@ func TestTrustedCertSuccessChainFromRoot(t *testing.T) { subPool := x509.NewCertPool() subPool.AddCert(subCert) - err := TrustedCert(leafCert, rootPool, subPool) + _, err := TrustedCert(leafCert, rootPool, subPool) if err != nil { t.Fatalf("expected no error verifying certificate, got %v", err) } diff --git a/test/e2e_test.go b/test/e2e_test.go index c62280ba28a..1302b9d8478 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -501,8 +501,8 @@ func TestSignBlob(t *testing.T) { KeyRef: pubKeyPath2, } // Verify should fail on a bad input - mustErr(cliverify.VerifyBlobCmd(ctx, ko1, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, "badsig", blob), t) - mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, "badsig", blob), t) + mustErr(cliverify.VerifyBlobCmd(ctx, ko1, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, "badsig", blob, false), t) + mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, "badsig", blob, false), t) // Now sign the blob with one key ko := sign.KeyOpts{ @@ -514,8 +514,8 @@ func TestSignBlob(t *testing.T) { t.Fatal(err) } // Now verify should work with that one, but not the other - must(cliverify.VerifyBlobCmd(ctx, ko1, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, string(sig), bp), t) - mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, string(sig), bp), t) + must(cliverify.VerifyBlobCmd(ctx, ko1, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, string(sig), bp, false), t) + mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, string(sig), bp, false), t) } func TestSignBlobBundle(t *testing.T) { @@ -540,7 +540,7 @@ func TestSignBlobBundle(t *testing.T) { BundlePath: bundlePath, } // Verify should fail on a bad input - mustErr(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", blob), t) + mustErr(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", blob, false), t) // Now sign the blob with one key ko := sign.KeyOpts{ @@ -553,7 +553,7 @@ func TestSignBlobBundle(t *testing.T) { t.Fatal(err) } // Now verify should work - must(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", bp), t) + must(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", bp, false), t) // Now we turn on the tlog and sign again defer setenv(t, options.ExperimentalEnv, "1")() @@ -563,7 +563,7 @@ func TestSignBlobBundle(t *testing.T) { // Point to a fake rekor server to make sure offline verification of the tlog entry works os.Setenv(serverEnv, "notreal") - must(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", bp), t) + must(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", bp, false), t) } func TestGenerate(t *testing.T) { diff --git a/third_party/VENDOR-LICENSE/github.com/google/trillian/LICENSE b/third_party/VENDOR-LICENSE/github.com/google/trillian/merkle/LICENSE similarity index 100% rename from third_party/VENDOR-LICENSE/github.com/google/trillian/LICENSE rename to third_party/VENDOR-LICENSE/github.com/google/trillian/merkle/LICENSE From 3af58ac5a5b9612ac331f163dfec523a39404982 Mon Sep 17 00:00:00 2001 From: Hector Fernandez Date: Fri, 15 Apr 2022 02:21:16 +0200 Subject: [PATCH 18/53] chore: add warning when downloading a sBOM (#1763) Signed-off-by: hectorj2f --- cmd/cosign/cli/download.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cmd/cosign/cli/download.go b/cmd/cosign/cli/download.go index ce2410bf848..cdd5fcf99b7 100644 --- a/cmd/cosign/cli/download.go +++ b/cmd/cosign/cli/download.go @@ -16,6 +16,9 @@ package cli import ( + "fmt" + "os" + "github.com/spf13/cobra" "github.com/sigstore/cosign/cmd/cosign/cli/download" @@ -64,6 +67,7 @@ func downloadSBOM() *cobra.Command { Example: " cosign download sbom ", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + fmt.Fprintln(os.Stderr, "WARNING: Downloading SBOMs this way does not ensure its authenticity. If you want to ensure a tamper-proof SBOM, download it using 'cosign download attestation ' or verify its signature.") _, err := download.SBOMCmd(cmd.Context(), *o, args[0], cmd.OutOrStdout()) return err }, From 1bd687139fe50e4456ab49390db93c0026d1c4b0 Mon Sep 17 00:00:00 2001 From: Vladimir Nachev Date: Fri, 15 Apr 2022 17:51:14 +0300 Subject: [PATCH 19/53] [policy-webhook] The webhooks name is now configurable via --(validating|mutating)-webhook-name flags (#1757) Signed-off-by: Vladimir Nachev --- cmd/cosign/policy_webhook/main.go | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/cmd/cosign/policy_webhook/main.go b/cmd/cosign/policy_webhook/main.go index 44a4700419d..54a90fa6dcd 100644 --- a/cmd/cosign/policy_webhook/main.go +++ b/cmd/cosign/policy_webhook/main.go @@ -42,6 +42,25 @@ import ( _ "github.com/sigstore/sigstore/pkg/signature/kms/hashivault" ) +var ( + // mutatingWebhookName holds the name of the mutating webhook configuration + // resource dispatching admission requests to policy-webhook. + // It is also the name of the webhook which is injected by the controller + // with the resource types, namespace selectors, CABindle and service path. + // If this changes, you must also change: + // ./config/501-policy-webhook-configurations.yaml + // https://github.com/sigstore/helm-charts/blob/main/charts/cosigned/templates/policy-webhook/policy_webhook_configurations.yaml + mutatingWebhookName = flag.String("mutating-webhook-name", "defaulting.clusterimagepolicy.sigstore.dev", "The name of the mutating webhook configuration as well as the webhook name that is automatically configured, if exists, with different rules and client settings setting how the admission requests to be dispatched to policy-webhook.") + // validatingWebhookName holds the name of the validating webhook configuration + // resource dispatching admission requests to policy-webhook. + // It is also the name of the webhook which is injected by the controller + // with the resource types, namespace selectors, CABindle and service path. + // If this changes, you must also change: + // ./config/501-policy-webhook-configurations.yaml + // https://github.com/sigstore/helm-charts/blob/main/charts/cosigned/templates/policy-webhook/policy_webhook_configurations.yaml + validatingWebhookName = flag.String("validating-webhook-name", "validating.clusterimagepolicy.sigstore.dev", "The name of the validating webhook configuration as well as the webhook name that is automatically configured, if exists, with different rules and client settings setting how the admission requests to be dispatched to policy-webhook.") +) + func main() { opts := webhook.Options{ ServiceName: "policy-webhook", @@ -68,7 +87,7 @@ func main() { func NewPolicyValidatingAdmissionController(ctx context.Context, cmw configmap.Watcher) *controller.Impl { return validation.NewAdmissionController( ctx, - "validating.clusterimagepolicy.sigstore.dev", + *validatingWebhookName, "/validating", map[schema.GroupVersionKind]resourcesemantics.GenericCRD{ v1alpha1.SchemeGroupVersion.WithKind("ClusterImagePolicy"): &v1alpha1.ClusterImagePolicy{}, @@ -83,7 +102,7 @@ func NewPolicyValidatingAdmissionController(ctx context.Context, cmw configmap.W func NewPolicyMutatingAdmissionController(ctx context.Context, cmw configmap.Watcher) *controller.Impl { return defaulting.NewAdmissionController( ctx, - "defaulting.clusterimagepolicy.sigstore.dev", + *mutatingWebhookName, "/defaulting", map[schema.GroupVersionKind]resourcesemantics.GenericCRD{ v1alpha1.SchemeGroupVersion.WithKind("ClusterImagePolicy"): &v1alpha1.ClusterImagePolicy{}, From c8b83c0b006310a986407cf7bdcad6f490464cdd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Apr 2022 12:08:50 -0500 Subject: [PATCH 20/53] Bump mikefarah/yq from 4.24.4 to 4.24.5 (#1765) Bumps [mikefarah/yq](https://github.com/mikefarah/yq) from 4.24.4 to 4.24.5. - [Release notes](https://github.com/mikefarah/yq/releases) - [Changelog](https://github.com/mikefarah/yq/blob/master/release_notes.txt) - [Commits](https://github.com/mikefarah/yq/compare/13a27e8b2da75353d4172a98d9ead8a79b36edc9...ed5b811f37384d92f62898492ddd81b6dc3af38f) --- updated-dependencies: - dependency-name: mikefarah/yq dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/kind-cluster-image-policy.yaml | 2 +- .github/workflows/kind-e2e-cosigned.yaml | 2 +- .github/workflows/kind-verify-attestation.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/kind-cluster-image-policy.yaml b/.github/workflows/kind-cluster-image-policy.yaml index 963f5f6a4be..d552113442a 100644 --- a/.github/workflows/kind-cluster-image-policy.yaml +++ b/.github/workflows/kind-cluster-image-policy.yaml @@ -59,7 +59,7 @@ jobs: - uses: imranismail/setup-kustomize@8fa954828ed3cfa7a487a2ba9f7104899bb48b2f # v1.6.1 - name: Install yq - uses: mikefarah/yq@13a27e8b2da75353d4172a98d9ead8a79b36edc9 # v4.16.2 + uses: mikefarah/yq@ed5b811f37384d92f62898492ddd81b6dc3af38f # v4.16.2 - name: Setup mirror uses: chainguard-dev/actions/setup-mirror@main diff --git a/.github/workflows/kind-e2e-cosigned.yaml b/.github/workflows/kind-e2e-cosigned.yaml index c6efdc7f13a..272c6f67d51 100644 --- a/.github/workflows/kind-e2e-cosigned.yaml +++ b/.github/workflows/kind-e2e-cosigned.yaml @@ -53,7 +53,7 @@ jobs: - uses: imranismail/setup-kustomize@8fa954828ed3cfa7a487a2ba9f7104899bb48b2f # v1.6.1 - name: Install yq - uses: mikefarah/yq@13a27e8b2da75353d4172a98d9ead8a79b36edc9 # v4.16.2 + uses: mikefarah/yq@ed5b811f37384d92f62898492ddd81b6dc3af38f # v4.16.2 - name: Install Cosign run: | diff --git a/.github/workflows/kind-verify-attestation.yaml b/.github/workflows/kind-verify-attestation.yaml index 558a9d156a9..679873289cb 100644 --- a/.github/workflows/kind-verify-attestation.yaml +++ b/.github/workflows/kind-verify-attestation.yaml @@ -60,7 +60,7 @@ jobs: - uses: imjasonh/setup-ko@2c3450ca27f6e6f2b02e72a40f2163c281a1f675 # v0.4 - name: Install yq - uses: mikefarah/yq@13a27e8b2da75353d4172a98d9ead8a79b36edc9 # v4.16.2 + uses: mikefarah/yq@ed5b811f37384d92f62898492ddd81b6dc3af38f # v4.16.2 - name: build cosign run: | From 56eb155817ea48e60f32af20289f61cbfbc90a39 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Apr 2022 12:12:36 -0500 Subject: [PATCH 21/53] Bump actions/checkout from 3.0.0 to 3.0.1 (#1764) Bumps [actions/checkout](https://github.com/actions/checkout) from 3.0.0 to 3.0.1. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/a12a3943b4bdde767164f792f33f40b04645d846...dcd71f646680f2efd8db4afa5ad64fdcba30e748) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yaml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/cross.yaml | 2 +- .github/workflows/donotsubmit.yaml | 2 +- .github/workflows/e2e-with-binary.yml | 2 +- .github/workflows/e2e_tests.yml | 2 +- .github/workflows/github-oidc.yaml | 2 +- .github/workflows/kind-cluster-image-policy.yaml | 2 +- .github/workflows/kind-e2e-cosigned.yaml | 2 +- .github/workflows/kind-verify-attestation.yaml | 2 +- .github/workflows/scorecard_action.yml | 2 +- .github/workflows/style.yaml | 4 ++-- .github/workflows/tests.yaml | 10 +++++----- .github/workflows/validate-release.yml | 2 +- .github/workflows/verify-codegen.yaml | 2 +- .github/workflows/verify-docgen.yaml | 2 +- .github/workflows/whitespace.yaml | 2 +- 17 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 4a617c200cd..69d9194d692 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -33,7 +33,7 @@ jobs: contents: read steps: - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 - uses: sigstore/cosign-installer@bb61838e7ee5bf314f85f2e219b3706835fa6306 # v2.0.1 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a759e2eef77..4f9d993e68d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,7 +39,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 - name: Utilize Go Module Cache uses: actions/cache@48af2dc4a9e8278b89d7fa154b955c30c6aaab09 # v3.0.2 diff --git a/.github/workflows/cross.yaml b/.github/workflows/cross.yaml index 8f7e0acd411..e265fb8d55e 100644 --- a/.github/workflows/cross.yaml +++ b/.github/workflows/cross.yaml @@ -33,7 +33,7 @@ jobs: with: go-version: '1.17.x' - name: Checkout code - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 - name: build cosign run: | make cosign && mv ./cosign ./${{matrix.COSIGN_TARGET}} diff --git a/.github/workflows/donotsubmit.yaml b/.github/workflows/donotsubmit.yaml index 5a046551509..a64d27bb876 100644 --- a/.github/workflows/donotsubmit.yaml +++ b/.github/workflows/donotsubmit.yaml @@ -14,7 +14,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 #v2.4.0 + uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 #v2.4.0 - name: Do Not Submit uses: chainguard-dev/actions/donotsubmit@84c993eaf02da1c325854fb272a4df9184bd80fc # main diff --git a/.github/workflows/e2e-with-binary.yml b/.github/workflows/e2e-with-binary.yml index 95b02ff9561..2378c17a9a8 100644 --- a/.github/workflows/e2e-with-binary.yml +++ b/.github/workflows/e2e-with-binary.yml @@ -38,7 +38,7 @@ jobs: COSIGN_EXPERIMENTAL: "true" steps: - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 with: go-version: '1.17.x' diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index ee407d8fa26..9bece8b4e80 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -31,7 +31,7 @@ jobs: os: [macos-latest, ubuntu-latest, windows-latest] steps: - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 with: go-version: '1.17.x' diff --git a/.github/workflows/github-oidc.yaml b/.github/workflows/github-oidc.yaml index 72c571eb5f5..d2bd092c7f8 100644 --- a/.github/workflows/github-oidc.yaml +++ b/.github/workflows/github-oidc.yaml @@ -35,7 +35,7 @@ jobs: KO_PREFIX: ghcr.io/${{ github.repository }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 with: go-version: '1.17.x' diff --git a/.github/workflows/kind-cluster-image-policy.yaml b/.github/workflows/kind-cluster-image-policy.yaml index d552113442a..88c6271c2f9 100644 --- a/.github/workflows/kind-cluster-image-policy.yaml +++ b/.github/workflows/kind-cluster-image-policy.yaml @@ -48,7 +48,7 @@ jobs: COSIGN_EXPERIMENTAL: true steps: - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 with: go-version: '1.17.x' diff --git a/.github/workflows/kind-e2e-cosigned.yaml b/.github/workflows/kind-e2e-cosigned.yaml index 272c6f67d51..25bc7520f60 100644 --- a/.github/workflows/kind-e2e-cosigned.yaml +++ b/.github/workflows/kind-e2e-cosigned.yaml @@ -43,7 +43,7 @@ jobs: KO_DOCKER_REPO: registry.local:5000/cosigned steps: - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 with: go-version: 1.17.x diff --git a/.github/workflows/kind-verify-attestation.yaml b/.github/workflows/kind-verify-attestation.yaml index 679873289cb..3e953c64122 100644 --- a/.github/workflows/kind-verify-attestation.yaml +++ b/.github/workflows/kind-verify-attestation.yaml @@ -51,7 +51,7 @@ jobs: COSIGN_EXPERIMENTAL: "true" steps: - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 with: go-version: '1.17.x' diff --git a/.github/workflows/scorecard_action.yml b/.github/workflows/scorecard_action.yml index 90a76bc0fe8..2b5a48bbc18 100644 --- a/.github/workflows/scorecard_action.yml +++ b/.github/workflows/scorecard_action.yml @@ -23,7 +23,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 with: persist-credentials: false diff --git a/.github/workflows/style.yaml b/.github/workflows/style.yaml index e57b1b27ab1..de015e60a72 100644 --- a/.github/workflows/style.yaml +++ b/.github/workflows/style.yaml @@ -18,7 +18,7 @@ jobs: go-version: 1.16.x - name: Check out code - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 - uses: chainguard-dev/actions/gofmt@84c993eaf02da1c325854fb272a4df9184bd80fc # main with: @@ -35,6 +35,6 @@ jobs: go-version: 1.16.x - name: Check out code - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 - uses: chainguard-dev/actions/goimports@84c993eaf02da1c325854fb272a4df9184bd80fc # main diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index d4bc6daf8d2..d6e525b4be5 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -38,7 +38,7 @@ jobs: OS: ${{ matrix.os }} steps: - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 # https://github.com/mvdan/github-actions-golang#how-do-i-set-up-caching-between-builds - uses: actions/cache@48af2dc4a9e8278b89d7fa154b955c30c6aaab09 # v3.0.2 with: @@ -73,7 +73,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 # https://github.com/mvdan/github-actions-golang#how-do-i-set-up-caching-between-builds - uses: actions/cache@48af2dc4a9e8278b89d7fa154b955c30c6aaab09 # v3.0.2 with: @@ -111,7 +111,7 @@ jobs: name: Run PowerShell E2E tests runs-on: windows-latest steps: - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.1.5 with: go-version: ${{ env.GO_VERSION }} @@ -136,7 +136,7 @@ jobs: name: license boilerplate check runs-on: ubuntu-latest steps: - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 with: go-version: ${{ env.GO_VERSION }} @@ -151,7 +151,7 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 with: go-version: ${{ env.GO_VERSION }} diff --git a/.github/workflows/validate-release.yml b/.github/workflows/validate-release.yml index 93f8a93507b..8e08a79ccef 100644 --- a/.github/workflows/validate-release.yml +++ b/.github/workflows/validate-release.yml @@ -43,7 +43,7 @@ jobs: COSIGN_IMAGE: gcr.io/projectsigstore/cosign:v1.7.2@sha256:ad2985a87622d5934a4bc06a61faadff772e377937e42519af4f506e1b019d1e steps: - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 #v2.4.0 + - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 #v2.4.0 - name: Check Signature run: | diff --git a/.github/workflows/verify-codegen.yaml b/.github/workflows/verify-codegen.yaml index dcb1b65b796..53fa4ff45d2 100644 --- a/.github/workflows/verify-codegen.yaml +++ b/.github/workflows/verify-codegen.yaml @@ -36,7 +36,7 @@ jobs: with: go-version: 1.17.x - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 + - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 with: path: ./src/github.com/${{ github.repository }} fetch-depth: 0 diff --git a/.github/workflows/verify-docgen.yaml b/.github/workflows/verify-docgen.yaml index c9aed73023d..da3da6064f3 100644 --- a/.github/workflows/verify-docgen.yaml +++ b/.github/workflows/verify-docgen.yaml @@ -31,7 +31,7 @@ jobs: steps: - name: deps run: sudo apt-get update && sudo apt-get install -yq libpcsclite-dev - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0 + - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 with: go-version: '1.17.x' diff --git a/.github/workflows/whitespace.yaml b/.github/workflows/whitespace.yaml index 5d3589fe5c8..7ebb3638d53 100644 --- a/.github/workflows/whitespace.yaml +++ b/.github/workflows/whitespace.yaml @@ -14,7 +14,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 #v2.4.0 + uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 #v2.4.0 - uses: chainguard-dev/actions/trailing-space@84c993eaf02da1c325854fb272a4df9184bd80fc # main if: ${{ always() }} From fe68fece0d6fd4e244dc36eb10f5cf614cb82c12 Mon Sep 17 00:00:00 2001 From: Ville Aikas <11279988+vaikas@users.noreply.github.com> Date: Fri, 15 Apr 2022 11:07:22 -0700 Subject: [PATCH 22/53] Break the CIP action tests into a sh script. (#1767) Signed-off-by: Ville Aikas --- .../workflows/kind-cluster-image-policy.yaml | 158 +------------ test/e2e_test_cluster_image_policy.sh | 218 ++++++++++++++++++ 2 files changed, 220 insertions(+), 156 deletions(-) create mode 100755 test/e2e_test_cluster_image_policy.sh diff --git a/.github/workflows/kind-cluster-image-policy.yaml b/.github/workflows/kind-cluster-image-policy.yaml index 88c6271c2f9..44ae852b725 100644 --- a/.github/workflows/kind-cluster-image-policy.yaml +++ b/.github/workflows/kind-cluster-image-policy.yaml @@ -88,163 +88,9 @@ jobs: # Wait for the webhook to come up and become Ready kubectl rollout status --timeout 5m --namespace cosign-system deployments/webhook - - name: Create sample image - demoimage + - name: Run Cluster Image Policy Tests run: | - pushd $(mktemp -d) - go mod init example.com/demo - cat < main.go - package main - import "fmt" - func main() { - fmt.Println("hello world") - } - EOF - demoimage=`ko publish -B example.com/demo` - echo "demoimage=$demoimage" >> $GITHUB_ENV - echo Created image $demoimage - popd - - - name: Create sample image2 - demoimage2 - run: | - pushd $(mktemp -d) - go mod init example.com/demo2 - cat < main.go - package main - import "fmt" - func main() { - fmt.Println("hello world 2") - } - EOF - demoimage2=`ko publish -B example.com/demo2` - echo "demoimage2=$demoimage2" >> $GITHUB_ENV - echo Created image $demoimage2 - popd - - - name: Deploy ClusterImagePolicy With Keyless Signing - run: | - kubectl apply -f ./test/testdata/cosigned/e2e/cip-keyless.yaml - - - name: Sign demoimage with cosign - run: | - ./cosign sign --rekor-url ${{ env.REKOR_URL }} --fulcio-url ${{ env.FULCIO_URL }} --force --allow-insecure-registry ${{ env.demoimage }} --identity-token ${{ env.OIDC_TOKEN }} - - - name: Verify with cosign - run: | - SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY=1 COSIGN_EXPERIMENTAL=1 ./cosign verify --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry ${{ env.demoimage }} - - - name: Deploy jobs and verify signed works, unsigned fails - run: | - kubectl create namespace demo-keyless-signing - kubectl label namespace demo-keyless-signing cosigned.sigstore.dev/include=true - - echo '::group:: test job success' - # We signed this above, this should work - if ! kubectl create -n demo-keyless-signing job demo --image=${{ env.demoimage }} ; then - echo Failed to create Job in namespace with matching signature! - exit 1 - else - echo Succcessfully created Job with signed image - fi - echo '::endgroup:: test job success' - - echo '::group:: test job rejection' - # We did not sign this, should fail - if kubectl create -n demo-keyless-signing job demo2 --image=${{ env.demoimage2 }} ; then - echo Failed to block unsigned Job creation! - exit 1 - else - echo Successfully blocked Job creation with unsigned image - fi - echo '::endgroup::' - - - name: Add ClusterImagePolicy with identities that match issuer and subject - run: | - kubectl apply -f ./test/testdata/cosigned/e2e/cip-keyless-with-identities.yaml - # make sure the reconciler has enough time to update the configmap - sleep 5 - - - name: Verify the job still works with additional constraints - run: | - echo '::group:: test job success' - # This has correct issuer/subject, so should work - if ! kubectl create -n demo-keyless-signing job demo-identities-works --image=${{ env.demoimage }} ; then - echo Failed to create Job in namespace without label! - exit 1 - else - echo Succcessfully created Job with signed image - fi - echo '::endgroup:: test job success' - - - name: Add ClusterImagePolicy with identities that do not match issuer and subject - run: | - kubectl apply -f ./test/testdata/cosigned/e2e/cip-keyless-with-identities-mismatch.yaml - # make sure the reconciler has enough time to update the configmap - sleep 5 - - - name: Verify the job now fails because subject and issuer do not match - run: | - echo '::group:: test job block' - if kubectl create -n demo-keyless-signing job demo-identities-works --image=${{ env.demoimage }} ; then - echo Failed to block Job in namespace with non matching issuer and subject! - exit 1 - else - echo Succcessfully blocked Job with mismatching issuer and subject - fi - echo '::endgroup:: test job block' - - - name: Remove the mismatching cip - run: | - kubectl delete cip image-policy-keyless-with-identities-mismatch - sleep 5 - - - name: Generate New Signing Key - run: | - COSIGN_PASSWORD="" ./cosign generate-key-pair - - - name: Deploy ClusterImagePolicy With Key Signing - run: | - yq '. | .spec.authorities[0].key.data |= load_str("cosign.pub")' ./test/testdata/cosigned/e2e/cip-key.yaml | \ - kubectl apply -f - - - - name: Verify with two CIP, one not signed with public key - run: | - if kubectl create -n demo-key-signing job demo --image=${{ env.demoimage }}; then - echo Failed to block unsigned Job creation! - exit 1 - fi - - - name: Sign demoimage with cosign key - run: | - ./cosign sign --key cosign.key --force --allow-insecure-registry ${{ env.demoimage }} - - - name: Verify with cosign - run: | - ./cosign verify --key cosign.pub --allow-insecure-registry ${{ env.demoimage }} - - - name: Deploy jobs and verify signed works, unsigned fails - run: | - kubectl create namespace demo-key-signing - kubectl label namespace demo-key-signing cosigned.sigstore.dev/include=true - - echo '::group:: test job success' - # We signed this above, this should work - if ! kubectl create -n demo-key-signing job demo --image=${{ env.demoimage }} ; then - echo Failed to create Job in namespace without label! - exit 1 - else - echo Succcessfully created Job with signed image - fi - echo '::endgroup:: test job success' - - echo '::group:: test job rejection' - # We did not sign this, should fail - if kubectl create -n demo-key-signing job demo2 --image=${{ env.demoimage2 }} ; then - echo Failed to block unsigned Job creation! - exit 1 - else - echo Successfully blocked Job creation with unsigned image - fi - echo '::endgroup::' + ./test/e2e_test_cluster_image_policy.sh - name: Collect diagnostics if: ${{ failure() }} diff --git a/test/e2e_test_cluster_image_policy.sh b/test/e2e_test_cluster_image_policy.sh new file mode 100755 index 00000000000..0f53f7ae141 --- /dev/null +++ b/test/e2e_test_cluster_image_policy.sh @@ -0,0 +1,218 @@ +#!/usr/bin/env bash +# +# 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. + + +set -ex + +if [[ -z "${OIDC_TOKEN}" ]]; then + if [[ -z "${TOKEN_ISSUER}" ]]; then + echo "Must specify either env variable OIDC_TOKEN or TOKEN_ISSUER" + exit 1 + else + export OIDC_TOKEN=`curl -s ${ISSUER_URL}` + fi +fi + +if [[ -z "${KO_DOCKER_REPO}" ]]; then + echo "Must specify env variable KO_DOCKER_REPO" + exit 1 +fi + +if [[ -z "${FULCIO_URL}" ]]; then + echo "Must specify env variable FULCIO_URL" + exit 1 +fi + +if [[ -z "${REKOR_URL}" ]]; then + echo "Must specify env variable REKOR_URL" + exit 1 +fi + +if [[ -z "${SIGSTORE_CT_LOG_PUBLIC_KEY_FILE}" ]]; then + echo "must specify env variable SIGSTORE_CT_LOG_PUBLIC_KEY_FILE" + exit 1 +fi + +if [[ "${NON_REPRODUCIBLE}"=="1" ]]; then + echo "creating non-reproducible build by adding a timestamp" + export TIMESTAMP=`date +%s` +else + export TIMESTAMP="TIMESTAMP" +fi + +# Trust our own custom Rekor API +export SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY=1 + +# Publish the first test image +echo '::group:: publish test image demoimage' +pushd $(mktemp -d) +go mod init example.com/demo +cat < main.go +package main +import "fmt" +func main() { + fmt.Println("hello world TIMESTAMP") +} +EOF + +sed -i'' -e "s@TIMESTAMP@${TIMESTAMP}@g" main.go +cat main.go +export demoimage=`ko publish -B example.com/demo` +echo Created image $demoimage +popd +echo '::endgroup::' + +# Publish the second test image +echo '::group:: publish test image demoimage' +pushd $(mktemp -d) +go mod init example.com/demo +cat < main.go +package main +import "fmt" +func main() { + fmt.Println("hello world 2 TIMESTAMP") +} +EOF +sed -i'' -e "s@TIMESTAMP@${TIMESTAMP}@g" main.go +cat main.go +export demoimage2=`ko publish -B example.com/demo` +popd +echo '::endgroup::' + +echo '::group:: Deploy ClusterImagePolicy with keyless signing' +kubectl apply -f ./test/testdata/cosigned/e2e/cip-keyless.yaml +echo '::endgroup::' + +echo '::group:: Sign demo image' +COSIGN_EXPERIMENTAL=1 ./cosign sign --rekor-url ${REKOR_URL} --fulcio-url ${FULCIO_URL} --force --allow-insecure-registry ${demoimage} --identity-token ${OIDC_TOKEN} +echo '::endgroup::' + +echo '::group:: Verify demo image' +COSIGN_EXPERIMENTAL=1 ./cosign verify --rekor-url ${REKOR_URL} --allow-insecure-registry ${demoimage} +echo '::endgroup::' + +echo '::group:: Create test namespace and label for verification' +kubectl create namespace demo-keyless-signing +kubectl label namespace demo-keyless-signing cosigned.sigstore.dev/include=true +echo '::endgroup::' + +echo '::group:: test job success' +# We signed this above, this should work +if ! kubectl create -n demo-keyless-signing job demo --image=${demoimage} ; then + echo Failed to create Job in namespace with matching signature! + exit 1 +else + echo Succcessfully created Job with signed image +fi +echo '::endgroup::' + +# We did not sign this, should fail +echo '::group:: test job rejection' +if kubectl create -n demo-keyless-signing job demo2 --image=${demoimage2} ; then + echo Failed to block unsigned Job creation! + exit 1 +else + echo Successfully blocked Job creation with unsigned image +fi +echo '::endgroup::' + +echo '::group:: Add cip with identities that match issuer/subject' +kubectl apply -f ./test/testdata/cosigned/e2e/cip-keyless-with-identities.yaml +# make sure the reconciler has enough time to update the configmap +sleep 5 +echo '::endgroup::' + +# This has correct issuer/subject, so should work +echo '::group:: test job success with identities' +if ! kubectl create -n demo-keyless-signing job demo-identities-works --image=${demoimage} ; then + echo Failed to create Job in namespace without label! + exit 1 +else + echo Succcessfully created Job with signed image +fi +echo '::endgroup::' + +echo '::group:: Add cip with identities that match issuer/subject' +kubectl apply -f ./test/testdata/cosigned/e2e/cip-keyless-with-identities-mismatch.yaml +# make sure the reconciler has enough time to update the configmap +sleep 5 +echo '::endgroup::' + +echo '::group:: test job block' +if kubectl create -n demo-keyless-signing job demo-identities-works --image=${demoimage} ; then + echo Failed to block Job in namespace with non matching issuer and subject! + exit 1 +else + echo Succcessfully blocked Job with mismatching issuer and subject +fi +echo '::endgroup::' + +echo '::group:: Remove mismatching cip' +kubectl delete cip image-policy-keyless-with-identities-mismatch +sleep 5 +echo '::endgroup::' + +echo '::group:: Generate signing key' +COSIGN_PASSWORD="" ./cosign generate-key-pair +echo '::endgroup::' + +echo '::group:: Deploy ClusterImagePolicy With Key Signing' +yq '. | .spec.authorities[0].key.data |= load_str("cosign.pub")' ./test/testdata/cosigned/e2e/cip-key.yaml | kubectl apply -f - +echo '::endgroup::' + +echo '::group:: Verify blocks unsigned with the key' +if kubectl create -n demo-key-signing job demo --image=${demoimage}; then + echo Failed to block unsigned Job creation! + exit 1 +fi +echo '::endgroup::' + +echo '::group:: Sign demoimage with cosign key' +COSIGN_PASSWORD="" ./cosign sign --key cosign.key --force --allow-insecure-registry ${demoimage} +echo '::endgroup::' + +echo '::group:: Verify demoimage with cosign key' +./cosign verify --key cosign.pub --allow-insecure-registry ${demoimage} +echo '::endgroup::' + +echo '::group:: Create and label new namespace for verification' +kubectl create namespace demo-key-signing +kubectl label namespace demo-key-signing cosigned.sigstore.dev/include=true + +echo '::group:: test job success' +# We signed this above, this should work +if ! kubectl create -n demo-key-signing job demo --image=${demoimage} ; then + echo Failed to create Job in namespace without label! + exit 1 +else + echo Succcessfully created Job with signed image +fi +echo '::endgroup:: test job success' + +echo '::group:: test job rejection' +# We did not sign this, should fail +if kubectl create -n demo-key-signing job demo2 --image=${demoimage2} ; then + echo Failed to block unsigned Job creation! + exit 1 +else + echo Successfully blocked Job creation with unsigned image +fi +echo '::endgroup::' + +echo '::group::' Cleanup +kubectl delete cip --all +kubectl delete ns demo-key-signing demo-keyless-signing +rm cosign.key cosign.pub From f2c360eb97e52fa7766ecde370f1a48b910d7404 Mon Sep 17 00:00:00 2001 From: asraa Date: Sun, 17 Apr 2022 07:47:50 -0500 Subject: [PATCH 23/53] tuf: add debug info if tuf update fails (#1766) * add debug info for tuf update fail Signed-off-by: Asra Ali * move debugging funcs to top Signed-off-by: Asra Ali --- pkg/cosign/tuf/client.go | 197 +++++++++++++++++++++++++++------------ 1 file changed, 135 insertions(+), 62 deletions(-) diff --git a/pkg/cosign/tuf/client.go b/pkg/cosign/tuf/client.go index ab632fac130..b37902bc3bc 100644 --- a/pkg/cosign/tuf/client.go +++ b/pkg/cosign/tuf/client.go @@ -22,6 +22,7 @@ import ( "encoding/json" "fmt" "io" + "io/ioutil" "net/url" "os" "path" @@ -55,10 +56,17 @@ type TUF struct { // JSON output representing the configured root status type RootStatus struct { - Local string `json:"local"` - Remote string `json:"remote"` - Expiration map[string]string `json:"expiration"` - Targets []string `json:"targets"` + Local string `json:"local"` + Remote string `json:"remote"` + Metadata map[string]MetadataStatus `json:"metadata"` + Targets []string `json:"targets"` +} + +type MetadataStatus struct { + Version int `json:"version"` + Size int `json:"len"` + Expiration string `json:"expiration"` + Error string `json:"error"` } type TargetFile struct { @@ -75,20 +83,64 @@ type sigstoreCustomMetadata struct { Sigstore customMetadata `json:"sigstore"` } +type signedMeta struct { + Type string `json:"_type"` + Expires time.Time `json:"expires"` + Version int64 `json:"version"` +} + // RemoteCache contains information to cache on the location of the remote // repository. type remoteCache struct { Mirror string `json:"mirror"` } -// GetRootStatus gets the current root status for info logging -func GetRootStatus(ctx context.Context) (*RootStatus, error) { - t, err := NewFromEnv(ctx) +func getExpiration(metadata []byte) (*time.Time, error) { + s := &data.Signed{} + if err := json.Unmarshal(metadata, s); err != nil { + return nil, err + } + sm := &signedMeta{} + if err := json.Unmarshal(s.Signed, sm); err != nil { + return nil, err + } + return &sm.Expires, nil +} + +func getVersion(metadata []byte) (int64, error) { + s := &data.Signed{} + if err := json.Unmarshal(metadata, s); err != nil { + return 0, err + } + sm := &signedMeta{} + if err := json.Unmarshal(s.Signed, sm); err != nil { + return 0, err + } + return sm.Version, nil +} + +var isExpiredTimestamp = func(metadata []byte) bool { + expiration, err := getExpiration(metadata) + if err != nil { + return true + } + return time.Until(*expiration) <= 0 +} + +func getMetadataStatus(b []byte) (*MetadataStatus, error) { + expires, err := getExpiration(b) if err != nil { return nil, err } - defer t.Close() - return t.getRootStatus() + version, err := getVersion(b) + if err != nil { + return nil, err + } + return &MetadataStatus{ + Size: len(b), + Expiration: expires.Format(time.RFC822), + Version: int(version), + }, nil } func (t *TUF) getRootStatus() (*RootStatus, error) { @@ -97,10 +149,10 @@ func (t *TUF) getRootStatus() (*RootStatus, error) { local = rootCacheDir() } status := &RootStatus{ - Local: local, - Remote: t.mirror, - Expiration: map[string]string{}, - Targets: []string{}, + Local: local, + Remote: t.mirror, + Metadata: make(map[string]MetadataStatus), + Targets: []string{}, } // Get targets @@ -118,16 +170,40 @@ func (t *TUF) getRootStatus() (*RootStatus, error) { return nil, errors.Wrap(err, "getting trusted meta") } for role, md := range trustedMeta { - expires, err := getExpiration(md) + mdStatus, err := getMetadataStatus(md) if err != nil { - return nil, errors.Wrap(err, fmt.Sprintf("getting expiration for %s", role)) + status.Metadata[role] = MetadataStatus{Error: err.Error()} + continue } - status.Expiration[role] = expires.Format(time.RFC822) + status.Metadata[role] = *mdStatus } return status, nil } +func getRoot(meta map[string]json.RawMessage) (json.RawMessage, error) { + trustedRoot, ok := meta["root.json"] + if ok { + return trustedRoot, nil + } + // On first initialize, there will be no root in the TUF DB, so read from embedded. + trustedRoot, err := embeddedRootRepo.ReadFile(path.Join("repository", "root.json")) + if err != nil { + return nil, err + } + return trustedRoot, nil +} + +// GetRootStatus gets the current root status for info logging +func GetRootStatus(ctx context.Context) (*RootStatus, error) { + t, err := NewFromEnv(ctx) + if err != nil { + return nil, err + } + defer t.Close() + return t.getRootStatus() +} + // Close closes the local TUF store. Should only be called once per client. func (t *TUF) Close() error { return t.local.Close() @@ -239,19 +315,6 @@ func NewFromEnv(ctx context.Context) (*TUF, error) { return initializeTUF(ctx, embed, mirror, nil, false) } -func getRoot(meta map[string]json.RawMessage) (json.RawMessage, error) { - trustedRoot, ok := meta["root.json"] - if ok { - return trustedRoot, nil - } - // On first initialize, there will be no root in the TUF DB, so read from embedded. - trustedRoot, err := embeddedRootRepo.ReadFile(path.Join("repository", "root.json")) - if err != nil { - return nil, err - } - return trustedRoot, nil -} - func Initialize(ctx context.Context, mirror string, root []byte) error { // Initialize the client. Force an update. t, err := initializeTUF(ctx, false, mirror, root, true) @@ -360,34 +423,41 @@ func embeddedLocalStore() (client.LocalStore, error) { return local, nil } -//go:embed repository -var embeddedRootRepo embed.FS - -func getExpiration(metadata []byte) (*time.Time, error) { - s := &data.Signed{} - if err := json.Unmarshal(metadata, s); err != nil { - return nil, err - } - sm := &data.Timestamp{} - if err := json.Unmarshal(s.Signed, sm); err != nil { - return nil, err - } - return &sm.Expires, nil -} - -var isExpiredTimestamp = func(metadata []byte) bool { - expiration, err := getExpiration(metadata) - if err != nil { - return true - } - return time.Until(*expiration) <= 0 -} - func (t *TUF) updateMetadataAndDownloadTargets() error { // Download updated targets and cache new metadata and targets in ${TUF_ROOT}. targetFiles, err := t.client.Update() if err != nil && !client.IsLatestSnapshot(err) { - return errors.Wrap(err, "updating tuf metadata") + // Get some extra information for debugging. What was the state of the metadata + // on the remote? + status := struct { + Mirror string `json:"mirror"` + Metadata map[string]MetadataStatus `json:"metadata"` + }{ + Mirror: t.mirror, + Metadata: make(map[string]MetadataStatus), + } + for _, md := range []string{"root.json", "targets.json", "snapshot.json", "timestamp.json"} { + r, _, err := t.remote.GetMeta(md) + if err != nil { + // May be missing, or failed download. + continue + } + defer r.Close() + b, err := ioutil.ReadAll(r) + if err != nil { + continue + } + mdStatus, err := getMetadataStatus(b) + if err != nil { + continue + } + status.Metadata[md] = *mdStatus + } + b, innerErr := json.MarshalIndent(status, "", "\t") + if innerErr != nil { + return innerErr + } + return fmt.Errorf("error updating to TUF remote mirror: %w\nremote status:%s", err, string(b)) } // Update the in-memory targets. @@ -405,15 +475,6 @@ func (t *TUF) updateMetadataAndDownloadTargets() error { return nil } -func downloadRemoteTarget(name string, c *client.Client, w io.Writer) error { - dest := targetDestination{} - if err := c.Download(name, &dest); err != nil { - return errors.Wrap(err, "downloading target") - } - _, err := io.Copy(w, &dest.buf) - return err -} - type targetDestination struct { buf bytes.Buffer } @@ -427,6 +488,15 @@ func (t *targetDestination) Delete() error { return nil } +func downloadRemoteTarget(name string, c *client.Client, w io.Writer) error { + dest := targetDestination{} + if err := c.Download(name, &dest); err != nil { + return errors.Wrap(err, "downloading target") + } + _, err := io.Copy(w, &dest.buf) + return err +} + func rootCacheDir() string { rootDir := os.Getenv(TufRootEnv) if rootDir == "" { @@ -468,6 +538,9 @@ func (m *memoryCache) Set(p string, b []byte) error { return nil } +//go:embed repository +var embeddedRootRepo embed.FS + type embedded struct { setImpl } From 546ca56aeb420cc4c6219d8acc0989744eed9b1f Mon Sep 17 00:00:00 2001 From: Hector Fernandez Date: Mon, 18 Apr 2022 00:20:58 +0200 Subject: [PATCH 24/53] fix: add support for rsa keys (#1768) Signed-off-by: hectorj2f --- go.mod | 4 +- go.sum | 6 ++- pkg/apis/config/image_policies_test.go | 4 +- .../clusterimagepolicy_types.go | 48 +++++++++++-------- pkg/cosign/kubernetes/webhook/validation.go | 11 ++--- .../kubernetes/webhook/validator_test.go | 3 +- 6 files changed, 43 insertions(+), 33 deletions(-) diff --git a/go.mod b/go.mod index d622821d8cf..b0f9a6da0b0 100644 --- a/go.mod +++ b/go.mod @@ -113,7 +113,7 @@ require ( cloud.google.com/go/kms v1.4.0 // indirect contrib.go.opencensus.io/exporter/ocagent v0.7.1-0.20200907061046-05415f1de66d // indirect contrib.go.opencensus.io/exporter/prometheus v0.4.0 // indirect - github.com/Azure/azure-sdk-for-go v63.0.0+incompatible // indirect + github.com/Azure/azure-sdk-for-go v63.2.0+incompatible // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest v0.11.25 // indirect github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect @@ -131,7 +131,7 @@ require ( github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/ReneKroon/ttlcache/v2 v2.11.0 // indirect github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect - github.com/aws/aws-sdk-go v1.43.30 // indirect + github.com/aws/aws-sdk-go v1.43.37 // indirect github.com/aws/aws-sdk-go-v2 v1.14.0 // indirect github.com/aws/aws-sdk-go-v2/config v1.14.0 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.9.0 // indirect diff --git a/go.sum b/go.sum index 538d3d7f87b..2d0f124866c 100644 --- a/go.sum +++ b/go.sum @@ -130,8 +130,9 @@ github.com/Azure/azure-sdk-for-go v59.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9mo github.com/Azure/azure-sdk-for-go v60.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v60.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v62.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go v63.0.0+incompatible h1:whPsa+jCHQSo5wGMPNLw4bz8q9Co2+vnXHzXGctoTaQ= github.com/Azure/azure-sdk-for-go v63.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v63.2.0+incompatible h1:OIqkK/zTGqVUuzpEvY0B1YSYDRAFC/j+y0w2GovCggI= +github.com/Azure/azure-sdk-for-go v63.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-service-bus-go v0.9.1/go.mod h1:yzBx6/BUGfjfeqbRZny9AQIbIe3AcV9WZbAdpkoXOa0= github.com/Azure/azure-service-bus-go v0.11.5/go.mod h1:MI6ge2CuQWBVq+ly456MY7XqNLJip5LO1iSFodbNLbU= github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0= @@ -323,8 +324,9 @@ github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zK github.com/aws/aws-sdk-go v1.42.8/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/aws/aws-sdk-go v1.42.22/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/aws/aws-sdk-go v1.42.25/go.mod h1:gyRszuZ/icHmHAVE4gc/r+cfCmhA1AD+vqfWbgI+eHs= -github.com/aws/aws-sdk-go v1.43.30 h1:Q3lgrX/tz/MkEiPVVQnOQThBAK2QC2SCTCKTD1mwGFA= github.com/aws/aws-sdk-go v1.43.30/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.43.37 h1:kyZ7UjaPZaCik+asF33UFOOYSwr9liDRr/UM/vuw8yY= +github.com/aws/aws-sdk-go v1.43.37/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/aws/aws-sdk-go-v2 v1.7.1/go.mod h1:L5LuPC1ZgDr2xQS7AmIec/Jlc7O/Y1u2KxJyNVab250= github.com/aws/aws-sdk-go-v2 v1.11.0/go.mod h1:SQfA+m2ltnu1cA0soUkj4dRSsmITiVQUJvBIZjzfPyQ= diff --git a/pkg/apis/config/image_policies_test.go b/pkg/apis/config/image_policies_test.go index 7c9c4015c44..ff7146b53b3 100644 --- a/pkg/apis/config/image_policies_test.go +++ b/pkg/apis/config/image_policies_test.go @@ -15,7 +15,7 @@ package config import ( - "crypto/ecdsa" + "crypto" "crypto/x509" "encoding/pem" "strings" @@ -143,7 +143,7 @@ func checkGetMatches(t *testing.T, c map[string][]webhookcip.Authority, err erro t.Error("Wanted a config and non-zero authorities, got no authorities") } -func checkPublicKey(t *testing.T, gotKey *ecdsa.PublicKey) { +func checkPublicKey(t *testing.T, gotKey crypto.PublicKey) { t.Helper() derBytes, err := x509.MarshalPKIXPublicKey(gotKey) diff --git a/pkg/cosign/kubernetes/webhook/clusterimagepolicy/clusterimagepolicy_types.go b/pkg/cosign/kubernetes/webhook/clusterimagepolicy/clusterimagepolicy_types.go index 41706d53fc1..4a06e9a2f67 100644 --- a/pkg/cosign/kubernetes/webhook/clusterimagepolicy/clusterimagepolicy_types.go +++ b/pkg/cosign/kubernetes/webhook/clusterimagepolicy/clusterimagepolicy_types.go @@ -15,7 +15,7 @@ package clusterimagepolicy import ( - "crypto/ecdsa" + "crypto" "crypto/x509" "encoding/json" "encoding/pem" @@ -55,7 +55,7 @@ type KeyRef struct { // PublicKeys are not marshalled because JSON unmarshalling // errors for *big.Int // +optional - PublicKeys []*ecdsa.PublicKey `json:"-"` + PublicKeys []crypto.PublicKey `json:"-"` } type KeylessRef struct { @@ -70,7 +70,7 @@ type KeylessRef struct { // UnmarshalJSON populates the PublicKeys using Data because // JSON unmashalling errors for *big.Int func (k *KeyRef) UnmarshalJSON(data []byte) error { - var publicKeys []*ecdsa.PublicKey + var publicKeys []crypto.PublicKey var err error ret := make(map[string]string) @@ -143,28 +143,36 @@ func convertKeylessRefV1Alpha1ToWebhook(in *v1alpha1.KeylessRef) *KeylessRef { } } -func ConvertKeyDataToPublicKeys(pubKey string) ([]*ecdsa.PublicKey, error) { - keys := []*ecdsa.PublicKey{} - pems := parsePems([]byte(pubKey)) +func parsePEMKey(b []byte) ([]*pem.Block, bool) { + pemKey, rest := pem.Decode(b) + valid := true + if pemKey == nil { + return nil, false + } + pemBlocks := []*pem.Block{pemKey} + + if len(rest) > 0 { + list, check := parsePEMKey(rest) + return append(pemBlocks, list...), check + } + return pemBlocks, valid +} + +func ConvertKeyDataToPublicKeys(pubKey string) ([]crypto.PublicKey, error) { + keys := []crypto.PublicKey{} + pems, validPEM := parsePEMKey([]byte(pubKey)) + if !validPEM { + // TODO: If it is not valid report the error instead of ignore the key + return keys, nil + } + for _, p := range pems { key, err := x509.ParsePKIXPublicKey(p.Bytes) if err != nil { return nil, err } - keys = append(keys, key.(*ecdsa.PublicKey)) + keys = append(keys, key.(crypto.PublicKey)) } - return keys, nil -} -func parsePems(b []byte) []*pem.Block { - p, rest := pem.Decode(b) - if p == nil { - return nil - } - pems := []*pem.Block{p} - - if rest != nil { - return append(pems, parsePems(rest)...) - } - return pems + return keys, nil } diff --git a/pkg/cosign/kubernetes/webhook/validation.go b/pkg/cosign/kubernetes/webhook/validation.go index 6a54ade1faa..f32bbc6068c 100644 --- a/pkg/cosign/kubernetes/webhook/validation.go +++ b/pkg/cosign/kubernetes/webhook/validation.go @@ -18,7 +18,6 @@ package webhook import ( "context" "crypto" - "crypto/ecdsa" "crypto/x509" "encoding/pem" "errors" @@ -37,7 +36,7 @@ import ( "github.com/sigstore/sigstore/pkg/signature" ) -func valid(ctx context.Context, ref name.Reference, keys []*ecdsa.PublicKey, opts ...ociremote.Option) ([]oci.Signature, error) { +func valid(ctx context.Context, ref name.Reference, keys []crypto.PublicKey, opts ...ociremote.Option) ([]oci.Signature, error) { if len(keys) == 0 { // If there are no keys, then verify against the fulcio root. sps, err := validSignaturesWithFulcio(ctx, ref, fulcioroots.Get(), nil /* rekor */, nil /* no identities */, opts...) @@ -52,7 +51,7 @@ func valid(ctx context.Context, ref name.Reference, keys []*ecdsa.PublicKey, opt // We return nil if ANY key matches var lastErr error for _, k := range keys { - verifier, err := signature.LoadECDSAVerifier(k, crypto.SHA256) + verifier, err := signature.LoadVerifier(k, crypto.SHA256) if err != nil { logging.FromContext(ctx).Errorf("error creating verifier: %v", err) lastErr = err @@ -98,8 +97,8 @@ func validSignaturesWithFulcio(ctx context.Context, ref name.Reference, fulcioRo return sigs, err } -func getKeys(ctx context.Context, cfg map[string][]byte) ([]*ecdsa.PublicKey, *apis.FieldError) { - keys := []*ecdsa.PublicKey{} +func getKeys(ctx context.Context, cfg map[string][]byte) ([]crypto.PublicKey, *apis.FieldError) { + keys := []crypto.PublicKey{} errs := []error{} logging.FromContext(ctx).Debugf("Got public key: %v", cfg["cosign.pub"]) @@ -111,7 +110,7 @@ func getKeys(ctx context.Context, cfg map[string][]byte) ([]*ecdsa.PublicKey, *a if err != nil { errs = append(errs, err) } else { - keys = append(keys, key.(*ecdsa.PublicKey)) + keys = append(keys, key.(crypto.PublicKey)) } } if keys == nil { diff --git a/pkg/cosign/kubernetes/webhook/validator_test.go b/pkg/cosign/kubernetes/webhook/validator_test.go index f66772f4766..6b1ca61fffc 100644 --- a/pkg/cosign/kubernetes/webhook/validator_test.go +++ b/pkg/cosign/kubernetes/webhook/validator_test.go @@ -18,6 +18,7 @@ package webhook import ( "bytes" "context" + "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/x509" @@ -240,7 +241,7 @@ UoJou2P8sbDxpLiE/v3yLw1/jyOrCPWYHWFXnyyeGlkgSVefG54tNoK7Uw== { Key: &webhookcip.KeyRef{ Data: authorityKeyCosignPubString, - PublicKeys: []*ecdsa.PublicKey{authorityKeyCosignPub}, + PublicKeys: []crypto.PublicKey{authorityKeyCosignPub}, }, }, }, From f89d69131b20e09eeffa5e707d40f35c99784378 Mon Sep 17 00:00:00 2001 From: Denny Date: Mon, 18 Apr 2022 13:36:25 -0400 Subject: [PATCH 25/53] Cosigned validate against remote sig src (#1754) Add github workflow steps for remote signatures Signed-off-by: Denny Hoang --- .../v1alpha1/clusterimagepolicy_validation.go | 14 ++++ .../clusterimagepolicy_validation_test.go | 50 ++++++++++++ .../clusterimagepolicy_types.go | 35 ++++++++ pkg/cosign/kubernetes/webhook/validator.go | 14 ++-- test/e2e_test_cluster_image_policy.sh | 81 ++++++++++++++++--- 5 files changed, 178 insertions(+), 16 deletions(-) diff --git a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go index c16db8736a7..81d0f2065fe 100644 --- a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go +++ b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go @@ -83,6 +83,12 @@ func (authority *Authority) Validate(ctx context.Context) *apis.FieldError { errs = errs.Also(authority.Keyless.Validate(ctx).ViaField("keyless")) } + if len(authority.Sources) > 0 { + for _, source := range authority.Sources { + errs = errs.Also(source.Validate(ctx).ViaField("source")) + } + } + return errs } @@ -130,6 +136,14 @@ func (keyless *KeylessRef) Validate(ctx context.Context) *apis.FieldError { return errs } +func (source *Source) Validate(ctx context.Context) *apis.FieldError { + var errs *apis.FieldError + if source.OCI == "" { + errs = errs.Also(apis.ErrMissingField("oci")) + } + return errs +} + func (identity *Identity) Validate(ctx context.Context) *apis.FieldError { var errs *apis.FieldError if identity.Issuer == "" && identity.Subject == "" { diff --git a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go index b3213cc67a8..5c288188e9d 100644 --- a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go +++ b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go @@ -397,7 +397,57 @@ func TestAuthoritiesValidation(t *testing.T) { }, }, }, + { + name: "Should pass when source oci is present", + expectErr: false, + policy: ClusterImagePolicy{ + Spec: ClusterImagePolicySpec{ + Images: []ImagePattern{{Regex: ".*"}}, + Authorities: []Authority{ + { + Key: &KeyRef{KMS: "kms://key/path"}, + Sources: []Source{{OCI: "registry.example.com"}}, + }, + }, + }, + }, + }, + { + name: "Should fail when source oci is empty", + expectErr: true, + errorString: "missing field(s): spec.authorities[0].source.oci", + policy: ClusterImagePolicy{ + Spec: ClusterImagePolicySpec{ + Images: []ImagePattern{{Regex: ".*"}}, + Authorities: []Authority{ + { + Key: &KeyRef{KMS: "kms://key/path"}, + Sources: []Source{{OCI: ""}}, + }, + }, + }, + }, + }, + { + name: "Should pass with multiple source oci is present", + expectErr: false, + policy: ClusterImagePolicy{ + Spec: ClusterImagePolicySpec{ + Images: []ImagePattern{{Regex: ".*"}}, + Authorities: []Authority{ + { + Key: &KeyRef{KMS: "kms://key/path"}, + Sources: []Source{ + {OCI: "registry1"}, + {OCI: "registry2"}, + }, + }, + }, + }, + }, + }, } + for _, test := range tests { t.Run(test.name, func(t *testing.T) { err := test.policy.Validate(context.TODO()) diff --git a/pkg/cosign/kubernetes/webhook/clusterimagepolicy/clusterimagepolicy_types.go b/pkg/cosign/kubernetes/webhook/clusterimagepolicy/clusterimagepolicy_types.go index 4a06e9a2f67..5da8c7f83f2 100644 --- a/pkg/cosign/kubernetes/webhook/clusterimagepolicy/clusterimagepolicy_types.go +++ b/pkg/cosign/kubernetes/webhook/clusterimagepolicy/clusterimagepolicy_types.go @@ -20,7 +20,10 @@ import ( "encoding/json" "encoding/pem" + "github.com/google/go-containerregistry/pkg/name" + "github.com/pkg/errors" "github.com/sigstore/cosign/pkg/apis/cosigned/v1alpha1" + "github.com/sigstore/cosign/pkg/oci/remote" "knative.dev/pkg/apis" ) @@ -44,6 +47,10 @@ type Authority struct { Sources []v1alpha1.Source `json:"source,omitempty"` // +optional CTLog *v1alpha1.TLog `json:"ctlog,omitempty"` + // RemoteOpts are not marshalled because they are an unsupported type + // RemoteOpts will be populated by the Authority UnmarshalJSON override + // +optional + RemoteOpts []remote.Option `json:"-"` } // This references a public verification key stored in @@ -92,6 +99,34 @@ func (k *KeyRef) UnmarshalJSON(data []byte) error { return nil } +// UnmarshalJSON populates the authority with the remoteOpts +// from authority sources +func (a *Authority) UnmarshalJSON(data []byte) error { + // Create a new type to avoid recursion + type RawAuthority Authority + + var rawAuthority RawAuthority + err := json.Unmarshal(data, &rawAuthority) + if err != nil { + return err + } + + // Determine additional RemoteOpts + if len(rawAuthority.Sources) > 0 { + for _, source := range rawAuthority.Sources { + if targetRepoOverride, err := name.NewRepository(source.OCI); err != nil { + return errors.Wrap(err, "failed to determine source") + } else if (targetRepoOverride != name.Repository{}) { + rawAuthority.RemoteOpts = append(rawAuthority.RemoteOpts, remote.WithTargetRepository(targetRepoOverride)) + } + } + } + + // Set the new type instance to casted original + *a = Authority(rawAuthority) + return nil +} + func ConvertClusterImagePolicyV1alpha1ToWebhook(in *v1alpha1.ClusterImagePolicy) *ClusterImagePolicy { copyIn := in.DeepCopy() diff --git a/pkg/cosign/kubernetes/webhook/validator.go b/pkg/cosign/kubernetes/webhook/validator.go index bd95f87623f..dc9ad4c037e 100644 --- a/pkg/cosign/kubernetes/webhook/validator.go +++ b/pkg/cosign/kubernetes/webhook/validator.go @@ -257,15 +257,15 @@ func validatePolicies(ctx context.Context, ref name.Reference, kc authn.Keychain // return a success if at least one of the Authorities validated the signatures. // Returns the validated signatures, or the errors encountered. func ValidatePolicy(ctx context.Context, ref name.Reference, kc authn.Keychain, authorities []webhookcip.Authority, remoteOpts ...ociremote.Option) ([]oci.Signature, []error) { + remoteOpts = append(remoteOpts, ociremote.WithRemoteOptions(remote.WithAuthFromKeychain(kc))) + // If none of the Authorities for a given policy pass the checks, gather // the errors here. If one passes, do not return the errors. authorityErrors := []error{} for _, authority := range authorities { - logging.FromContext(ctx).Debugf("Checking Authority: %+v", authority) - // TODO(vaikas): We currently only use the kc, we have to look - // at authority.Sources to determine additional information for the - // WithRemoteOptions below, at least the 'TargetRepository' - // https://github.com/sigstore/cosign/issues/1651 + // Assignment for appendAssign lint error + authorityRemoteOpts := remoteOpts + authorityRemoteOpts = append(authorityRemoteOpts, authority.RemoteOpts...) switch { case authority.Key != nil && len(authority.Key.PublicKeys) > 0: @@ -273,7 +273,7 @@ func ValidatePolicy(ctx context.Context, ref name.Reference, kc authn.Keychain, // Is it even allowed? 'valid' returns success if any key // matches. // https://github.com/sigstore/cosign/issues/1652 - sps, err := valid(ctx, ref, authority.Key.PublicKeys, remoteOpts...) + sps, err := valid(ctx, ref, authority.Key.PublicKeys, authorityRemoteOpts...) if err != nil { authorityErrors = append(authorityErrors, errors.Wrap(err, "failed to validate keys")) continue @@ -303,7 +303,7 @@ func ValidatePolicy(ctx context.Context, ref name.Reference, kc authn.Keychain, continue } } - sps, err := validSignaturesWithFulcio(ctx, ref, fulcioroot, rekorClient, authority.Keyless.Identities, remoteOpts...) + sps, err := validSignaturesWithFulcio(ctx, ref, fulcioroot, rekorClient, authority.Keyless.Identities, authorityRemoteOpts...) if err != nil { logging.FromContext(ctx).Errorf("failed validSignatures with fulcio for %s: %v", ref.Name(), err) authorityErrors = append(authorityErrors, errors.Wrap(err, "validate signatures with fulcio")) diff --git a/test/e2e_test_cluster_image_policy.sh b/test/e2e_test_cluster_image_policy.sh index 0f53f7ae141..45eb1712bb5 100755 --- a/test/e2e_test_cluster_image_policy.sh +++ b/test/e2e_test_cluster_image_policy.sh @@ -165,14 +165,22 @@ kubectl delete cip image-policy-keyless-with-identities-mismatch sleep 5 echo '::endgroup::' -echo '::group:: Generate signing key' +echo '::group:: Generate New Signing Key For Colocated Signature' COSIGN_PASSWORD="" ./cosign generate-key-pair +mv cosign.key cosign-colocated-signing.key +mv cosign.pub cosign-colocated-signing.pub echo '::endgroup::' echo '::group:: Deploy ClusterImagePolicy With Key Signing' -yq '. | .spec.authorities[0].key.data |= load_str("cosign.pub")' ./test/testdata/cosigned/e2e/cip-key.yaml | kubectl apply -f - +yq '. | .spec.authorities[0].key.data |= load_str("cosign-colocated-signing.pub")' \ + ./test/testdata/cosigned/e2e/cip-key.yaml | \ + kubectl apply -f - echo '::endgroup::' +echo '::group:: Create and label new namespace for verification' +kubectl create namespace demo-key-signing +kubectl label namespace demo-key-signing cosigned.sigstore.dev/include=true + echo '::group:: Verify blocks unsigned with the key' if kubectl create -n demo-key-signing job demo --image=${demoimage}; then echo Failed to block unsigned Job creation! @@ -181,17 +189,13 @@ fi echo '::endgroup::' echo '::group:: Sign demoimage with cosign key' -COSIGN_PASSWORD="" ./cosign sign --key cosign.key --force --allow-insecure-registry ${demoimage} +COSIGN_PASSWORD="" ./cosign sign --key cosign-colocated-signing.key --force --allow-insecure-registry ${demoimage} echo '::endgroup::' echo '::group:: Verify demoimage with cosign key' -./cosign verify --key cosign.pub --allow-insecure-registry ${demoimage} +./cosign verify --key cosign-colocated-signing.pub --allow-insecure-registry ${demoimage} echo '::endgroup::' -echo '::group:: Create and label new namespace for verification' -kubectl create namespace demo-key-signing -kubectl label namespace demo-key-signing cosigned.sigstore.dev/include=true - echo '::group:: test job success' # We signed this above, this should work if ! kubectl create -n demo-key-signing job demo --image=${demoimage} ; then @@ -212,7 +216,66 @@ else fi echo '::endgroup::' +echo '::group:: Generate New Signing Key For Remote Signature' +COSIGN_PASSWORD="" ./cosign generate-key-pair +mv cosign.key cosign-remote-signing.key +mv cosign.pub cosign-remote-signing.pub +echo '::endgroup::' + +echo '::group:: Deploy ClusterImagePolicy With Remote Public Key But Missing Source' +yq '. | .metadata.name = "image-policy-remote-source" + | .spec.authorities[0].key.data |= load_str("cosign-remote-signing.pub")' \ + ./test/testdata/cosigned/e2e/cip-key.yaml | \ + kubectl apply -f - +echo '::endgroup::' + +echo '::group:: Sign demoimage with cosign remote key' +COSIGN_REPOSITORY="${KO_DOCKER_REPO}/remote-signature" ./cosign sign --key cosign-remote-signing.key --force --allow-insecure-registry ${demoimage} +echo '::endgroup::' + +echo '::group:: Verify demoimage with cosign remote key' +if ./cosign verify --key cosign-remote-signing.pub --allow-insecure-registry ${demoimage}; then + echo "Signature should not have been verified unless COSIGN_REPOSITORY was defined" + exit 1 +fi + +if ! COSIGN_REPOSITORY="${KO_DOCKER_REPO}/remote-signature" ./cosign verify --key cosign-remote-signing.pub --allow-insecure-registry ${demoimage}; then + echo "Signature should have been verified when COSIGN_REPOSITORY was defined" + exit 1 +fi +echo '::endgroup::' + +echo '::group:: Create test namespace and label for remote key verification' +kubectl create namespace demo-key-remote +kubectl label namespace demo-key-remote cosigned.sigstore.dev/include=true +echo '::endgroup::' + +echo '::group:: Verify with three CIP, one without correct Source set' +if kubectl create -n demo-key-remote job demo --image=${demoimage}; then + echo Failed to block unsigned Job creation! + exit 1 +fi +echo '::endgroup::' + +echo '::group:: Deploy ClusterImagePolicy With Remote Public Key With Source' +yq '. | .metadata.name = "image-policy-remote-source" + | .spec.authorities[0].key.data |= load_str("cosign-remote-signing.pub") + | .spec.authorities[0] += {"source": [{"oci": env(KO_DOCKER_REPO)+"/remote-signature"}]}' \ + ./test/testdata/cosigned/e2e/cip-key.yaml | \ + kubectl apply -f - +echo '::endgroup::' + +echo '::group:: Verify with three CIP, one with correct Source set' +# We signed this above and applied remote signature source location above +if ! kubectl create -n demo-key-remote job demo --image=${demoimage}; then + echo Failed to create Job in namespace without label! + exit 1 +else + echo Succcessfully created Job with signed image +fi +echo '::endgroup::' + echo '::group::' Cleanup kubectl delete cip --all kubectl delete ns demo-key-signing demo-keyless-signing -rm cosign.key cosign.pub +rm cosign*.key cosign*.pub From 623d50f9b77ee85886a166daac648455e65003ec Mon Sep 17 00:00:00 2001 From: Hayden B Date: Mon, 18 Apr 2022 17:02:41 -0700 Subject: [PATCH 26/53] Add Fulcio intermediate CA certificate to intermediate pool (#1774) This certificate will be necessary for chain building from a leaf certificate to a root once a new version of Fulcio is rolled out. For OCI, the chain is stored in an annotation. This intermediate is currently only needed for verify-blob when looking up the certificate from Rekor. For the V3 TUF Root, the intermediate will be bundled, so that it is easily discoverable and revokable. For now, we'll simply bundle it with Cosign. Note that intermediates are considered untrusted, so it's fine if the intermediate is not in TUF currently, as the root that issued the intermediate certificate is in TUF. Signed-off-by: Hayden Blauzvern --- .../cli/fulcio/fulcioroots/fulcioroots.go | 18 ++++++++++++++++++ .../cli/fulcio/fulcioroots/fulcioroots_test.go | 3 +-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots.go b/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots.go index 9b48f2b2d74..db1fc460c7e 100644 --- a/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots.go +++ b/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots.go @@ -39,6 +39,23 @@ var fulcioTargetStr = `fulcio.crt.pem` // This is the v1 migrated root. var fulcioV1TargetStr = `fulcio_v1.crt.pem` +// The untrusted intermediate CA certificate, used for chain building +// TODO: Remove once this is bundled in TUF metadata. +var fulcioIntermediateV1 = `-----BEGIN CERTIFICATE----- +MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMw +KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y +MjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3Jl +LmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0C +AQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV7 +7LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS +0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYB +BQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjp +KFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZI +zj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJR +nZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsP +mygUY7Ii2zbdCdliiow= +-----END CERTIFICATE-----` + const ( altRoot = "SIGSTORE_ROOT_FILE" ) @@ -116,6 +133,7 @@ func initRoots() (*x509.CertPool, *x509.CertPool, error) { } } } + intermediatePool.AppendCertsFromPEM([]byte(fulcioIntermediateV1)) } return rootPool, intermediatePool, nil } diff --git a/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots_test.go b/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots_test.go index 9071db8cd4a..b400f453a82 100644 --- a/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots_test.go +++ b/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots_test.go @@ -40,8 +40,7 @@ func TestGetFulcioRoots(t *testing.T) { if _, err := tmpCertFile.Write(chain); err != nil { t.Fatalf("failed to write cert file: %v", err) } - os.Setenv("SIGSTORE_ROOT_FILE", tmpCertFile.Name()) - defer os.Unsetenv("SIGSTORE_ROOT_FILE") + t.Setenv("SIGSTORE_ROOT_FILE", tmpCertFile.Name()) rootCertPool := Get() // ignore deprecation error because certificates do not contain from SystemCertPool From b26191a735d551d3ba00a19190c61e2448e6f714 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Apr 2022 11:45:11 -0500 Subject: [PATCH 27/53] Bump codecov/codecov-action from 3.0.0 to 3.1.0 (#1784) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3.0.0 to 3.1.0. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/e3c560433a6cc60aec8812599b7844a7b4fa0d71...81cd2dc8148241f03f5839d295e000b8f761e378) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index d6e525b4be5..dae13f62d0a 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -61,7 +61,7 @@ jobs: - name: Run Go tests run: go test -covermode atomic -coverprofile coverage.txt $(go list ./... | grep -v third_party/) - name: Upload Coverage Report - uses: codecov/codecov-action@e3c560433a6cc60aec8812599b7844a7b4fa0d71 # v2.1.0 + uses: codecov/codecov-action@81cd2dc8148241f03f5839d295e000b8f761e378 # v2.1.0 with: env_vars: OS - name: Run Go tests w/ `-race` From 68bbaad8378377d50425e9b1b15ae51b641434e3 Mon Sep 17 00:00:00 2001 From: Youssef Bel Mekki <38552193+ybelMekk@users.noreply.github.com> Date: Thu, 21 Apr 2022 18:45:24 +0200 Subject: [PATCH 28/53] fix: more informative error (#1778) When running the attach attestation command with empty file input the cli returns hard to understand output like: ``` file name too long Using payload from: filename.json Error: unexpected end of JSON input main.go:46: error during command execution: unexpected end of JSON input ``` By some investigation I found out that attestation file was empty, yes this is a user-error, so I recon it could be more informative as it throws the error while parsing the input making the real error a bit shaded. Signed-off-by: ybelMekk --- cmd/cosign/cli/attach/attestation.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cmd/cosign/cli/attach/attestation.go b/cmd/cosign/cli/attach/attestation.go index 47255aa966c..e6f35c7527c 100644 --- a/cmd/cosign/cli/attach/attestation.go +++ b/cmd/cosign/cli/attach/attestation.go @@ -42,6 +42,10 @@ func AttestationCmd(ctx context.Context, regOpts options.RegistryOptions, signed return err } + if len(payload) == 0 { + return fmt.Errorf("%s payload is empty", signedPayload) + } + env := ssldsse.Envelope{} if err := json.Unmarshal(payload, &env); err != nil { return err From 0a4dffb5928fca366ebe175532bea9b4feb0985e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Apr 2022 11:46:03 -0500 Subject: [PATCH 29/53] Bump cuelang.org/go from 0.4.2 to 0.4.3 (#1779) Bumps [cuelang.org/go](https://github.com/cue-lang/cue) from 0.4.2 to 0.4.3. - [Release notes](https://github.com/cue-lang/cue/releases) - [Changelog](https://github.com/cue-lang/cue/blob/master/.goreleaser.yml) - [Commits](https://github.com/cue-lang/cue/compare/v0.4.2...v0.4.3) --- updated-dependencies: - dependency-name: cuelang.org/go dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index b0f9a6da0b0..6bcbe3888e9 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.17 require ( cloud.google.com/go/storage v1.22.0 - cuelang.org/go v0.4.2 + cuelang.org/go v0.4.3 github.com/ThalesIgnite/crypto11 v1.2.5 github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20220228164355-396b2034c795 github.com/cenkalti/backoff/v3 v3.2.2 diff --git a/go.sum b/go.sum index 2d0f124866c..be1c58f6ec1 100644 --- a/go.sum +++ b/go.sum @@ -108,8 +108,8 @@ contrib.go.opencensus.io/exporter/zipkin v0.1.2/go.mod h1:mP5xM3rrgOjpn79MM8fZbj contrib.go.opencensus.io/integrations/ocsql v0.1.4/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE= contrib.go.opencensus.io/integrations/ocsql v0.1.7/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE= contrib.go.opencensus.io/resource v0.1.1/go.mod h1:F361eGI91LCmW1I/Saf+rX0+OFcigGlFvXwEGEnkRLA= -cuelang.org/go v0.4.2 h1:l+ptgjryFJ/aikhEMSem36LoWkNi6YNFmsERW2hgww4= -cuelang.org/go v0.4.2/go.mod h1:P09/R4UfAEzLkV9DXxwlxQnIZbkaT4uIhiEgs6Vsz2Q= +cuelang.org/go v0.4.3 h1:W3oBBjDTm7+IZfCKZAmC8uDG0eYfJL4Pp/xbbCMKaVo= +cuelang.org/go v0.4.3/go.mod h1:7805vR9H+VoBNdWFdI7jyDR3QLUPp4+naHfbcgp55HI= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= @@ -1971,8 +1971,9 @@ github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM= github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= From 5efdd115344177cd521e235c2d349303d0ba504c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Apr 2022 11:46:27 -0500 Subject: [PATCH 30/53] Bump google.golang.org/api from 0.74.0 to 0.75.0 (#1780) Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.74.0 to 0.75.0. - [Release notes](https://github.com/googleapis/google-api-go-client/releases) - [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md) - [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.74.0...v0.75.0) --- updated-dependencies: - dependency-name: google.golang.org/api dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 10 +++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 6bcbe3888e9..bcd41824bbd 100644 --- a/go.mod +++ b/go.mod @@ -51,7 +51,7 @@ require ( golang.org/x/sys v0.0.0-20220412211240-33da011f77ad golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 - google.golang.org/api v0.74.0 + google.golang.org/api v0.75.0 gopkg.in/square/go-jose.v2 v2.6.0 gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.23.5 @@ -108,7 +108,7 @@ require ( require ( bitbucket.org/creachadair/shell v0.0.6 // indirect cloud.google.com/go v0.100.2 // indirect - cloud.google.com/go/compute v1.5.0 // indirect + cloud.google.com/go/compute v1.6.0 // indirect cloud.google.com/go/iam v0.3.0 // indirect cloud.google.com/go/kms v1.4.0 // indirect contrib.go.opencensus.io/exporter/ocagent v0.7.1-0.20200907061046-05415f1de66d // indirect @@ -302,7 +302,7 @@ require ( golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac // indirect + google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4 // indirect gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.66.4 // indirect diff --git a/go.sum b/go.sum index be1c58f6ec1..9c3eed16ba4 100644 --- a/go.sum +++ b/go.sum @@ -51,8 +51,9 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= -cloud.google.com/go/compute v1.5.0 h1:b1zWmYuuHz7gO9kDcM/EpHGr06UgsYNRpNJzI2kFiLM= cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0 h1:XdQIN5mdPTSBVwSIVDuY5e8ZzVAccsHvD3qTEz4zIps= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= @@ -3029,8 +3030,9 @@ google.golang.org/api v0.65.0/go.mod h1:ArYhxgGadlWmqO1IqVujw6Cs8IdD33bTmzKo2Sh+ google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= -google.golang.org/api v0.74.0 h1:ExR2D+5TYIrMphWgs5JCgwRhEDlPDXXrLwHHMgPHTXE= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0 h1:0AYh/ae6l9TDUvIQrDw5QRpM100P6oHgD+o3dYHMzJg= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -3150,8 +3152,10 @@ google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2 google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac h1:qSNTkEN+L2mvWcLgJOR+8bdHX9rN/IdU3A1Ghpfb1Rg= google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4 h1:myaecH64R0bIEDjNORIel4iXubqzaHU1K2z8ajBwWcM= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= From acfd6cb2e61352fac740e85701e7d386653e7ac4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Apr 2022 11:46:49 -0500 Subject: [PATCH 31/53] Bump k8s.io/code-generator from 0.23.5 to 0.23.6 (#1781) Bumps [k8s.io/code-generator](https://github.com/kubernetes/code-generator) from 0.23.5 to 0.23.6. - [Release notes](https://github.com/kubernetes/code-generator/releases) - [Commits](https://github.com/kubernetes/code-generator/compare/v0.23.5...v0.23.6) --- updated-dependencies: - dependency-name: k8s.io/code-generator dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index bcd41824bbd..749fb7f2837 100644 --- a/go.mod +++ b/go.mod @@ -99,7 +99,7 @@ require ( golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 google.golang.org/grpc v1.45.0 google.golang.org/protobuf v1.28.0 - k8s.io/code-generator v0.23.5 + k8s.io/code-generator v0.23.6 k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf knative.dev/hack v0.0.0-20220224013837-e1785985d364 knative.dev/hack/schema v0.0.0-20220224013837-e1785985d364 diff --git a/go.sum b/go.sum index 9c3eed16ba4..485d3bab7a8 100644 --- a/go.sum +++ b/go.sum @@ -3321,8 +3321,9 @@ k8s.io/client-go v0.23.4/go.mod h1:PKnIL4pqLuvYUK1WU7RLTMYKPiIh7MYShLshtRY9cj0= k8s.io/client-go v0.23.5 h1:zUXHmEuqx0RY4+CsnkOn5l0GU+skkRXKGJrhmE2SLd8= k8s.io/client-go v0.23.5/go.mod h1:flkeinTO1CirYgzMPRWxUCnV0G4Fbu2vLhYCObnt/r4= k8s.io/code-generator v0.23.4/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= -k8s.io/code-generator v0.23.5 h1:xn3a6J5pUL49AoH6SPrOFtnB5cvdMl76f/bEY176R3c= k8s.io/code-generator v0.23.5/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= +k8s.io/code-generator v0.23.6 h1:4J4zL5TU7e96kjGvr5LOFsgR1P9ZU/C6EQeGYcNrFvw= +k8s.io/code-generator v0.23.6/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk= k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI= k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM= From 907657b579b068008401e44776f26d888fdc7c69 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Apr 2022 11:47:09 -0500 Subject: [PATCH 32/53] Bump github.com/mitchellh/mapstructure from 1.4.3 to 1.5.0 (#1782) Bumps [github.com/mitchellh/mapstructure](https://github.com/mitchellh/mapstructure) from 1.4.3 to 1.5.0. - [Release notes](https://github.com/mitchellh/mapstructure/releases) - [Changelog](https://github.com/mitchellh/mapstructure/blob/master/CHANGELOG.md) - [Commits](https://github.com/mitchellh/mapstructure/compare/v1.4.3...v1.5.0) --- updated-dependencies: - dependency-name: github.com/mitchellh/mapstructure dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 749fb7f2837..0bf5857d0fa 100644 --- a/go.mod +++ b/go.mod @@ -86,7 +86,7 @@ require ( github.com/mattn/go-runewidth v0.0.13 // indirect github.com/mitchellh/copystructure v1.2.0 github.com/mitchellh/go-testing-interface v1.14.1 - github.com/mitchellh/mapstructure v1.4.3 + github.com/mitchellh/mapstructure v1.5.0 github.com/pierrec/lz4 v2.6.1+incompatible github.com/prometheus/procfs v0.7.3 // indirect github.com/spf13/afero v1.8.2 // indirect diff --git a/go.sum b/go.sum index 485d3bab7a8..3fa87f6e4f9 100644 --- a/go.sum +++ b/go.sum @@ -1696,8 +1696,9 @@ github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/mapstructure v1.4.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= From 3c2084c30c7e8a54e90f380bd434b355ceea1605 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Apr 2022 11:47:39 -0500 Subject: [PATCH 33/53] Bump actions/checkout from 3.0.1 to 3.0.2 (#1783) Bumps [actions/checkout](https://github.com/actions/checkout) from 3.0.1 to 3.0.2. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/dcd71f646680f2efd8db4afa5ad64fdcba30e748...2541b1294d2704b0964813337f33b291d3f8596b) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yaml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/cross.yaml | 2 +- .github/workflows/donotsubmit.yaml | 2 +- .github/workflows/e2e-with-binary.yml | 2 +- .github/workflows/e2e_tests.yml | 2 +- .github/workflows/github-oidc.yaml | 2 +- .github/workflows/kind-cluster-image-policy.yaml | 2 +- .github/workflows/kind-e2e-cosigned.yaml | 2 +- .github/workflows/kind-verify-attestation.yaml | 2 +- .github/workflows/scorecard_action.yml | 2 +- .github/workflows/style.yaml | 4 ++-- .github/workflows/tests.yaml | 10 +++++----- .github/workflows/validate-release.yml | 2 +- .github/workflows/verify-codegen.yaml | 2 +- .github/workflows/verify-docgen.yaml | 2 +- .github/workflows/whitespace.yaml | 2 +- 17 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 69d9194d692..7fe33260229 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -33,7 +33,7 @@ jobs: contents: read steps: - - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - uses: sigstore/cosign-installer@bb61838e7ee5bf314f85f2e219b3706835fa6306 # v2.0.1 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 4f9d993e68d..b7fcebd959f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,7 +39,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - name: Utilize Go Module Cache uses: actions/cache@48af2dc4a9e8278b89d7fa154b955c30c6aaab09 # v3.0.2 diff --git a/.github/workflows/cross.yaml b/.github/workflows/cross.yaml index e265fb8d55e..e501b45ec3a 100644 --- a/.github/workflows/cross.yaml +++ b/.github/workflows/cross.yaml @@ -33,7 +33,7 @@ jobs: with: go-version: '1.17.x' - name: Checkout code - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - name: build cosign run: | make cosign && mv ./cosign ./${{matrix.COSIGN_TARGET}} diff --git a/.github/workflows/donotsubmit.yaml b/.github/workflows/donotsubmit.yaml index a64d27bb876..c92b095dd29 100644 --- a/.github/workflows/donotsubmit.yaml +++ b/.github/workflows/donotsubmit.yaml @@ -14,7 +14,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 #v2.4.0 + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b #v2.4.0 - name: Do Not Submit uses: chainguard-dev/actions/donotsubmit@84c993eaf02da1c325854fb272a4df9184bd80fc # main diff --git a/.github/workflows/e2e-with-binary.yml b/.github/workflows/e2e-with-binary.yml index 2378c17a9a8..4c1a81ab22c 100644 --- a/.github/workflows/e2e-with-binary.yml +++ b/.github/workflows/e2e-with-binary.yml @@ -38,7 +38,7 @@ jobs: COSIGN_EXPERIMENTAL: "true" steps: - - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 with: go-version: '1.17.x' diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index 9bece8b4e80..d3ab0597bd3 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -31,7 +31,7 @@ jobs: os: [macos-latest, ubuntu-latest, windows-latest] steps: - - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 with: go-version: '1.17.x' diff --git a/.github/workflows/github-oidc.yaml b/.github/workflows/github-oidc.yaml index d2bd092c7f8..8d62acda63a 100644 --- a/.github/workflows/github-oidc.yaml +++ b/.github/workflows/github-oidc.yaml @@ -35,7 +35,7 @@ jobs: KO_PREFIX: ghcr.io/${{ github.repository }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 with: go-version: '1.17.x' diff --git a/.github/workflows/kind-cluster-image-policy.yaml b/.github/workflows/kind-cluster-image-policy.yaml index 44ae852b725..cacedf1c2c3 100644 --- a/.github/workflows/kind-cluster-image-policy.yaml +++ b/.github/workflows/kind-cluster-image-policy.yaml @@ -48,7 +48,7 @@ jobs: COSIGN_EXPERIMENTAL: true steps: - - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 with: go-version: '1.17.x' diff --git a/.github/workflows/kind-e2e-cosigned.yaml b/.github/workflows/kind-e2e-cosigned.yaml index 25bc7520f60..555a2e9f7b6 100644 --- a/.github/workflows/kind-e2e-cosigned.yaml +++ b/.github/workflows/kind-e2e-cosigned.yaml @@ -43,7 +43,7 @@ jobs: KO_DOCKER_REPO: registry.local:5000/cosigned steps: - - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 with: go-version: 1.17.x diff --git a/.github/workflows/kind-verify-attestation.yaml b/.github/workflows/kind-verify-attestation.yaml index 3e953c64122..a72e8a71fe6 100644 --- a/.github/workflows/kind-verify-attestation.yaml +++ b/.github/workflows/kind-verify-attestation.yaml @@ -51,7 +51,7 @@ jobs: COSIGN_EXPERIMENTAL: "true" steps: - - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 with: go-version: '1.17.x' diff --git a/.github/workflows/scorecard_action.yml b/.github/workflows/scorecard_action.yml index 2b5a48bbc18..897ce5ea8a7 100644 --- a/.github/workflows/scorecard_action.yml +++ b/.github/workflows/scorecard_action.yml @@ -23,7 +23,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 with: persist-credentials: false diff --git a/.github/workflows/style.yaml b/.github/workflows/style.yaml index de015e60a72..349f58c3ce7 100644 --- a/.github/workflows/style.yaml +++ b/.github/workflows/style.yaml @@ -18,7 +18,7 @@ jobs: go-version: 1.16.x - name: Check out code - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - uses: chainguard-dev/actions/gofmt@84c993eaf02da1c325854fb272a4df9184bd80fc # main with: @@ -35,6 +35,6 @@ jobs: go-version: 1.16.x - name: Check out code - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - uses: chainguard-dev/actions/goimports@84c993eaf02da1c325854fb272a4df9184bd80fc # main diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index dae13f62d0a..4dafc04aa55 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -38,7 +38,7 @@ jobs: OS: ${{ matrix.os }} steps: - - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 # https://github.com/mvdan/github-actions-golang#how-do-i-set-up-caching-between-builds - uses: actions/cache@48af2dc4a9e8278b89d7fa154b955c30c6aaab09 # v3.0.2 with: @@ -73,7 +73,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 # https://github.com/mvdan/github-actions-golang#how-do-i-set-up-caching-between-builds - uses: actions/cache@48af2dc4a9e8278b89d7fa154b955c30c6aaab09 # v3.0.2 with: @@ -111,7 +111,7 @@ jobs: name: Run PowerShell E2E tests runs-on: windows-latest steps: - - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.1.5 with: go-version: ${{ env.GO_VERSION }} @@ -136,7 +136,7 @@ jobs: name: license boilerplate check runs-on: ubuntu-latest steps: - - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 with: go-version: ${{ env.GO_VERSION }} @@ -151,7 +151,7 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 with: go-version: ${{ env.GO_VERSION }} diff --git a/.github/workflows/validate-release.yml b/.github/workflows/validate-release.yml index 8e08a79ccef..7bb34d48668 100644 --- a/.github/workflows/validate-release.yml +++ b/.github/workflows/validate-release.yml @@ -43,7 +43,7 @@ jobs: COSIGN_IMAGE: gcr.io/projectsigstore/cosign:v1.7.2@sha256:ad2985a87622d5934a4bc06a61faadff772e377937e42519af4f506e1b019d1e steps: - - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 #v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b #v2.4.0 - name: Check Signature run: | diff --git a/.github/workflows/verify-codegen.yaml b/.github/workflows/verify-codegen.yaml index 53fa4ff45d2..f9708711763 100644 --- a/.github/workflows/verify-codegen.yaml +++ b/.github/workflows/verify-codegen.yaml @@ -36,7 +36,7 @@ jobs: with: go-version: 1.17.x - - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b with: path: ./src/github.com/${{ github.repository }} fetch-depth: 0 diff --git a/.github/workflows/verify-docgen.yaml b/.github/workflows/verify-docgen.yaml index da3da6064f3..a247277967c 100644 --- a/.github/workflows/verify-docgen.yaml +++ b/.github/workflows/verify-docgen.yaml @@ -31,7 +31,7 @@ jobs: steps: - name: deps run: sudo apt-get update && sudo apt-get install -yq libpcsclite-dev - - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.4.0 - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 with: go-version: '1.17.x' diff --git a/.github/workflows/whitespace.yaml b/.github/workflows/whitespace.yaml index 7ebb3638d53..4c9833de92d 100644 --- a/.github/workflows/whitespace.yaml +++ b/.github/workflows/whitespace.yaml @@ -14,7 +14,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 #v2.4.0 + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b #v2.4.0 - uses: chainguard-dev/actions/trailing-space@84c993eaf02da1c325854fb272a4df9184bd80fc # main if: ${{ always() }} From d9b4da1eac1c33dcba3757a89c8866da751313d1 Mon Sep 17 00:00:00 2001 From: Billy Lynch Date: Fri, 22 Apr 2022 15:20:20 -0400 Subject: [PATCH 34/53] Run update-codegen. (#1789) Signed-off-by: Billy Lynch --- pkg/client/clientset/versioned/clientset.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/client/clientset/versioned/clientset.go b/pkg/client/clientset/versioned/clientset.go index 463dca6b1a1..9b3e1a1f1c0 100644 --- a/pkg/client/clientset/versioned/clientset.go +++ b/pkg/client/clientset/versioned/clientset.go @@ -59,6 +59,10 @@ func (c *Clientset) Discovery() discovery.DiscoveryInterface { func NewForConfig(c *rest.Config) (*Clientset, error) { configShallowCopy := *c + if configShallowCopy.UserAgent == "" { + configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent() + } + // share the transport between all clients httpClient, err := rest.HTTPClientFor(&configShallowCopy) if err != nil { From afaf0a308b35d8d43b008d92d8563430fe556687 Mon Sep 17 00:00:00 2001 From: Ville Aikas <11279988+vaikas@users.noreply.github.com> Date: Fri, 22 Apr 2022 13:42:07 -0700 Subject: [PATCH 35/53] Remove the dependency on v1alpha1.Identity which brings in (#1790) unnecessary k8s libraries. Brought up as an issue that was merged in: #1759 Signed-off-by: Ville Aikas --- pkg/cosign/kubernetes/webhook/validation.go | 6 ++++- pkg/cosign/verify.go | 11 ++++++--- pkg/cosign/verify_test.go | 25 ++++++++++----------- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/pkg/cosign/kubernetes/webhook/validation.go b/pkg/cosign/kubernetes/webhook/validation.go index f32bbc6068c..be74173daa7 100644 --- a/pkg/cosign/kubernetes/webhook/validation.go +++ b/pkg/cosign/kubernetes/webhook/validation.go @@ -87,12 +87,16 @@ func validSignatures(ctx context.Context, ref name.Reference, verifier signature // validSignaturesWithFulcio expects a Fulcio Cert to verify against. An // optional rekorClient can also be given, if nil passed, default is assumed. func validSignaturesWithFulcio(ctx context.Context, ref name.Reference, fulcioRoots *x509.CertPool, rekorClient *client.Rekor, identities []v1alpha1.Identity, opts ...ociremote.Option) ([]oci.Signature, error) { + ids := make([]cosign.Identity, len(identities)) + for i, id := range identities { + ids[i] = cosign.Identity{Issuer: id.Issuer, Subject: id.Subject} + } sigs, _, err := cosignVerifySignatures(ctx, ref, &cosign.CheckOpts{ RegistryClientOpts: opts, RootCerts: fulcioRoots, RekorClient: rekorClient, ClaimVerifier: cosign.SimpleClaimVerifier, - Identities: identities, + Identities: ids, }) return sigs, err } diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index b00ed733c75..f60c40ead31 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -32,7 +32,6 @@ import ( "time" "github.com/sigstore/cosign/cmd/cosign/cli/fulcio/fulcioverifier/ctl" - "github.com/sigstore/cosign/pkg/apis/cosigned/v1alpha1" cbundle "github.com/sigstore/cosign/pkg/cosign/bundle" "github.com/sigstore/cosign/pkg/cosign/tuf" @@ -58,6 +57,13 @@ import ( sigPayload "github.com/sigstore/sigstore/pkg/signature/payload" ) +// Identity specifies an issuer/subject to verify a signature against. +// Both Issuer/Subject support regexp. +type Identity struct { + Issuer string + Subject string +} + // CheckOpts are the options for checking signatures. type CheckOpts struct { // RegistryClientOpts are the options for interacting with the container registry. @@ -94,7 +100,7 @@ type CheckOpts struct { // Identities is an array of Identity (Subject, Issuer) matchers that have // to be met for the signature to ve valid. // Supercedes CertEmail / CertOidcIssuer - Identities []v1alpha1.Identity + Identities []Identity } func getSignedEntity(signedImgRef name.Reference, regClientOpts []ociremote.Option) (oci.SignedEntity, v1.Hash, error) { @@ -189,7 +195,6 @@ func ValidateAndUnpackCert(cert *x509.Certificate, co *CheckOpts) (signature.Ver for _, identity := range co.Identities { issuerMatches := false // Check the issuer first - fmt.Fprintf(os.Stderr, "Checking identity: %+v", identity) if identity.Issuer != "" { issuer := getIssuer(cert) if regex, err := regexp.Compile(identity.Issuer); err != nil { diff --git a/pkg/cosign/verify_test.go b/pkg/cosign/verify_test.go index 3f735c6d3ec..6208cd02032 100644 --- a/pkg/cosign/verify_test.go +++ b/pkg/cosign/verify_test.go @@ -40,7 +40,6 @@ import ( "github.com/in-toto/in-toto-golang/in_toto" "github.com/pkg/errors" "github.com/secure-systems-lab/go-securesystemslib/dsse" - "github.com/sigstore/cosign/pkg/apis/cosigned/v1alpha1" "github.com/sigstore/cosign/pkg/cosign/bundle" ctuf "github.com/sigstore/cosign/pkg/cosign/tuf" "github.com/sigstore/cosign/pkg/oci/static" @@ -558,7 +557,7 @@ func TestValidateAndUnpackCertWithIdentities(t *testing.T) { oidcIssuer := "https://accounts.google.com" tests := []struct { - identities []v1alpha1.Identity + identities []Identity wantErrSubstring string dnsNames []string emailAddresses []string @@ -566,48 +565,48 @@ func TestValidateAndUnpackCertWithIdentities(t *testing.T) { uris []*url.URL }{ {identities: nil /* No matches required, checks out */}, - {identities: []v1alpha1.Identity{ // Strict match on both + {identities: []Identity{ // Strict match on both {Subject: emailSubject, Issuer: oidcIssuer}}, emailAddresses: []string{emailSubject}, wantErrSubstring: ""}, - {identities: []v1alpha1.Identity{ // just issuer + {identities: []Identity{ // just issuer {Issuer: oidcIssuer}}, emailAddresses: []string{emailSubject}, wantErrSubstring: ""}, - {identities: []v1alpha1.Identity{ // just subject + {identities: []Identity{ // just subject {Subject: emailSubject}}, emailAddresses: []string{emailSubject}, wantErrSubstring: ""}, - {identities: []v1alpha1.Identity{ // mis-match + {identities: []Identity{ // mis-match {Subject: "wrongsubject", Issuer: oidcIssuer}, {Subject: emailSubject, Issuer: "wrongissuer"}}, emailAddresses: []string{emailSubject}, wantErrSubstring: "none of the expected identities matched"}, - {identities: []v1alpha1.Identity{ // one good identity, other does not match + {identities: []Identity{ // one good identity, other does not match {Subject: "wrongsubject", Issuer: "wrongissuer"}, {Subject: emailSubject, Issuer: oidcIssuer}}, emailAddresses: []string{emailSubject}, wantErrSubstring: ""}, - {identities: []v1alpha1.Identity{ // illegal regex for subject + {identities: []Identity{ // illegal regex for subject {Subject: "****", Issuer: oidcIssuer}}, emailAddresses: []string{emailSubject}, wantErrSubstring: "malformed subject in identity"}, - {identities: []v1alpha1.Identity{ // illegal regex for issuer + {identities: []Identity{ // illegal regex for issuer {Subject: emailSubject, Issuer: "****"}}, wantErrSubstring: "malformed issuer in identity"}, - {identities: []v1alpha1.Identity{ // regex matches + {identities: []Identity{ // regex matches {Subject: ".*example.com", Issuer: ".*accounts.google.*"}}, emailAddresses: []string{emailSubject}, wantErrSubstring: ""}, - {identities: []v1alpha1.Identity{ // regex matches dnsNames + {identities: []Identity{ // regex matches dnsNames {Subject: ".*ubject.example.com", Issuer: ".*accounts.google.*"}}, dnsNames: dnsSubjects, wantErrSubstring: ""}, - {identities: []v1alpha1.Identity{ // regex matches ip + {identities: []Identity{ // regex matches ip {Subject: "1.2.3.*", Issuer: ".*accounts.google.*"}}, ipAddresses: ipSubjects, wantErrSubstring: ""}, - {identities: []v1alpha1.Identity{ // regex matches urls + {identities: []Identity{ // regex matches urls {Subject: ".*url.examp.*", Issuer: ".*accounts.google.*"}}, uris: uriSubjects, wantErrSubstring: ""}, From 8368baddf688c6eac4a47adee1ed42a4fcd0f83d Mon Sep 17 00:00:00 2001 From: Billy Lynch Date: Fri, 22 Apr 2022 17:12:08 -0400 Subject: [PATCH 36/53] Refactor fulcio signer to take in KeyOpts. (#1788) This commit should not have change in behavior. It changes the fulcio signer to take in the KeyOpts struct instead of passing individual parameters through, as well as moves common code dependent on the values to the same place. The motivation behind this change is to make it easier to add new options without needing to plumb through another param to the already long list. To avoid circular dependencies (sign -> fulcio -> sign), KeyOpts was moved to the options package. Signed-off-by: Billy Lynch --- cmd/cosign/cli/attest.go | 3 +- cmd/cosign/cli/attest/attest.go | 2 +- cmd/cosign/cli/fulcio/fulcio.go | 23 +++++++++-- .../fulcio/fulcioverifier/fulcioverifier.go | 6 +-- cmd/cosign/cli/options/key.go | 37 ++++++++++++++++++ cmd/cosign/cli/policy_init.go | 2 +- cmd/cosign/cli/sign.go | 2 +- cmd/cosign/cli/sign/sign.go | 32 +++++----------- cmd/cosign/cli/sign/sign_blob.go | 21 +--------- cmd/cosign/cli/sign/sign_test.go | 2 +- cmd/cosign/cli/signblob.go | 2 +- cmd/cosign/cli/verify.go | 3 +- cmd/cosign/cli/verify/verify_blob.go | 7 ++-- test/e2e_test.go | 38 +++++++++---------- 14 files changed, 99 insertions(+), 81 deletions(-) create mode 100644 cmd/cosign/cli/options/key.go diff --git a/cmd/cosign/cli/attest.go b/cmd/cosign/cli/attest.go index aabb1c54b4c..ffc30866681 100644 --- a/cmd/cosign/cli/attest.go +++ b/cmd/cosign/cli/attest.go @@ -22,7 +22,6 @@ import ( "github.com/sigstore/cosign/cmd/cosign/cli/attest" "github.com/sigstore/cosign/cmd/cosign/cli/generate" "github.com/sigstore/cosign/cmd/cosign/cli/options" - "github.com/sigstore/cosign/cmd/cosign/cli/sign" ) func Attest() *cobra.Command { @@ -63,7 +62,7 @@ func Attest() *cobra.Command { if err != nil { return err } - ko := sign.KeyOpts{ + ko := options.KeyOpts{ KeyRef: o.Key, PassFunc: generate.GetPass, Sk: o.SecurityKey.Use, diff --git a/cmd/cosign/cli/attest/attest.go b/cmd/cosign/cli/attest/attest.go index bef095b5ba1..473064b376f 100644 --- a/cmd/cosign/cli/attest/attest.go +++ b/cmd/cosign/cli/attest/attest.go @@ -74,7 +74,7 @@ func uploadToTlog(ctx context.Context, sv *sign.SignerVerifier, rekorURL string, } //nolint -func AttestCmd(ctx context.Context, ko sign.KeyOpts, regOpts options.RegistryOptions, imageRef string, certPath string, certChainPath string, +func AttestCmd(ctx context.Context, ko options.KeyOpts, regOpts options.RegistryOptions, imageRef string, certPath string, certChainPath string, noUpload bool, predicatePath string, force bool, predicateType string, replace bool, timeout time.Duration) error { // A key file or token is required unless we're in experimental mode! if options.EnableExperimental() { diff --git a/cmd/cosign/cli/fulcio/fulcio.go b/cmd/cosign/cli/fulcio/fulcio.go index d7eedabafb8..eed76b39496 100644 --- a/cmd/cosign/cli/fulcio/fulcio.go +++ b/cmd/cosign/cli/fulcio/fulcio.go @@ -30,8 +30,9 @@ import ( "golang.org/x/term" "github.com/sigstore/cosign/cmd/cosign/cli/fulcio/fulcioroots" - clioptions "github.com/sigstore/cosign/cmd/cosign/cli/options" + "github.com/sigstore/cosign/cmd/cosign/cli/options" "github.com/sigstore/cosign/pkg/cosign" + "github.com/sigstore/cosign/pkg/providers" "github.com/sigstore/fulcio/pkg/api" "github.com/sigstore/sigstore/pkg/oauthflow" "github.com/sigstore/sigstore/pkg/signature" @@ -110,7 +111,21 @@ type Signer struct { *signature.ECDSASignerVerifier } -func NewSigner(ctx context.Context, idToken, oidcIssuer, oidcClientID, oidcClientSecret, oidcRedirectURL string, fClient api.Client) (*Signer, error) { +func NewSigner(ctx context.Context, ko options.KeyOpts) (*Signer, error) { + fClient, err := NewClient(ko.FulcioURL) + if err != nil { + return nil, errors.Wrap(err, "creating Fulcio client") + } + + idToken := ko.IDToken + // If token is not set in the options, get one from the provders + if idToken == "" && providers.Enabled(ctx) { + idToken, err = providers.Provide(ctx, "sigstore") + if err != nil { + return nil, errors.Wrap(err, "fetching ambient OIDC credentials") + } + } + priv, err := cosign.GeneratePrivateKey() if err != nil { return nil, errors.Wrap(err, "generating cert") @@ -131,7 +146,7 @@ func NewSigner(ctx context.Context, idToken, oidcIssuer, oidcClientID, oidcClien default: flow = FlowNormal } - Resp, err := GetCert(ctx, priv, idToken, flow, oidcIssuer, oidcClientID, oidcClientSecret, oidcRedirectURL, fClient) // TODO, use the chain. + Resp, err := GetCert(ctx, priv, idToken, flow, ko.OIDCIssuer, ko.OIDCClientID, ko.OIDCClientSecret, ko.OIDCRedirectURL, fClient) // TODO, use the chain. if err != nil { return nil, errors.Wrap(err, "retrieving cert") } @@ -166,6 +181,6 @@ func NewClient(fulcioURL string) (api.Client, error) { if err != nil { return nil, err } - fClient := api.NewClient(fulcioServer, api.WithUserAgent(clioptions.UserAgent())) + fClient := api.NewClient(fulcioServer, api.WithUserAgent(options.UserAgent())) return fClient, nil } diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go b/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go index 3687f5db01f..c2905ec12a9 100644 --- a/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go +++ b/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go @@ -24,11 +24,11 @@ import ( "github.com/sigstore/cosign/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/cmd/cosign/cli/fulcio/fulcioverifier/ctl" - "github.com/sigstore/fulcio/pkg/api" + "github.com/sigstore/cosign/cmd/cosign/cli/options" ) -func NewSigner(ctx context.Context, idToken, oidcIssuer, oidcClientID, oidcClientSecret, oidcRedirectURL string, fClient api.Client) (*fulcio.Signer, error) { - fs, err := fulcio.NewSigner(ctx, idToken, oidcIssuer, oidcClientID, oidcClientSecret, oidcRedirectURL, fClient) +func NewSigner(ctx context.Context, ko options.KeyOpts) (*fulcio.Signer, error) { + fs, err := fulcio.NewSigner(ctx, ko) if err != nil { return nil, err } diff --git a/cmd/cosign/cli/options/key.go b/cmd/cosign/cli/options/key.go new file mode 100644 index 00000000000..db36e9235b7 --- /dev/null +++ b/cmd/cosign/cli/options/key.go @@ -0,0 +1,37 @@ +// +// 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 options + +import "github.com/sigstore/cosign/pkg/cosign" + +type KeyOpts struct { + Sk bool + Slot string + KeyRef string + FulcioURL string + RekorURL string + IDToken string + PassFunc cosign.PassFunc + OIDCIssuer string + OIDCClientID string + OIDCClientSecret string + OIDCRedirectURL string + BundlePath string + + // Modeled after InsecureSkipVerify in tls.Config, this disables + // verifying the SCT. + InsecureSkipFulcioVerify bool +} diff --git a/cmd/cosign/cli/policy_init.go b/cmd/cosign/cli/policy_init.go index 9e9c0f0bae0..89e100c36d0 100644 --- a/cmd/cosign/cli/policy_init.go +++ b/cmd/cosign/cli/policy_init.go @@ -179,7 +179,7 @@ func signPolicy() *cobra.Command { if err != nil { return err } - sv, err := sign.SignerFromKeyOpts(ctx, "", "", sign.KeyOpts{ + sv, err := sign.SignerFromKeyOpts(ctx, "", "", options.KeyOpts{ FulcioURL: o.Fulcio.URL, IDToken: o.Fulcio.IdentityToken, InsecureSkipFulcioVerify: o.Fulcio.InsecureSkipFulcioVerify, diff --git a/cmd/cosign/cli/sign.go b/cmd/cosign/cli/sign.go index 6e3076f566d..3acdc1b2b61 100644 --- a/cmd/cosign/cli/sign.go +++ b/cmd/cosign/cli/sign.go @@ -79,7 +79,7 @@ func Sign() *cobra.Command { if err != nil { return err } - ko := sign.KeyOpts{ + ko := options.KeyOpts{ KeyRef: o.Key, PassFunc: generate.GetPass, Sk: o.SecurityKey.Use, diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go index 44da104baf8..adff00bad11 100644 --- a/cmd/cosign/cli/sign/sign.go +++ b/cmd/cosign/cli/sign/sign.go @@ -47,7 +47,6 @@ import ( "github.com/sigstore/cosign/pkg/oci/mutate" ociremote "github.com/sigstore/cosign/pkg/oci/remote" "github.com/sigstore/cosign/pkg/oci/walk" - providers "github.com/sigstore/cosign/pkg/providers/all" sigs "github.com/sigstore/cosign/pkg/signature" "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" @@ -93,7 +92,7 @@ func GetAttachedImageRef(ref name.Reference, attachment string, opts ...ociremot } // nolint -func SignCmd(ro *options.RootOptions, ko KeyOpts, regOpts options.RegistryOptions, annotations map[string]interface{}, +func SignCmd(ro *options.RootOptions, ko options.KeyOpts, regOpts options.RegistryOptions, annotations map[string]interface{}, imgs []string, certPath string, certChainPath string, upload bool, outputSignature, outputCertificate string, payloadPath string, force bool, recursive bool, attachment string) error { if options.EnableExperimental() { @@ -183,7 +182,7 @@ func SignCmd(ro *options.RootOptions, ko KeyOpts, regOpts options.RegistryOption return nil } -func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko KeyOpts, +func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko options.KeyOpts, regOpts options.RegistryOptions, annotations map[string]interface{}, upload bool, outputSignature, outputCertificate string, force bool, recursive bool, dd mutate.DupeDetector, sv *SignerVerifier, se oci.SignedEntity) error { var err error @@ -436,29 +435,18 @@ func signerFromKeyRef(ctx context.Context, certPath, certChainPath, keyRef strin return certSigner, nil } -func keylessSigner(ctx context.Context, ko KeyOpts) (*SignerVerifier, error) { - fClient, err := fulcio.NewClient(ko.FulcioURL) - if err != nil { - return nil, errors.Wrap(err, "creating Fulcio client") - } - - tok := ko.IDToken - // If token is not set in the options, get one from the provders - if tok == "" && providers.Enabled(ctx) { - tok, err = providers.Provide(ctx, "sigstore") - if err != nil { - return nil, errors.Wrap(err, "fetching ambient OIDC credentials") - } - } - - var k *fulcio.Signer +func keylessSigner(ctx context.Context, ko options.KeyOpts) (*SignerVerifier, error) { + var ( + k *fulcio.Signer + err error + ) if ko.InsecureSkipFulcioVerify { - if k, err = fulcio.NewSigner(ctx, tok, ko.OIDCIssuer, ko.OIDCClientID, ko.OIDCClientSecret, ko.OIDCRedirectURL, fClient); err != nil { + if k, err = fulcio.NewSigner(ctx, ko); err != nil { return nil, errors.Wrap(err, "getting key from Fulcio") } } else { - if k, err = fulcioverifier.NewSigner(ctx, tok, ko.OIDCIssuer, ko.OIDCClientID, ko.OIDCClientSecret, ko.OIDCRedirectURL, fClient); err != nil { + if k, err = fulcioverifier.NewSigner(ctx, ko); err != nil { return nil, errors.Wrap(err, "getting key from Fulcio") } } @@ -470,7 +458,7 @@ func keylessSigner(ctx context.Context, ko KeyOpts) (*SignerVerifier, error) { }, nil } -func SignerFromKeyOpts(ctx context.Context, certPath string, certChainPath string, ko KeyOpts) (*SignerVerifier, error) { +func SignerFromKeyOpts(ctx context.Context, certPath string, certChainPath string, ko options.KeyOpts) (*SignerVerifier, error) { if ko.Sk { return signerFromSecurityKey(ko.Slot) } diff --git a/cmd/cosign/cli/sign/sign_blob.go b/cmd/cosign/cli/sign/sign_blob.go index d21799ff9f3..401922b21d1 100644 --- a/cmd/cosign/cli/sign/sign_blob.go +++ b/cmd/cosign/cli/sign/sign_blob.go @@ -34,27 +34,8 @@ import ( signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" ) -type KeyOpts struct { - Sk bool - Slot string - KeyRef string - FulcioURL string - RekorURL string - IDToken string - PassFunc cosign.PassFunc - OIDCIssuer string - OIDCClientID string - OIDCClientSecret string - OIDCRedirectURL string - BundlePath string - - // Modeled after InsecureSkipVerify in tls.Config, this disables - // verifying the SCT. - InsecureSkipFulcioVerify bool -} - // nolint -func SignBlobCmd(ro *options.RootOptions, ko KeyOpts, regOpts options.RegistryOptions, payloadPath string, b64 bool, outputSignature string, outputCertificate string) ([]byte, error) { +func SignBlobCmd(ro *options.RootOptions, ko options.KeyOpts, regOpts options.RegistryOptions, payloadPath string, b64 bool, outputSignature string, outputCertificate string) ([]byte, error) { var payload []byte var err error var rekorBytes []byte diff --git a/cmd/cosign/cli/sign/sign_test.go b/cmd/cosign/cli/sign/sign_test.go index bb7255fd361..bc0eabcf8a8 100644 --- a/cmd/cosign/cli/sign/sign_test.go +++ b/cmd/cosign/cli/sign/sign_test.go @@ -110,7 +110,7 @@ func generateCertificateFiles(t *testing.T, tmpDir string, pf cosign.PassFunc) ( func TestSignCmdLocalKeyAndSk(t *testing.T) { ro := &options.RootOptions{Timeout: options.DefaultTimeout} - for _, ko := range []KeyOpts{ + for _, ko := range []options.KeyOpts{ // local and sk keys { KeyRef: "testLocalPath", diff --git a/cmd/cosign/cli/signblob.go b/cmd/cosign/cli/signblob.go index fcb894c8369..e5e14b2cf08 100644 --- a/cmd/cosign/cli/signblob.go +++ b/cmd/cosign/cli/signblob.go @@ -68,7 +68,7 @@ func SignBlob() *cobra.Command { if err != nil { return err } - ko := sign.KeyOpts{ + ko := options.KeyOpts{ KeyRef: o.Key, PassFunc: generate.GetPass, Sk: o.SecurityKey.Use, diff --git a/cmd/cosign/cli/verify.go b/cmd/cosign/cli/verify.go index 5471da9a1b0..e48b4175e6c 100644 --- a/cmd/cosign/cli/verify.go +++ b/cmd/cosign/cli/verify.go @@ -20,7 +20,6 @@ import ( "github.com/spf13/cobra" "github.com/sigstore/cosign/cmd/cosign/cli/options" - "github.com/sigstore/cosign/cmd/cosign/cli/sign" "github.com/sigstore/cosign/cmd/cosign/cli/verify" ) @@ -246,7 +245,7 @@ The blob may be specified as a path to a file or - for stdin.`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - ko := sign.KeyOpts{ + ko := options.KeyOpts{ KeyRef: o.Key, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index 0341ed0a025..77bef0e92e1 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -35,7 +35,6 @@ import ( "github.com/sigstore/cosign/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/cmd/cosign/cli/options" "github.com/sigstore/cosign/cmd/cosign/cli/rekor" - "github.com/sigstore/cosign/cmd/cosign/cli/sign" "github.com/sigstore/cosign/pkg/blob" "github.com/sigstore/cosign/pkg/cosign" "github.com/sigstore/cosign/pkg/cosign/pivkey" @@ -61,7 +60,7 @@ func isb64(data []byte) bool { } // nolint -func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, +func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail, certOidcIssuer, certChain, sigRef, blobRef string, enforceSCT bool) error { var verifier signature.Verifier var cert *x509.Certificate @@ -186,7 +185,7 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, return nil } -func verifySigByUUID(ctx context.Context, ko sign.KeyOpts, rClient *client.Rekor, certEmail, certOidcIssuer, sig, b64sig string, +func verifySigByUUID(ctx context.Context, ko options.KeyOpts, rClient *client.Rekor, certEmail, certOidcIssuer, sig, b64sig string, uuids []string, blobBytes []byte, enforceSCT bool) error { var validSigExists bool for _, u := range uuids { @@ -289,7 +288,7 @@ func payloadBytes(blobRef string) ([]byte, error) { return blobBytes, nil } -func verifyRekorEntry(ctx context.Context, ko sign.KeyOpts, e *models.LogEntryAnon, pubKey signature.Verifier, cert *x509.Certificate, b64sig string, blobBytes []byte) error { +func verifyRekorEntry(ctx context.Context, ko options.KeyOpts, e *models.LogEntryAnon, pubKey signature.Verifier, cert *x509.Certificate, b64sig string, blobBytes []byte) error { // If we have a bundle with a rekor entry, let's first try to verify offline if ko.BundlePath != "" { if err := verifyRekorBundle(ctx, ko.BundlePath, cert); err == nil { diff --git a/test/e2e_test.go b/test/e2e_test.go index 1302b9d8478..75c7701f864 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -125,7 +125,7 @@ func TestSignVerify(t *testing.T) { mustErr(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName), t) // Now sign the image - ko := sign.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} must(sign.SignCmd(ro, ko, options.RegistryOptions{}, nil, []string{imgName}, "", "", true, "", "", "", false, false, ""), t) // Now verify and download should work! @@ -160,7 +160,7 @@ func TestSignVerifyClean(t *testing.T) { ctx := context.Background() // Now sign the image - ko := sign.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} must(sign.SignCmd(ro, ko, options.RegistryOptions{}, nil, []string{imgName}, "", "", true, "", "", "", false, false, ""), t) // Now verify and download should work! @@ -189,7 +189,7 @@ func TestImportSignVerifyClean(t *testing.T) { ctx := context.Background() // Now sign the image - ko := sign.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} must(sign.SignCmd(ro, ko, options.RegistryOptions{}, nil, []string{imgName}, "", "", true, "", "", "", false, false, ""), t) // Now verify and download should work! @@ -232,7 +232,7 @@ func TestAttestVerify(t *testing.T) { } // Now attest the image - ko := sign.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} must(attest.AttestCmd(ctx, ko, options.RegistryOptions{}, imgName, "", "", false, slsaAttestationPath, false, "slsaprovenance", false, 30*time.Second), t) @@ -273,7 +273,7 @@ func TestAttestationReplace(t *testing.T) { defer cleanup() _, privKeyPath, _ := keypair(t, td) - ko := sign.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} ctx := context.Background() @@ -327,7 +327,7 @@ func TestRekorBundle(t *testing.T) { _, privKeyPath, pubKeyPath := keypair(t, td) - ko := sign.KeyOpts{ + ko := options.KeyOpts{ KeyRef: privKeyPath, PassFunc: passFunc, RekorURL: rekorURL, @@ -363,7 +363,7 @@ func TestDuplicateSign(t *testing.T) { mustErr(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName), t) // Now sign the image - ko := sign.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} must(sign.SignCmd(ro, ko, options.RegistryOptions{}, nil, []string{imgName}, "", "", true, "", "", "", false, false, ""), t) // Now verify and download should work! @@ -460,7 +460,7 @@ func TestMultipleSignatures(t *testing.T) { mustErr(verify(pub2, imgName, true, nil, ""), t) // Now sign the image with one key - ko := sign.KeyOpts{KeyRef: priv1, PassFunc: passFunc} + ko := options.KeyOpts{KeyRef: priv1, PassFunc: passFunc} must(sign.SignCmd(ro, ko, options.RegistryOptions{}, nil, []string{imgName}, "", "", true, "", "", "", false, false, ""), t) // Now verify should work with that one, but not the other must(verify(pub1, imgName, true, nil, ""), t) @@ -494,10 +494,10 @@ func TestSignBlob(t *testing.T) { ctx := context.Background() - ko1 := sign.KeyOpts{ + ko1 := options.KeyOpts{ KeyRef: pubKeyPath1, } - ko2 := sign.KeyOpts{ + ko2 := options.KeyOpts{ KeyRef: pubKeyPath2, } // Verify should fail on a bad input @@ -505,7 +505,7 @@ func TestSignBlob(t *testing.T) { mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, "badsig", blob, false), t) // Now sign the blob with one key - ko := sign.KeyOpts{ + ko := options.KeyOpts{ KeyRef: privKeyPath1, PassFunc: passFunc, } @@ -535,7 +535,7 @@ func TestSignBlobBundle(t *testing.T) { ctx := context.Background() - ko1 := sign.KeyOpts{ + ko1 := options.KeyOpts{ KeyRef: pubKeyPath1, BundlePath: bundlePath, } @@ -543,7 +543,7 @@ func TestSignBlobBundle(t *testing.T) { mustErr(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", blob, false), t) // Now sign the blob with one key - ko := sign.KeyOpts{ + ko := options.KeyOpts{ KeyRef: privKeyPath1, PassFunc: passFunc, BundlePath: bundlePath, @@ -849,7 +849,7 @@ func TestSaveLoad(t *testing.T) { ctx := context.Background() // Now sign the image and verify it - ko := sign.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} must(sign.SignCmd(ro, ko, options.RegistryOptions{}, nil, []string{imgName}, "", "", true, "", "", "", false, false, ""), t) must(verify(pubKeyPath, imgName, true, nil, ""), t) @@ -882,7 +882,7 @@ func TestSaveLoadAttestation(t *testing.T) { ctx := context.Background() // Now sign the image and verify it - ko := sign.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} must(sign.SignCmd(ro, ko, options.RegistryOptions{}, nil, []string{imgName}, "", "", true, "", "", "", false, false, ""), t) must(verify(pubKeyPath, imgName, true, nil, ""), t) @@ -894,7 +894,7 @@ func TestSaveLoadAttestation(t *testing.T) { } // Now attest the image - ko = sign.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + ko = options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} must(attest.AttestCmd(ctx, ko, options.RegistryOptions{}, imgName, "", "", false, slsaAttestationPath, false, "custom", false, 30*time.Second), t) @@ -971,7 +971,7 @@ func TestAttachSBOM(t *testing.T) { mustErr(verify(pubKeyPath2, imgName, true, nil, "sbom"), t) // Now sign the sbom with one key - ko1 := sign.KeyOpts{KeyRef: privKeyPath1, PassFunc: passFunc} + ko1 := options.KeyOpts{KeyRef: privKeyPath1, PassFunc: passFunc} must(sign.SignCmd(ro, ko1, options.RegistryOptions{}, nil, []string{imgName}, "", "", true, "", "", "", false, false, "sbom"), t) // Now verify should work with that one, but not the other @@ -1004,7 +1004,7 @@ func TestTlog(t *testing.T) { mustErr(verify(pubKeyPath, imgName, true, nil, ""), t) // Now sign the image without the tlog - ko := sign.KeyOpts{ + ko := options.KeyOpts{ KeyRef: privKeyPath, PassFunc: passFunc, RekorURL: rekorURL, @@ -1182,7 +1182,7 @@ func TestInvalidBundle(t *testing.T) { // (we're just using it for its bundle) defer setenv(t, options.ExperimentalEnv, "1")() remoteOpts := ociremote.WithRemoteOptions(registryClientOpts(ctx)...) - ko := sign.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc, RekorURL: rekorURL} + ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc, RekorURL: rekorURL} regOpts := options.RegistryOptions{} must(sign.SignCmd(ro, ko, regOpts, nil, []string{img1}, "", "", true, "", "", "", true, false, ""), t) From c96ebb47f2473f17817b20cb9bfd35a883423395 Mon Sep 17 00:00:00 2001 From: Hector Fernandez Date: Sat, 23 Apr 2022 07:13:35 +0300 Subject: [PATCH 37/53] test: add cue unit tests (#1791) Signed-off-by: hectorj2f --- pkg/cosign/cue/cue_test.go | 182 +++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 pkg/cosign/cue/cue_test.go diff --git a/pkg/cosign/cue/cue_test.go b/pkg/cosign/cue/cue_test.go new file mode 100644 index 00000000000..3feeca92d9c --- /dev/null +++ b/pkg/cosign/cue/cue_test.go @@ -0,0 +1,182 @@ +// +// 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 cue + +import ( + "fmt" + "os" + + "testing" +) + +var cueJSONAttestationsBody = ` +{ + "authorityMatches": { + "keyatt": { + "signatures": null, + "attestations": { + "vuln-key": [ + { + "subject": "PLACEHOLDER", + "issuer": "PLACEHOLDER" + } + ] + } + }, + "keysignature": { + "signatures": [ + { + "subject": "PLACEHOLDER", + "issuer": "PLACEHOLDER" + } + ], + "attestations": null + }, + "keylessatt": { + "signatures": null, + "attestations": { + "key1": [ + { + "subject": "PLACEHOLDER", + "issuer": "PLACEHOLDER" + } + ], + "key2": [ + { + "subject": "PLACEHOLDER", + "issuer": "PLACEHOLDER" + } + ] + } + }, + "keylesssignature": { + "signatures": [ + { + "subject": "PLACEHOLDER", + "issuer": "PLACEHOLDER" + } + ], + "attestations": null + } + } + } +` + +var cueJSONSampleBody = `{ + "seq": [ + 1, 2, 3, { + "a": 1, + "b": 2 + } + ], + "a": {"b": {"c": 3}}, + "b": { + "x": 0, + "y": 1, + "z": 2 + } +}` + +func TestValidationJSON(t *testing.T) { + cases := []struct { + name string + jsonBody string + policy string + pass bool + errorMsg string + }{ + { + name: "passing policy", + jsonBody: cueJSONSampleBody, + policy: ` + package test + + seq: [ + 1, 2, 3, { + a: 1 + b: 2 + } + ] + a: b: c: 3 + b: { + x: 0 + y: 1 + z: 2 + } + `, + pass: true, + }, + { + name: "passing result due to matching rules", + jsonBody: cueJSONAttestationsBody, + policy: ` + package test + import "struct" + import "list" + + authorityMatches: { + keyatt: { + attestations: struct.MaxFields(1) & struct.MinFields(1) + }, + keysignature: { + signatures: list.MaxItems(1) & list.MinItems(1) + }, + keylessatt: { + attestations: struct.MinFields(2) & struct.MaxFields(2) + }, + keylesssignature: { + signatures: list.MaxItems(1) & list.MinItems(1) + } + } + `, + pass: true, + }, + { + name: "policy query evaluates to false signatures array min items", + jsonBody: cueJSONAttestationsBody, + policy: ` + package test + import "list" + + authorityMatches: { + keysignature: { + signatures: list.MaxItems(2) & list.MinItems(2) + } + } + `, + pass: false, + errorMsg: "authorityMatches.keysignature.signatures: invalid value [{subject:\"PLACEHOLDER\",issuer:\"PLACEHOLDER\"}] (does not satisfy list.MinItems(2))", + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + policyFileName := "tmp-policy.cue" + if err := os.WriteFile(policyFileName, []byte(tt.policy), 0644); err != nil { + t.Fatal(err) + } + defer os.Remove(policyFileName) + + if err := ValidateJSON([]byte(tt.jsonBody), []string{policyFileName}); (err == nil) != tt.pass { + t.Fatalf("Unexpected result: %v", err) + } else if err != nil { + if fmt.Sprintf("%s", err) != tt.errorMsg { + t.Errorf("Expected error %q, got %q", tt.errorMsg, err) + } + } + }) + } +} From e1469ba182426c9cd5c4e119bb1c949818ce385f Mon Sep 17 00:00:00 2001 From: Ville Aikas <11279988+vaikas@users.noreply.github.com> Date: Sat, 23 Apr 2022 06:47:40 -0700 Subject: [PATCH 38/53] Attestations + policy in cip. (#1772) * Modify types, introduce name defaults, codegen. Signed-off-by: Ville Aikas * Start refactoring and adding verify-attestation pieces. Signed-off-by: Ville Aikas * Sort of works e2e. Signed-off-by: Ville Aikas * E2E tests. Plumb rekor through for key-ful signing too. Signed-off-by: Ville Aikas * bad rebase. Signed-off-by: Ville Aikas * more bad rebases. Signed-off-by: Ville Aikas * regen keys. Signed-off-by: Ville Aikas * at the right place. Signed-off-by: Ville Aikas * forgot one rekor-url. Signed-off-by: Ville Aikas * Lint. Signed-off-by: Ville Aikas * remove these manually. Signed-off-by: Ville Aikas * Cleanups, do not unnecessarily eval empty json bytes for attestation policy. Signed-off-by: Ville Aikas * Add name to attestations for easier referencing from cip policy. Signed-off-by: Ville Aikas * Do not return empty PolicyResult when errors. Start adding UT for ValidatePolicy. Getting ready for rebase. Signed-off-by: Ville Aikas * fix: cue policy missing double-quotes Signed-off-by: hectorj2f * Add start of unit tests for policy validation stuff. Add start of UT for ValidatePolicy. Signed-off-by: Ville Aikas * Refactor policy eval code. Remove the attestation CIP from normal CIP tests, will follow up with those. Add validation for attestations in CIPs Signed-off-by: Ville Aikas * loving yaml, thanks validation! Signed-off-by: Ville Aikas * Starting to break apart the attestation tests. Signed-off-by: Ville Aikas * checkpoint Signed-off-by: Ville Aikas * Remove unused keychain from the Validate* calls and use one from the remoteopts instead consistently. Address PR feedback. Signed-off-by: Ville Aikas * lint. Signed-off-by: Ville Aikas Co-authored-by: hectorj2f --- ...luster-image-policy-with-attestations.yaml | 97 ++++++ config/300-clusterimagepolicy.yaml | 53 ++++ pkg/apis/config/image_policies.go | 12 +- pkg/apis/config/image_policies_test.go | 61 +++- .../testdata/config-image-policies.yaml | 41 ++- .../v1alpha1/clusterimagepolicy_defaults.go | 16 +- .../clusterimagepolicy_defaults_test.go | 63 ++++ .../v1alpha1/clusterimagepolicy_types.go | 53 +++- .../v1alpha1/clusterimagepolicy_validation.go | 48 ++- .../clusterimagepolicy_validation_test.go | 106 +++++++ .../v1alpha1/zz_generated.deepcopy.go | 75 +++++ .../clusterimagepolicy_types.go | 58 +++- pkg/cosign/kubernetes/webhook/validation.go | 36 ++- pkg/cosign/kubernetes/webhook/validator.go | 291 ++++++++++++++---- .../kubernetes/webhook/validator_result.go | 60 ++++ .../kubernetes/webhook/validator_test.go | 258 +++++++++++++++- pkg/policy/eval.go | 69 +++++ pkg/policy/eval_test.go | 143 +++++++++ .../clusterimagepolicy_test.go | 14 +- test/e2e_test_cluster_image_policy.sh | 21 +- ..._cluster_image_policy_with_attestations.sh | 243 +++++++++++++++ .../e2e/cip-key-with-attestations.yaml | 48 +++ test/testdata/cosigned/e2e/cip-key.yaml | 3 +- .../e2e/cip-keyless-with-attestations.yaml | 40 +++ ...s-two-signatures-and-two-attestations.yaml | 142 +++++++++ .../cosigned/valid/valid-policy-regex.yaml | 43 ++- 26 files changed, 1955 insertions(+), 139 deletions(-) create mode 100644 .github/workflows/kind-cluster-image-policy-with-attestations.yaml create mode 100644 pkg/apis/cosigned/v1alpha1/clusterimagepolicy_defaults_test.go create mode 100644 pkg/cosign/kubernetes/webhook/validator_result.go create mode 100644 pkg/policy/eval.go create mode 100644 pkg/policy/eval_test.go create mode 100755 test/e2e_test_cluster_image_policy_with_attestations.sh create mode 100644 test/testdata/cosigned/e2e/cip-key-with-attestations.yaml create mode 100644 test/testdata/cosigned/e2e/cip-keyless-with-attestations.yaml create mode 100644 test/testdata/cosigned/e2e/cip-requires-two-signatures-and-two-attestations.yaml diff --git a/.github/workflows/kind-cluster-image-policy-with-attestations.yaml b/.github/workflows/kind-cluster-image-policy-with-attestations.yaml new file mode 100644 index 00000000000..c52d490d592 --- /dev/null +++ b/.github/workflows/kind-cluster-image-policy-with-attestations.yaml @@ -0,0 +1,97 @@ +# 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. + +name: Test cosigned with ClusterImagePolicy with attestations + +on: + pull_request: + branches: [ 'main', 'release-*' ] + +defaults: + run: + shell: bash + +permissions: read-all + +jobs: + cip-test: + name: ClusterImagePolicy e2e tests + runs-on: ubuntu-latest + + strategy: + matrix: + k8s-version: + - v1.21.x + - v1.22.x + # Try without this one now, might have problems with job restartings + # may require upstream changes. + - v1.23.x + + env: + KNATIVE_VERSION: "1.1.0" + KO_DOCKER_REPO: "registry.local:5000/cosigned" + SCAFFOLDING_RELEASE_VERSION: "v0.2.8" + GO111MODULE: on + GOFLAGS: -ldflags=-s -ldflags=-w + KOCACHE: ~/ko + COSIGN_EXPERIMENTAL: true + + steps: + - uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 # v2.4.0 + - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2.2.0 + with: + go-version: '1.17.x' + + # will use the latest release available for ko + - uses: imjasonh/setup-ko@2c3450ca27f6e6f2b02e72a40f2163c281a1f675 # v0.4 + + - uses: imranismail/setup-kustomize@8fa954828ed3cfa7a487a2ba9f7104899bb48b2f # v1.6.1 + + - name: Install yq + uses: mikefarah/yq@ed5b811f37384d92f62898492ddd81b6dc3af38f # v4.16.2 + + - name: Setup mirror + uses: chainguard-dev/actions/setup-mirror@main + with: + mirror: mirror.gcr.io + + - name: build cosign + run: | + make cosign + + - name: Install cluster + cosign + uses: sigstore/scaffolding/actions/setup@main + + - name: Install cosigned + env: + GIT_HASH: ${{ github.sha }} + GIT_VERSION: ci + LDFLAGS: "" + COSIGNED_YAML: cosigned-e2e.yaml + KO_PREFIX: registry.local:5000/cosigned + COSIGNED_ARCHS: linux/amd64 + run: | + make ko-cosigned + kubectl apply -f cosigned-e2e.yaml + + # Wait for the webhook to come up and become Ready + kubectl rollout status --timeout 5m --namespace cosign-system deployments/webhook + + - name: Run Cluster Image Policy Tests with attestations + run: | + ./test/e2e_test_cluster_image_policy_with_attestations.sh + + - name: Collect diagnostics + if: ${{ failure() }} + uses: chainguard-dev/actions/kind-diag@84c993eaf02da1c325854fb272a4df9184bd80fc # main diff --git a/config/300-clusterimagepolicy.yaml b/config/300-clusterimagepolicy.yaml index 5301b2cb5fa..cd5545806f1 100644 --- a/config/300-clusterimagepolicy.yaml +++ b/config/300-clusterimagepolicy.yaml @@ -44,6 +44,36 @@ spec: items: type: object properties: + attestations: + type: array + items: + type: object + properties: + name: + description: Name of the attestation. These can then be referenced at the CIP level policy. + type: string + policy: + type: object + properties: + configMapRef: + type: object + properties: + name: + description: Name is unique within a namespace to reference a configmap resource. + type: string + namespace: + description: Namespace defines the space within which the configmap name must be unique. + type: string + data: + type: string + type: + description: Which kind of policy this is, currently only rego or cue are supported. Furthermore, only cue is tested :) + type: string + url: + type: string + predicateType: + description: Which predicate type to verify. Matches cosign verify-attestation options. + type: string ctlog: type: object properties: @@ -99,6 +129,9 @@ spec: type: string url: type: string + name: + description: Name is the name for this authority. Used by the CIP Policy validator to be able to reference matching signature or attestation verifications. If not specified, the name will be authority- + type: string source: type: array items: @@ -115,3 +148,23 @@ spec: type: string regex: type: string + policy: + description: Policy is an optional policy that can be applied against all the successfully validated Authorities. If no authorities pass, this does not even get evaluated, as the Policy is considered failed. + type: object + properties: + configMapRef: + type: object + properties: + name: + description: Name is unique within a namespace to reference a configmap resource. + type: string + namespace: + description: Namespace defines the space within which the configmap name must be unique. + type: string + data: + type: string + type: + description: Which kind of policy this is, currently only rego or cue are supported. Furthermore, only cue is tested :) + type: string + url: + type: string diff --git a/pkg/apis/config/image_policies.go b/pkg/apis/config/image_policies.go index 3bce8bc80fc..40a6fa5164c 100644 --- a/pkg/apis/config/image_policies.go +++ b/pkg/apis/config/image_policies.go @@ -77,15 +77,15 @@ func parseEntry(entry string, out interface{}) error { // GetMatchingPolicies returns all matching Policies and their Authorities that // need to be matched for the given Image. -// Returned map contains the name of the CIP as the key, and an array of -// authorities from that Policy that must be validated against. -func (p *ImagePolicyConfig) GetMatchingPolicies(image string) (map[string][]webhookcip.Authority, error) { +// Returned map contains the name of the CIP as the key, and a normalized +// ClusterImagePolicy for it. +func (p *ImagePolicyConfig) GetMatchingPolicies(image string) (map[string]webhookcip.ClusterImagePolicy, error) { if p == nil { return nil, errors.New("config is nil") } var lastError error - ret := map[string][]webhookcip.Authority{} + ret := make(map[string]webhookcip.ClusterImagePolicy) // TODO(vaikas): this is very inefficient, we should have a better // way to go from image to Authorities, but just seeing if this is even @@ -94,13 +94,13 @@ func (p *ImagePolicyConfig) GetMatchingPolicies(image string) (map[string][]webh for _, pattern := range v.Images { if pattern.Glob != "" { if GlobMatch(image, pattern.Glob) { - ret[k] = append(ret[k], v.Authorities...) + ret[k] = v } } else if pattern.Regex != "" { if regex, err := regexp.Compile(pattern.Regex); err != nil { lastError = err } else if regex.MatchString(image) { - ret[k] = append(ret[k], v.Authorities...) + ret[k] = v } } } diff --git a/pkg/apis/config/image_policies_test.go b/pkg/apis/config/image_policies_test.go index ff7146b53b3..0336bb80afe 100644 --- a/pkg/apis/config/image_policies_test.go +++ b/pkg/apis/config/image_policies_test.go @@ -51,7 +51,7 @@ func TestGetAuthorities(t *testing.T) { checkGetMatches(t, c, err) matchedPolicy := "cluster-image-policy-0" want := "inlinedata here" - if got := c[matchedPolicy][0].Key.Data; got != want { + if got := c[matchedPolicy].Authorities[0].Key.Data; got != want { t.Errorf("Did not get what I wanted %q, got %+v", want, got) } // Make sure glob matches 'randomstuff*' @@ -59,22 +59,22 @@ func TestGetAuthorities(t *testing.T) { checkGetMatches(t, c, err) matchedPolicy = "cluster-image-policy-1" want = "otherinline here" - if got := c[matchedPolicy][0].Key.Data; got != want { + if got := c[matchedPolicy].Authorities[0].Key.Data; got != want { t.Errorf("Did not get what I wanted %q, got %+v", want, got) } c, err = defaults.GetMatchingPolicies("rando3") checkGetMatches(t, c, err) matchedPolicy = "cluster-image-policy-2" want = "cacert chilling here" - if got := c[matchedPolicy][0].Keyless.CACert.Data; got != want { + if got := c[matchedPolicy].Authorities[0].Keyless.CACert.Data; got != want { t.Errorf("Did not get what I wanted %q, got %+v", want, got) } want = "issuer" - if got := c[matchedPolicy][0].Keyless.Identities[0].Issuer; got != want { + if got := c[matchedPolicy].Authorities[0].Keyless.Identities[0].Issuer; got != want { t.Errorf("Did not get what I wanted %q, got %+v", want, got) } want = "subject" - if got := c[matchedPolicy][0].Keyless.Identities[0].Subject; got != want { + if got := c[matchedPolicy].Authorities[0].Keyless.Identities[0].Subject; got != want { t.Errorf("Did not get what I wanted %q, got %+v", want, got) } // Make sure regex matches ".*regexstring.*" @@ -82,30 +82,30 @@ func TestGetAuthorities(t *testing.T) { checkGetMatches(t, c, err) matchedPolicy = "cluster-image-policy-4" want = inlineKeyData - if got := c[matchedPolicy][0].Key.Data; got != want { + if got := c[matchedPolicy].Authorities[0].Key.Data; got != want { t.Errorf("Did not get what I wanted %q, got %+v", want, got) } - checkPublicKey(t, c[matchedPolicy][0].Key.PublicKeys[0]) + checkPublicKey(t, c[matchedPolicy].Authorities[0].Key.PublicKeys[0]) // Test multiline yaml cert c, err = defaults.GetMatchingPolicies("inlinecert") checkGetMatches(t, c, err) matchedPolicy = "cluster-image-policy-3" want = inlineKeyData - if got := c[matchedPolicy][0].Key.Data; got != want { + if got := c[matchedPolicy].Authorities[0].Key.Data; got != want { t.Errorf("Did not get what I wanted %q, got %+v", want, got) } - checkPublicKey(t, c[matchedPolicy][0].Key.PublicKeys[0]) + checkPublicKey(t, c[matchedPolicy].Authorities[0].Key.PublicKeys[0]) // Test multiline cert but json encoded c, err = defaults.GetMatchingPolicies("ghcr.io/example/*") checkGetMatches(t, c, err) matchedPolicy = "cluster-image-policy-json" want = inlineKeyData - if got := c[matchedPolicy][0].Key.Data; got != want { + if got := c[matchedPolicy].Authorities[0].Key.Data; got != want { t.Errorf("Did not get what I wanted %q, got %+v", want, got) } - checkPublicKey(t, c[matchedPolicy][0].Key.PublicKeys[0]) + checkPublicKey(t, c[matchedPolicy].Authorities[0].Key.PublicKeys[0]) // Test multiple matches c, err = defaults.GetMatchingPolicies("regexstringtoo") @@ -115,19 +115,48 @@ func TestGetAuthorities(t *testing.T) { } matchedPolicy = "cluster-image-policy-4" want = inlineKeyData - if got := c[matchedPolicy][0].Key.Data; got != want { + if got := c[matchedPolicy].Authorities[0].Key.Data; got != want { t.Errorf("Did not get what I wanted %q, got %+v", want, got) } - checkPublicKey(t, c[matchedPolicy][0].Key.PublicKeys[0]) + checkPublicKey(t, c[matchedPolicy].Authorities[0].Key.PublicKeys[0]) matchedPolicy = "cluster-image-policy-5" want = "inlinedata here" - if got := c[matchedPolicy][0].Key.Data; got != want { + if got := c[matchedPolicy].Authorities[0].Key.Data; got != want { + t.Errorf("Did not get what I wanted %q, got %+v", want, got) + } + + // Test attestations + top level policy + c, err = defaults.GetMatchingPolicies("withattestations") + checkGetMatches(t, c, err) + if len(c) != 1 { + t.Errorf("Wanted 1 match, got %d", len(c)) + } + matchedPolicy = "cluster-image-policy-with-policy-attestations" + want = "attestation-0" + if got := c[matchedPolicy].Authorities[0].Name; got != want { + t.Errorf("Did not get what I wanted %q, got %+v", want, got) + } + // Both top & authority policy is using cue + want = "cue" + if got := c[matchedPolicy].Policy.Type; got != want { + t.Errorf("Did not get what I wanted %q, got %+v", want, got) + } + want = "cip level cue here" + if got := c[matchedPolicy].Policy.Data; got != want { + t.Errorf("Did not get what I wanted %q, got %+v", want, got) + } + want = "cue" + if got := c[matchedPolicy].Authorities[0].Attestations[0].Type; got != want { + t.Errorf("Did not get what I wanted %q, got %+v", want, got) + } + want = "test-cue-here" + if got := c[matchedPolicy].Authorities[0].Attestations[0].Data; got != want { t.Errorf("Did not get what I wanted %q, got %+v", want, got) } } -func checkGetMatches(t *testing.T, c map[string][]webhookcip.Authority, err error) { +func checkGetMatches(t *testing.T, c map[string]webhookcip.ClusterImagePolicy, err error) { t.Helper() if err != nil { t.Error("GetMatches Failed =", err) @@ -136,7 +165,7 @@ func checkGetMatches(t *testing.T, c map[string][]webhookcip.Authority, err erro t.Error("Wanted a config, got none.") } for _, v := range c { - if v != nil || len(v) > 0 { + if v.Authorities != nil || len(v.Authorities) > 0 { return } } diff --git a/pkg/apis/config/testdata/config-image-policies.yaml b/pkg/apis/config/testdata/config-image-policies.yaml index ad7154c3f02..23a0d0f6c79 100644 --- a/pkg/apis/config/testdata/config-image-policies.yaml +++ b/pkg/apis/config/testdata/config-image-policies.yaml @@ -31,21 +31,25 @@ data: images: - glob: rando authorities: - - key: + - name: attestation-0 + key: data: inlinedata here - - key: + - name: attestation-1 + key: kms: whatevs cluster-image-policy-1: | images: - glob: randomstuff* authorities: - - key: + - name: attestation-0 + key: data: otherinline here cluster-image-policy-2: | images: - glob: rando3 authorities: - - keyless: + - name: attestation-0 + keyless: ca-cert: data: cacert chilling here url: http://keylessurl.here @@ -56,7 +60,8 @@ data: images: - glob: inlinecert authorities: - - key: + - name: attestation-0 + key: data: |- -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J @@ -66,7 +71,8 @@ data: images: - regex: .*regexstring.* authorities: - - key: + - name: attestation-0 + key: data: |- -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J @@ -76,7 +82,26 @@ data: images: - regex: .*regexstringtoo.* authorities: - - key: + - name: attestation-0 + key: data: inlinedata here cluster-image-policy-json: "{\"images\":[{\"glob\":\"ghcr.io/example/*\",\"regex\":\"\"}],\"authorities\":[{\"key\":{\"data\":\"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\\n-----END PUBLIC KEY-----\"}}]}" - + cluster-image-policy-with-policy-attestations: | + images: + - glob: withattestations + authorities: + - name: attestation-0 + keyless: + ca-cert: + data: cacert chilling here + url: http://keylessurl.here + identities: + - issuer: issuer + subject: subject + attestations: + - predicateType: vuln + type: cue + data: "test-cue-here" + policy: + type: cue + data: "cip level cue here" diff --git a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_defaults.go b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_defaults.go index 66761c5ddc6..431a68d9516 100644 --- a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_defaults.go +++ b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_defaults.go @@ -14,8 +14,20 @@ package v1alpha1 -import "context" +import ( + "context" + "fmt" +) // SetDefaults implements apis.Defaultable -func (*ClusterImagePolicy) SetDefaults(ctx context.Context) { +func (c *ClusterImagePolicy) SetDefaults(ctx context.Context) { + c.Spec.SetDefaults(ctx) +} + +func (spec *ClusterImagePolicySpec) SetDefaults(ctx context.Context) { + for i, authority := range spec.Authorities { + if authority.Name == "" { + spec.Authorities[i].Name = fmt.Sprintf("authority-%d", i) + } + } } diff --git a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_defaults_test.go b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_defaults_test.go new file mode 100644 index 00000000000..0a1ba8e27bf --- /dev/null +++ b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_defaults_test.go @@ -0,0 +1,63 @@ +// 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 v1alpha1 + +import ( + "context" + + "testing" + + "knative.dev/pkg/apis" +) + +func TestNameDefaulting(t *testing.T) { + tests := []struct { + in *ClusterImagePolicy + wantNames []string + }{ + {in: cipWithNames([]string{""}), + wantNames: []string{"authority-0"}, + }, + {in: cipWithNames([]string{"", "vuln-scan"}), + wantNames: []string{"authority-0", "vuln-scan"}, + }, + {in: cipWithNames([]string{"vuln-scan", ""}), + wantNames: []string{"vuln-scan", "authority-1"}, + }, + {in: cipWithNames([]string{"first", "second"}), + wantNames: []string{"first", "second"}, + }} + for _, tc := range tests { + tc.in.SetDefaults(context.TODO()) + if len(tc.in.Spec.Authorities) != len(tc.wantNames) { + t.Fatalf("Mismatch number of wantNames: %d vs authorities: %d", len(tc.wantNames), len(tc.in.Spec.Authorities)) + } + for i, wantName := range tc.wantNames { + if tc.in.Spec.Authorities[i].Name != wantName { + t.Errorf("Wanted name: %s got %s", wantName, tc.in.Spec.Authorities[i].Name) + } + } + } +} + +func cipWithNames(names []string) *ClusterImagePolicy { + cip := &ClusterImagePolicy{ + Spec: ClusterImagePolicySpec{}, + } + for _, name := range names { + cip.Spec.Authorities = append(cip.Spec.Authorities, Authority{Name: name, Keyless: &KeylessRef{URL: &apis.URL{Host: "tests.example.com"}}}) + } + return cip +} diff --git a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_types.go b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_types.go index d8f30028da4..bad7c36483b 100644 --- a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_types.go +++ b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_types.go @@ -46,7 +46,7 @@ var ( ) // GetGroupVersionKind implements kmeta.OwnerRefable -func (*ClusterImagePolicy) GetGroupVersionKind() schema.GroupVersionKind { +func (c *ClusterImagePolicy) GetGroupVersionKind() schema.GroupVersionKind { return SchemeGroupVersion.WithKind("ClusterImagePolicy") } @@ -54,6 +54,11 @@ func (*ClusterImagePolicy) GetGroupVersionKind() schema.GroupVersionKind { type ClusterImagePolicySpec struct { Images []ImagePattern `json:"images"` Authorities []Authority `json:"authorities"` + // Policy is an optional policy that can be applied against all the + // successfully validated Authorities. If no authorities pass, this does + // not even get evaluated, as the Policy is considered failed. + // +optional + Policy *Policy `json:"policy,omitempty"` } // ImagePattern defines a pattern and its associated authorties @@ -75,6 +80,11 @@ type ImagePattern struct { // image. type Authority struct { + // Name is the name for this authority. Used by the CIP Policy + // validator to be able to reference matching signature or attestation + // verifications. + // If not specified, the name will be authority- + Name string `json:"name"` // +optional Key *KeyRef `json:"key,omitempty"` // +optional @@ -83,6 +93,8 @@ type Authority struct { Sources []Source `json:"source,omitempty"` // +optional CTLog *TLog `json:"ctlog,omitempty"` + // +optional + Attestations []Attestation `json:"attestations,omitempty"` } // This references a public verification key stored in @@ -124,6 +136,45 @@ type KeylessRef struct { CACert *KeyRef `json:"ca-cert,omitempty"` } +// Attestation defines the type of attestation to validate and optionally +// apply a policy decision to it. Authority block is used to verify the +// specified attestation types, and if Policy is specified, then it's applied +// only after the validation of the Attestation signature has been verified. +type Attestation struct { + // Name of the attestation. These can then be referenced at the CIP level + // policy. + Name string `json:"name"` + // Which predicate type to verify. Matches cosign verify-attestation options. + PredicateType string `json:"predicateType"` + // +optional + Policy *Policy `json:"policy,omitempty"` +} + +// Policy specifies a policy to use for Attestation validation. +// Exactly one of Data, URL, or ConfigMapReference must be specified. +type Policy struct { + // Which kind of policy this is, currently only rego or cue are supported. + // Furthermore, only cue is tested :) + Type string `json:"type"` + // +optional + Data string `json:"data,omitempty"` + // +optional + URL *apis.URL `json:"url,omitempty"` + // +optional + ConfigMapRef *ConfigMapReference `json:"configMapRef,omitempty"` +} + +// ConfigMapReference is cut&paste from SecretReference, but for the life of me +// couldn't find one in the public types. If there's one, use it. +type ConfigMapReference struct { + // Name is unique within a namespace to reference a configmap resource. + // +optional + Name string `json:"name,omitempty"` + // Namespace defines the space within which the configmap name must be unique. + // +optional + Namespace string `json:"namespace,omitempty"` +} + // Identity may contain the issuer and/or the subject found in the transparency log. // Either field supports a pattern glob. type Identity struct { diff --git a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go index 81d0f2065fe..a307f33a834 100644 --- a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go +++ b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go @@ -25,8 +25,8 @@ import ( ) // Validate implements apis.Validatable -func (policy *ClusterImagePolicy) Validate(ctx context.Context) *apis.FieldError { - return policy.Spec.Validate(ctx).ViaField("spec") +func (c *ClusterImagePolicy) Validate(ctx context.Context) *apis.FieldError { + return c.Spec.Validate(ctx).ViaField("spec") } func (spec *ClusterImagePolicySpec) Validate(ctx context.Context) (errors *apis.FieldError) { @@ -42,6 +42,7 @@ func (spec *ClusterImagePolicySpec) Validate(ctx context.Context) (errors *apis. for i, authority := range spec.Authorities { errors = errors.Also(authority.Validate(ctx).ViaFieldIndex("authorities", i)) } + errors = errors.Also(spec.Policy.Validate(ctx)) return } @@ -83,10 +84,12 @@ func (authority *Authority) Validate(ctx context.Context) *apis.FieldError { errs = errs.Also(authority.Keyless.Validate(ctx).ViaField("keyless")) } - if len(authority.Sources) > 0 { - for _, source := range authority.Sources { - errs = errs.Also(source.Validate(ctx).ViaField("source")) - } + for _, source := range authority.Sources { + errs = errs.Also(source.Validate(ctx).ViaField("source")) + } + + for _, att := range authority.Attestations { + errs = errs.Also(att.Validate(ctx).ViaField("attestations")) } return errs @@ -144,6 +147,39 @@ func (source *Source) Validate(ctx context.Context) *apis.FieldError { return errs } +func (a *Attestation) Validate(ctx context.Context) *apis.FieldError { + var errs *apis.FieldError + if a.Name == "" { + errs = errs.Also(apis.ErrMissingField("name")) + } + if a.PredicateType == "" { + errs = errs.Also(apis.ErrMissingField("predicateType")) + } else if a.PredicateType != "custom" && a.PredicateType != "slsaprovenance" && a.PredicateType != "spdx" && a.PredicateType != "link" && a.PredicateType != "vuln" { + // TODO(vaikas): The above should be using something like: + // if _, ok := options.PredicateTypeMap[a.PrecicateType]; !ok { + // But it causes an import loop. That refactor can be part of + // another PR. + errs = errs.Also(apis.ErrInvalidValue(a.PredicateType, "predicateType", "unsupported precicate type")) + } + errs = errs.Also(a.Policy.Validate(ctx).ViaField("policy")) + return errs +} + +func (p *Policy) Validate(ctx context.Context) *apis.FieldError { + if p == nil { + return nil + } + var errs *apis.FieldError + if p.Type != "cue" { + errs = errs.Also(apis.ErrInvalidValue(p.Type, "type", "only cue is supported at the moment")) + } + if p.Data == "" { + errs = errs.Also(apis.ErrMissingField("data")) + } + // TODO(vaikas): How to validate the cue / rego bytes here (data). + return errs +} + func (identity *Identity) Validate(ctx context.Context) *apis.FieldError { var errs *apis.FieldError if identity.Issuer == "" && identity.Subject == "" { diff --git a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go index 5c288188e9d..9848f0a15e5 100644 --- a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go +++ b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go @@ -446,6 +446,46 @@ func TestAuthoritiesValidation(t *testing.T) { }, }, }, + { + name: "Should pass with multiple source oci is present", + expectErr: false, + policy: ClusterImagePolicy{ + Spec: ClusterImagePolicySpec{ + Images: []ImagePattern{{Regex: ".*"}}, + Authorities: []Authority{ + { + Key: &KeyRef{KMS: "kms://key/path"}, + Sources: []Source{ + {OCI: "registry1"}, + {OCI: "registry2"}, + }, + }, + }, + }, + }, + }, + { + name: "Should pass with attestations present", + expectErr: false, + policy: ClusterImagePolicy{ + Spec: ClusterImagePolicySpec{ + Images: []ImagePattern{{Regex: ".*"}}, + Authorities: []Authority{ + { + Key: &KeyRef{KMS: "kms://key/path"}, + Attestations: []Attestation{ + {Name: "first", PredicateType: "vuln"}, + {Name: "second", PredicateType: "custom", Policy: &Policy{ + Type: "cue", + Data: `predicateType: "cosign.sigstore.dev/attestation/vuln/v1"`, + }, + }, + }, + }, + }, + }, + }, + }, } for _, test := range tests { @@ -461,6 +501,72 @@ func TestAuthoritiesValidation(t *testing.T) { } } +func TestAttestationsValidation(t *testing.T) { + tests := []struct { + name string + expectErr bool + errorString string + attestation Attestation + }{{ + name: "vuln", + attestation: Attestation{Name: "first", PredicateType: "vuln"}, + }, { + name: "missing name", + attestation: Attestation{PredicateType: "vuln"}, + expectErr: true, + errorString: "missing field(s): name", + }, { + name: "missing predicatetype", + attestation: Attestation{Name: "first"}, + expectErr: true, + errorString: "missing field(s): predicateType", + }, { + name: "invalid predicatetype", + attestation: Attestation{Name: "first", PredicateType: "notsupported"}, + expectErr: true, + errorString: "invalid value: notsupported: predicateType\nunsupported precicate type", + }, { + name: "custom with invalid policy type", + attestation: Attestation{Name: "second", PredicateType: "custom", + Policy: &Policy{ + Type: "not-cue", + Data: `predicateType: "cosign.sigstore.dev/attestation/vuln/v1"`, + }, + }, + expectErr: true, + errorString: "invalid value: not-cue: policy.type\nonly cue is supported at the moment", + }, { + name: "custom with missing policy data", + attestation: Attestation{Name: "second", PredicateType: "custom", + Policy: &Policy{ + Type: "cue", + }, + }, + expectErr: true, + errorString: "missing field(s): policy.data", + }, { + name: "custom with policy", + attestation: Attestation{Name: "second", PredicateType: "custom", + Policy: &Policy{ + Type: "cue", + Data: `predicateType: "cosign.sigstore.dev/attestation/vuln/v1"`, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.attestation.Validate(context.TODO()) + if test.expectErr { + require.NotNil(t, err) + require.EqualError(t, err, test.errorString) + } else { + require.Nil(t, err) + } + }) + } +} func TestIdentitiesValidation(t *testing.T) { tests := []struct { name string diff --git a/pkg/apis/cosigned/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/cosigned/v1alpha1/zz_generated.deepcopy.go index 2c1c886712c..c6926846691 100644 --- a/pkg/apis/cosigned/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/cosigned/v1alpha1/zz_generated.deepcopy.go @@ -25,6 +25,27 @@ import ( apis "knative.dev/pkg/apis" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Attestation) DeepCopyInto(out *Attestation) { + *out = *in + if in.Policy != nil { + in, out := &in.Policy, &out.Policy + *out = new(Policy) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Attestation. +func (in *Attestation) DeepCopy() *Attestation { + if in == nil { + return nil + } + out := new(Attestation) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Authority) DeepCopyInto(out *Authority) { *out = *in @@ -48,6 +69,13 @@ func (in *Authority) DeepCopyInto(out *Authority) { *out = new(TLog) (*in).DeepCopyInto(*out) } + if in.Attestations != nil { + in, out := &in.Attestations, &out.Attestations + *out = make([]Attestation, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } @@ -136,6 +164,11 @@ func (in *ClusterImagePolicySpec) DeepCopyInto(out *ClusterImagePolicySpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Policy != nil { + in, out := &in.Policy, &out.Policy + *out = new(Policy) + (*in).DeepCopyInto(*out) + } return } @@ -149,6 +182,22 @@ func (in *ClusterImagePolicySpec) DeepCopy() *ClusterImagePolicySpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigMapReference) DeepCopyInto(out *ConfigMapReference) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigMapReference. +func (in *ConfigMapReference) DeepCopy() *ConfigMapReference { + if in == nil { + return nil + } + out := new(ConfigMapReference) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Identity) DeepCopyInto(out *Identity) { *out = *in @@ -233,6 +282,32 @@ func (in *KeylessRef) DeepCopy() *KeylessRef { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Policy) DeepCopyInto(out *Policy) { + *out = *in + if in.URL != nil { + in, out := &in.URL, &out.URL + *out = new(apis.URL) + (*in).DeepCopyInto(*out) + } + if in.ConfigMapRef != nil { + in, out := &in.ConfigMapRef, &out.ConfigMapRef + *out = new(ConfigMapReference) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Policy. +func (in *Policy) DeepCopy() *Policy { + if in == nil { + return nil + } + out := new(Policy) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Source) DeepCopyInto(out *Source) { *out = *in diff --git a/pkg/cosign/kubernetes/webhook/clusterimagepolicy/clusterimagepolicy_types.go b/pkg/cosign/kubernetes/webhook/clusterimagepolicy/clusterimagepolicy_types.go index 5da8c7f83f2..cbda0c7c0fb 100644 --- a/pkg/cosign/kubernetes/webhook/clusterimagepolicy/clusterimagepolicy_types.go +++ b/pkg/cosign/kubernetes/webhook/clusterimagepolicy/clusterimagepolicy_types.go @@ -36,9 +36,17 @@ import ( type ClusterImagePolicy struct { Images []v1alpha1.ImagePattern `json:"images"` Authorities []Authority `json:"authorities"` + // Policy is an optional policy used to evaluate the results of valid + // Authorities. Will not get evaluated unless at least one Authority + // succeeds. + Policy *AttestationPolicy `json:"policy,omitempty"` } type Authority struct { + // Name is the name for this authority. Used by the CIP Policy + // validator to be able to reference matching signature or attestation + // verifications. + Name string `json:"name"` // +optional Key *KeyRef `json:"key,omitempty"` // +optional @@ -51,6 +59,8 @@ type Authority struct { // RemoteOpts will be populated by the Authority UnmarshalJSON override // +optional RemoteOpts []remote.Option `json:"-"` + // +optional + Attestations []AttestationPolicy `json:"attestations,omitempty"` } // This references a public verification key stored in @@ -74,6 +84,18 @@ type KeylessRef struct { CACert *KeyRef `json:"ca-cert,omitempty"` } +type AttestationPolicy struct { + // Name of the Attestation + Name string `json:"name"` + // PredicateType to attest, one of the accepted in verify-attestation + PredicateType string `json:"predicateType"` + // Type specifies how to evaluate policy, only rego/cue are understood. + Type string `json:"type,omitempty"` + // Data is the inlined version of the Policy used to evaluate the + // Attestation. + Data string `json:"data,omitempty"` +} + // UnmarshalJSON populates the PublicKeys using Data because // JSON unmashalling errors for *big.Int func (k *KeyRef) UnmarshalJSON(data []byte) error { @@ -136,22 +158,50 @@ func ConvertClusterImagePolicyV1alpha1ToWebhook(in *v1alpha1.ClusterImagePolicy) outAuthorities = append(outAuthorities, *outAuthority) } + // If there's a ClusterImagePolicy level AttestationPolicy, convert it here. + var cipAttestationPolicy *AttestationPolicy + if in.Spec.Policy != nil { + cipAttestationPolicy = &AttestationPolicy{ + Type: in.Spec.Policy.Type, + Data: in.Spec.Policy.Data, + } + } return &ClusterImagePolicy{ Images: copyIn.Spec.Images, Authorities: outAuthorities, + Policy: cipAttestationPolicy, } } func convertAuthorityV1Alpha1ToWebhook(in v1alpha1.Authority) *Authority { keyRef := convertKeyRefV1Alpha1ToWebhook(in.Key) keylessRef := convertKeylessRefV1Alpha1ToWebhook(in.Keyless) + attestations := convertAttestationsV1Alpha1ToWebhook(in.Attestations) return &Authority{ - Key: keyRef, - Keyless: keylessRef, - Sources: in.Sources, - CTLog: in.CTLog, + Name: in.Name, + Key: keyRef, + Keyless: keylessRef, + Sources: in.Sources, + CTLog: in.CTLog, + Attestations: attestations, + } +} + +func convertAttestationsV1Alpha1ToWebhook(in []v1alpha1.Attestation) []AttestationPolicy { + ret := []AttestationPolicy{} + for _, inAtt := range in { + outAtt := AttestationPolicy{ + Name: inAtt.Name, + PredicateType: inAtt.PredicateType, + } + if inAtt.Policy != nil { + outAtt.Type = inAtt.Policy.Type + outAtt.Data = inAtt.Policy.Data + } + ret = append(ret, outAtt) } + return ret } func convertKeyRefV1Alpha1ToWebhook(in *v1alpha1.KeyRef) *KeyRef { diff --git a/pkg/cosign/kubernetes/webhook/validation.go b/pkg/cosign/kubernetes/webhook/validation.go index be74173daa7..278cc5439fc 100644 --- a/pkg/cosign/kubernetes/webhook/validation.go +++ b/pkg/cosign/kubernetes/webhook/validation.go @@ -36,7 +36,7 @@ import ( "github.com/sigstore/sigstore/pkg/signature" ) -func valid(ctx context.Context, ref name.Reference, keys []crypto.PublicKey, opts ...ociremote.Option) ([]oci.Signature, error) { +func valid(ctx context.Context, ref name.Reference, rekorClient *client.Rekor, keys []crypto.PublicKey, opts ...ociremote.Option) ([]oci.Signature, error) { if len(keys) == 0 { // If there are no keys, then verify against the fulcio root. sps, err := validSignaturesWithFulcio(ctx, ref, fulcioroots.Get(), nil /* rekor */, nil /* no identities */, opts...) @@ -58,7 +58,7 @@ func valid(ctx context.Context, ref name.Reference, keys []crypto.PublicKey, opt continue } - sps, err := validSignatures(ctx, ref, verifier, opts...) + sps, err := validSignatures(ctx, ref, verifier, rekorClient, opts...) if err != nil { logging.FromContext(ctx).Errorf("error validating signatures: %v", err) lastErr = err @@ -74,11 +74,13 @@ func valid(ctx context.Context, ref name.Reference, keys []crypto.PublicKey, opt // For testing var cosignVerifySignatures = cosign.VerifyImageSignatures +var cosignVerifyAttestations = cosign.VerifyImageAttestations -func validSignatures(ctx context.Context, ref name.Reference, verifier signature.Verifier, opts ...ociremote.Option) ([]oci.Signature, error) { +func validSignatures(ctx context.Context, ref name.Reference, verifier signature.Verifier, rekorClient *client.Rekor, opts ...ociremote.Option) ([]oci.Signature, error) { sigs, _, err := cosignVerifySignatures(ctx, ref, &cosign.CheckOpts{ RegistryClientOpts: opts, SigVerifier: verifier, + RekorClient: rekorClient, ClaimVerifier: cosign.SimpleClaimVerifier, }) return sigs, err @@ -101,6 +103,34 @@ func validSignaturesWithFulcio(ctx context.Context, ref name.Reference, fulcioRo return sigs, err } +func validAttestations(ctx context.Context, ref name.Reference, verifier signature.Verifier, rekorClient *client.Rekor, opts ...ociremote.Option) ([]oci.Signature, error) { + attestations, _, err := cosignVerifyAttestations(ctx, ref, &cosign.CheckOpts{ + RegistryClientOpts: opts, + SigVerifier: verifier, + RekorClient: rekorClient, + ClaimVerifier: cosign.IntotoSubjectClaimVerifier, + }) + return attestations, err +} + +// validAttestationsWithFulcio expects a Fulcio Cert to verify against. An +// optional rekorClient can also be given, if nil passed, default is assumed. +func validAttestationsWithFulcio(ctx context.Context, ref name.Reference, fulcioRoots *x509.CertPool, rekorClient *client.Rekor, identities []v1alpha1.Identity, opts ...ociremote.Option) ([]oci.Signature, error) { + ids := make([]cosign.Identity, len(identities)) + for i, id := range identities { + ids[i] = cosign.Identity{Issuer: id.Issuer, Subject: id.Subject} + } + + attestations, _, err := cosignVerifyAttestations(ctx, ref, &cosign.CheckOpts{ + RegistryClientOpts: opts, + RootCerts: fulcioRoots, + RekorClient: rekorClient, + ClaimVerifier: cosign.IntotoSubjectClaimVerifier, + Identities: ids, + }) + return attestations, err +} + func getKeys(ctx context.Context, cfg map[string][]byte) ([]crypto.PublicKey, *apis.FieldError) { keys := []crypto.PublicKey{} errs := []error{} diff --git a/pkg/cosign/kubernetes/webhook/validator.go b/pkg/cosign/kubernetes/webhook/validator.go index dc9ad4c037e..7590892a589 100644 --- a/pkg/cosign/kubernetes/webhook/validator.go +++ b/pkg/cosign/kubernetes/webhook/validator.go @@ -17,10 +17,11 @@ package webhook import ( "context" + "crypto" "crypto/x509" + "encoding/json" "fmt" - "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/authn/k8schain" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" @@ -29,9 +30,11 @@ import ( webhookcip "github.com/sigstore/cosign/pkg/cosign/kubernetes/webhook/clusterimagepolicy" "github.com/sigstore/cosign/pkg/oci" ociremote "github.com/sigstore/cosign/pkg/oci/remote" + "github.com/sigstore/cosign/pkg/policy" "github.com/sigstore/fulcio/pkg/api" rekor "github.com/sigstore/rekor/pkg/client" "github.com/sigstore/rekor/pkg/generated/client" + "github.com/sigstore/sigstore/pkg/signature" corev1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" listersv1 "k8s.io/client-go/listers/core/v1" @@ -168,15 +171,20 @@ func (v *Validator) validatePodSpec(ctx context.Context, ps *corev1.PodSpec, opt // If there is at least one policy that matches, that means it // has to be satisfied. if len(policies) > 0 { - signatures, fieldErrors := validatePolicies(ctx, ref, kc, policies) + signatures, fieldErrors := validatePolicies(ctx, ref, policies, ociremote.WithRemoteOptions(remote.WithAuthFromKeychain(kc))) + if len(signatures) != len(policies) { logging.FromContext(ctx).Warnf("Failed to validate at least one policy for %s", ref.Name()) // Do we really want to add all the error details here? // Seems like we can just say which policy failed, so // doing that for now. - for failingPolicy := range fieldErrors { + for failingPolicy, policyErrs := range fieldErrors { errorField := apis.ErrGeneric(fmt.Sprintf("failed policy: %s", failingPolicy), "image").ViaFieldIndex(field, i) - errorField.Details = c.Image + errDetails := c.Image + for _, policyErr := range policyErrs { + errDetails = errDetails + " " + policyErr.Error() + } + errorField.Details = errDetails errs = errs.Also(errorField) } // Because there was at least one policy that was @@ -203,7 +211,7 @@ func (v *Validator) validatePodSpec(ctx context.Context, ps *corev1.PodSpec, opt continue } - if _, err := valid(ctx, ref, containerKeys, ociremote.WithRemoteOptions(remote.WithAuthFromKeychain(kc))); err != nil { + if _, err := valid(ctx, ref, nil, containerKeys, ociremote.WithRemoteOptions(remote.WithAuthFromKeychain(kc))); err != nil { errorField := apis.ErrGeneric(err.Error(), "image").ViaFieldIndex(field, i) errorField.Details = c.Image errs = errs.Also(errorField) @@ -227,9 +235,9 @@ func (v *Validator) validatePodSpec(ctx context.Context, ps *corev1.PodSpec, opt // Note that if an image does not match any policies, it's perfectly // reasonable that the return value is 0, nil since there were no errors, but // the image was not validated against any matching policy and hence authority. -func validatePolicies(ctx context.Context, ref name.Reference, kc authn.Keychain, policies map[string][]webhookcip.Authority, remoteOpts ...ociremote.Option) (map[string][]oci.Signature, map[string][]error) { - // Gather all validated signatures here. - signatures := map[string][]oci.Signature{} +func validatePolicies(ctx context.Context, ref name.Reference, policies map[string]webhookcip.ClusterImagePolicy, remoteOpts ...ociremote.Option) (map[string]*PolicyResult, map[string][]error) { + // Gather all validated policies here. + policyResults := make(map[string]*PolicyResult) // For a policy that does not pass at least one authority, gather errors // here so that we can give meaningful errors to the user. ret := map[string][]error{} @@ -240,85 +248,232 @@ func validatePolicies(ctx context.Context, ref name.Reference, kc authn.Keychain // policies must be satisfied for the image to be admitted." // If none of the Authorities for a given policy pass the checks, gather // the errors here. If one passes, do not return the errors. - authorityErrors := []error{} - for p, authorities := range policies { - logging.FromContext(ctx).Debugf("Checking Policy: %s", p) - sigs, errs := ValidatePolicy(ctx, ref, kc, authorities, remoteOpts...) + for cipName, cip := range policies { + logging.FromContext(ctx).Debugf("Checking Policy: %s", cipName) + policyResult, errs := ValidatePolicy(ctx, ref, cip, remoteOpts...) if len(errs) > 0 { - ret[p] = append(ret[p], authorityErrors...) + ret[cipName] = append(ret[cipName], errs...) } else { - signatures[p] = append(signatures[p], sigs...) + // Ok, at least one Authority on the policy passed. If there's a CIP level + // policy, apply it against the results of the successful Authorities + // outputs. + if cip.Policy != nil { + logging.FromContext(ctx).Infof("Validating CIP level policy for %s", cipName) + policyJSON, err := json.Marshal(policyResult) + if err != nil { + ret[cipName] = append(ret[cipName], errors.Wrap(err, "marshaling policyresult")) + } else { + logging.FromContext(ctx).Infof("Validating CIP level policy against %s", string(policyJSON)) + err = policy.EvaluatePolicyAgainstJSON(ctx, "ClusterImagePolicy", cip.Policy.Type, cip.Policy.Data, policyJSON) + if err != nil { + ret[cipName] = append(ret[cipName], err) + } else { + policyResults[cipName] = policyResult + } + } + } else { + policyResults[cipName] = policyResult + } } } - return signatures, ret + return policyResults, ret } -// ValidatePolicy will go through all the Authorities for a given image and -// return a success if at least one of the Authorities validated the signatures. -// Returns the validated signatures, or the errors encountered. -func ValidatePolicy(ctx context.Context, ref name.Reference, kc authn.Keychain, authorities []webhookcip.Authority, remoteOpts ...ociremote.Option) ([]oci.Signature, []error) { - remoteOpts = append(remoteOpts, ociremote.WithRemoteOptions(remote.WithAuthFromKeychain(kc))) - +// ValidatePolicy will go through all the Authorities for a given image/policy +// and return a success if at least one of the Authorities validated the +// signatures OR attestations if atttestations were specified. +// Returns PolicyResult, or errors encountered if none of the authorities +// passed. +func ValidatePolicy(ctx context.Context, ref name.Reference, cip webhookcip.ClusterImagePolicy, remoteOpts ...ociremote.Option) (*PolicyResult, []error) { // If none of the Authorities for a given policy pass the checks, gather // the errors here. If one passes, do not return the errors. authorityErrors := []error{} - for _, authority := range authorities { + // We collect all the successfully satisfied Authorities into this and + // return it. + policyResult := PolicyResult{AuthorityMatches: make(map[string]AuthorityMatch)} + for _, authority := range cip.Authorities { + logging.FromContext(ctx).Debugf("Checking Authority: %s", authority.Name) // Assignment for appendAssign lint error authorityRemoteOpts := remoteOpts authorityRemoteOpts = append(authorityRemoteOpts, authority.RemoteOpts...) - switch { - case authority.Key != nil && len(authority.Key.PublicKeys) > 0: - // TODO(vaikas): What should happen if there are multiple keys - // Is it even allowed? 'valid' returns success if any key - // matches. - // https://github.com/sigstore/cosign/issues/1652 - sps, err := valid(ctx, ref, authority.Key.PublicKeys, authorityRemoteOpts...) + if len(authority.Attestations) > 0 { + // We're doing the verify-attestations path, so validate (.att) + validatedAttestations, err := ValidatePolicyAttestationsForAuthority(ctx, ref, authority, authorityRemoteOpts...) if err != nil { - authorityErrors = append(authorityErrors, errors.Wrap(err, "failed to validate keys")) - continue + authorityErrors = append(authorityErrors, err) } else { - if len(sps) > 0 { - logging.FromContext(ctx).Debugf("validated signature for %s, got %d signatures", ref.Name(), len(sps)) - return sps, nil - } - logging.FromContext(ctx).Errorf("no validSignatures found for %s", ref.Name()) - authorityErrors = append(authorityErrors, fmt.Errorf("no valid signatures found for %s", ref.Name())) + policyResult.AuthorityMatches[authority.Name] = AuthorityMatch{Attestations: validatedAttestations} } - case authority.Keyless != nil: - if authority.Keyless != nil && authority.Keyless.URL != nil { - logging.FromContext(ctx).Debugf("Fetching FulcioRoot for %s : From: %s ", ref.Name(), authority.Keyless.URL) - fulcioroot, err := getFulcioCert(authority.Keyless.URL) - if err != nil { - authorityErrors = append(authorityErrors, errors.Wrap(err, "fetching FulcioRoot")) - continue - } - var rekorClient *client.Rekor - if authority.CTLog != nil && authority.CTLog.URL != nil { - logging.FromContext(ctx).Debugf("Using CTLog %s for %s", authority.CTLog.URL, ref.Name()) - rekorClient, err = rekor.GetRekorClient(authority.CTLog.URL.String()) - if err != nil { - logging.FromContext(ctx).Errorf("failed creating rekor client: +v", err) - authorityErrors = append(authorityErrors, errors.Wrap(err, "creating Rekor client")) - continue - } - } - sps, err := validSignaturesWithFulcio(ctx, ref, fulcioroot, rekorClient, authority.Keyless.Identities, authorityRemoteOpts...) - if err != nil { - logging.FromContext(ctx).Errorf("failed validSignatures with fulcio for %s: %v", ref.Name(), err) - authorityErrors = append(authorityErrors, errors.Wrap(err, "validate signatures with fulcio")) - } else { - if len(sps) > 0 { - logging.FromContext(ctx).Debugf("validated signature for %s, got %d signatures", ref.Name(), len(sps)) - return sps, nil - } - logging.FromContext(ctx).Errorf("no validSignatures found for %s", ref.Name()) - authorityErrors = append(authorityErrors, fmt.Errorf("no valid signatures found for %s", ref.Name())) - } + } else { + // We're doing the verify path, so validate image signatures (.sig) + validatedSignatures, err := ValidatePolicySignaturesForAuthority(ctx, ref, authority, authorityRemoteOpts...) + if err != nil { + authorityErrors = append(authorityErrors, err) + } else { + policyResult.AuthorityMatches[authority.Name] = AuthorityMatch{Signatures: validatedSignatures} + } + } + } + if len(authorityErrors) > 0 { + return nil, authorityErrors + } + return &policyResult, authorityErrors +} + +func ociSignatureToPolicySignature(ctx context.Context, sigs []oci.Signature) []PolicySignature { + // TODO(vaikas): Validate whether these are useful at all, or if we should + // simplify at least for starters. + ret := []PolicySignature{} + for _, ociSig := range sigs { + logging.FromContext(ctx).Debugf("Converting signature %+v", ociSig) + ret = append(ret, PolicySignature{Subject: "PLACEHOLDER", Issuer: "PLACEHOLDER"}) + } + return ret +} + +// ValidatePolicySignaturesForAuthority takes the Authority and tries to +// verify a signature against it. +func ValidatePolicySignaturesForAuthority(ctx context.Context, ref name.Reference, authority webhookcip.Authority, remoteOpts ...ociremote.Option) ([]PolicySignature, error) { + name := authority.Name + + var rekorClient *client.Rekor + var err error + if authority.CTLog != nil && authority.CTLog.URL != nil { + logging.FromContext(ctx).Debugf("Using CTLog %s for %s", authority.CTLog.URL, ref.Name()) + rekorClient, err = rekor.GetRekorClient(authority.CTLog.URL.String()) + if err != nil { + logging.FromContext(ctx).Errorf("failed creating rekor client: +v", err) + return nil, errors.Wrap(err, "creating Rekor client") + } + } + + switch { + case authority.Key != nil && len(authority.Key.PublicKeys) > 0: + // TODO(vaikas): What should happen if there are multiple keys + // Is it even allowed? 'valid' returns success if any key + // matches. + // https://github.com/sigstore/cosign/issues/1652 + sps, err := valid(ctx, ref, rekorClient, authority.Key.PublicKeys, remoteOpts...) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("failed to validate public keys with authority %s for %s", name, ref.Name())) + } else if len(sps) > 0 { + logging.FromContext(ctx).Debugf("validated signature for %s with authority %s got %d signatures", ref.Name(), authority.Name, len(sps)) + return ociSignatureToPolicySignature(ctx, sps), nil + } + logging.FromContext(ctx).Errorf("no validSignatures found with authority %s for %s", name, ref.Name()) + return nil, fmt.Errorf("no valid signatures found with authority %s for %s", name, ref.Name()) + case authority.Keyless != nil: + if authority.Keyless != nil && authority.Keyless.URL != nil { + logging.FromContext(ctx).Debugf("Fetching FulcioRoot for %s : From: %s ", ref.Name(), authority.Keyless.URL) + fulcioroot, err := getFulcioCert(authority.Keyless.URL) + if err != nil { + return nil, errors.Wrap(err, "fetching FulcioRoot") + } + sps, err := validSignaturesWithFulcio(ctx, ref, fulcioroot, rekorClient, authority.Keyless.Identities, remoteOpts...) + if err != nil { + logging.FromContext(ctx).Errorf("failed validSignatures for authority %s with fulcio for %s: %v", name, ref.Name(), err) + return nil, errors.Wrap(err, "validate signatures with fulcio") + } else if len(sps) > 0 { + logging.FromContext(ctx).Debugf("validated signature for %s, got %d signatures", ref.Name(), len(sps)) + return ociSignatureToPolicySignature(ctx, sps), nil + } + logging.FromContext(ctx).Errorf("no validSignatures found for %s", ref.Name()) + return nil, fmt.Errorf("no valid signatures found with authority %s for %s", name, ref.Name()) + } + } + // This should never happen because authority has to have been + // validated to be either having a Key or Keyless + return nil, fmt.Errorf("authority has neither key or keyless specified") +} + +// ValidatePolicyAttestationsForAuthority takes the Authority and tries to +// verify attestations against it. +func ValidatePolicyAttestationsForAuthority(ctx context.Context, ref name.Reference, authority webhookcip.Authority, remoteOpts ...ociremote.Option) (map[string][]PolicySignature, error) { + name := authority.Name + var rekorClient *client.Rekor + var err error + if authority.CTLog != nil && authority.CTLog.URL != nil { + logging.FromContext(ctx).Debugf("Using CTLog %s for %s", authority.CTLog.URL, ref.Name()) + rekorClient, err = rekor.GetRekorClient(authority.CTLog.URL.String()) + if err != nil { + logging.FromContext(ctx).Errorf("failed creating rekor client: +v", err) + return nil, errors.Wrap(err, "creating Rekor client") + } + } + + verifiedAttestations := []oci.Signature{} + switch { + case authority.Key != nil && len(authority.Key.PublicKeys) > 0: + for _, k := range authority.Key.PublicKeys { + verifier, err := signature.LoadVerifier(k, crypto.SHA256) + if err != nil { + logging.FromContext(ctx).Errorf("error creating verifier: %v", err) + return nil, errors.Wrap(err, "creating verifier") + } + va, err := validAttestations(ctx, ref, verifier, rekorClient, remoteOpts...) + if err != nil { + logging.FromContext(ctx).Errorf("error validating attestations: %v", err) + return nil, errors.Wrap(err, "validating attestations") + } + verifiedAttestations = append(verifiedAttestations, va...) + } + logging.FromContext(ctx).Debug("No valid signatures were found.") + case authority.Keyless != nil: + if authority.Keyless != nil && authority.Keyless.URL != nil { + logging.FromContext(ctx).Debugf("Fetching FulcioRoot for %s : From: %s ", ref.Name(), authority.Keyless.URL) + fulcioroot, err := getFulcioCert(authority.Keyless.URL) + if err != nil { + return nil, errors.Wrap(err, "fetching FulcioRoot") + } + va, err := validAttestationsWithFulcio(ctx, ref, fulcioroot, rekorClient, authority.Keyless.Identities, remoteOpts...) + if err != nil { + logging.FromContext(ctx).Errorf("failed validAttestationsWithFulcio for authority %s with fulcio for %s: %v", name, ref.Name(), err) + return nil, errors.Wrap(err, "validate signatures with fulcio") + } + verifiedAttestations = append(verifiedAttestations, va...) + } + } + // If we didn't get any verified attestations either from the Key or Keyless + // path, then error out + if len(verifiedAttestations) == 0 { + logging.FromContext(ctx).Errorf("no valid attestations found with authority %s for %s", name, ref.Name()) + return nil, fmt.Errorf("no valid attestations found with authority %s for %s", name, ref.Name()) + } + logging.FromContext(ctx).Debugf("Found %d valid attestations, validating policies for them", len(verifiedAttestations)) + // Now spin through the Attestations that the user specified and validate + // them. + // TODO(vaikas): Pretty inefficient here, figure out a better way if + // possible. + ret := map[string][]PolicySignature{} + for _, wantedAttestation := range authority.Attestations { + // If there's no type / policy to do more checking against, + // then we're done here. It matches all the attestations + if wantedAttestation.Type == "" { + ret[wantedAttestation.Name] = ociSignatureToPolicySignature(ctx, verifiedAttestations) + continue + } + // There's a particular type, so we need to go through all the verified + // attestations and make sure that our particular one is satisfied. + for _, va := range verifiedAttestations { + attBytes, err := policy.AttestationToPayloadJSON(ctx, wantedAttestation.PredicateType, va) + if err != nil { + return nil, errors.Wrap(err, "failed to convert attestation payload to json") + } + if attBytes == nil { + // This happens when we ask for a predicate type that this + // attestation is not for. It's not an error, so we skip it. + continue + } + if err := policy.EvaluatePolicyAgainstJSON(ctx, wantedAttestation.Name, wantedAttestation.Type, wantedAttestation.Data, attBytes); err != nil { + return nil, err } + // Ok, so this passed aok, jot it down to our result set as + // verified attestation with the predicate type match + ret[wantedAttestation.Name] = ociSignatureToPolicySignature(ctx, verifiedAttestations) } } - return nil, authorityErrors + return ret, nil } // ResolvePodSpecable implements duckv1.PodSpecValidator diff --git a/pkg/cosign/kubernetes/webhook/validator_result.go b/pkg/cosign/kubernetes/webhook/validator_result.go new file mode 100644 index 00000000000..16a7b549c69 --- /dev/null +++ b/pkg/cosign/kubernetes/webhook/validator_result.go @@ -0,0 +1,60 @@ +// +// 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 webhook + +// PolicyResult is the result of a successful ValidatePolicy call. +// These are meant to be consumed by a higher level Policy engine that +// can reason about validated results. The 'first' level pass will verify +// signatures and attestations, and make the results then available for +// a policy that can be used to gate a passing of a ClusterImagePolicy. +// Some examples are, at least 'vulnerability' has to have been done +// and the scan must have been attested by a particular entity (sujbect/issuer) +// or a particular key. +// Other examples are N-of-M must be satisfied and so forth. +// We do not expose the low level details of signatures / attestations here +// since they have already been validated as per the Authority configuration +// and optionally by the Attestations which contain a particular policy that +// can be used to validate the Attestations (say vulnerability scanner must not +// have any High sev issues). +type PolicyResult struct { + // AuthorityMatches will have an entry for each successful Authority check + // on it. Key in the map is the Attestation.Name + AuthorityMatches map[string]AuthorityMatch `json:"authorityMatches"` +} + +// AuthorityMatch returns either Signatures (if there are no Attestations +// specified), or Attestations if there are Attestations specified. +type AuthorityMatch struct { + // All of the matching signatures for this authority + // Wonder if for consistency this should also have the matching + // attestations name, aka, make this into a map. + Signatures []PolicySignature `json:"signatures"` + + // Mapping from attestation name to all of verified attestations + Attestations map[string][]PolicySignature `json:"attestations"` +} + +// PolicySignature contains a normalized result of a validated signature, where +// signature could be a signature on the Image (.sig) or on an Attestation +// (.att). +type PolicySignature struct { + // Subject that was found to match on the Cert. + Subject string `json:"subject"` + // Issure that was found to match on the Cert. + Issuer string `json:"issuer"` + // TODO(vaikas): Add all the Fulcio specific extensions here too. + // https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md +} diff --git a/pkg/cosign/kubernetes/webhook/validator_test.go b/pkg/cosign/kubernetes/webhook/validator_test.go index 6b1ca61fffc..79593ef39cd 100644 --- a/pkg/cosign/kubernetes/webhook/validator_test.go +++ b/pkg/cosign/kubernetes/webhook/validator_test.go @@ -23,8 +23,11 @@ import ( "crypto/elliptic" "crypto/x509" "errors" + "fmt" "net/http" "net/http/httptest" + "reflect" + "strings" "testing" "time" @@ -286,10 +289,10 @@ UoJou2P8sbDxpLiE/v3yLw1/jyOrCPWYHWFXnyyeGlkgSVefG54tNoK7Uw== want: func() *apis.FieldError { var errs *apis.FieldError fe := apis.ErrGeneric("failed policy: cluster-image-policy-keyless", "image").ViaFieldIndex("initContainers", 0) - fe.Details = digest.String() + fe.Details = fmt.Sprintf("%s %s", digest.String(), `fetching FulcioRoot: getting root cert: parse "http://http:%2F%2Fexample.com%2F/api/v1/rootCert": invalid port ":%2F%2Fexample.com%2F" after host`) errs = errs.Also(fe) fe2 := apis.ErrGeneric("failed policy: cluster-image-policy-keyless", "image").ViaFieldIndex("containers", 0) - fe2.Details = digest.String() + fe2.Details = fmt.Sprintf("%s %s", digest.String(), `fetching FulcioRoot: getting root cert: parse "http://http:%2F%2Fexample.com%2F/api/v1/rootCert": invalid port ":%2F%2Fexample.com%2F" after host`) errs = errs.Also(fe2) return errs }(), @@ -329,14 +332,47 @@ UoJou2P8sbDxpLiE/v3yLw1/jyOrCPWYHWFXnyyeGlkgSVefG54tNoK7Uw== want: func() *apis.FieldError { var errs *apis.FieldError fe := apis.ErrGeneric("failed policy: cluster-image-policy-keyless", "image").ViaFieldIndex("initContainers", 0) - fe.Details = digest.String() + fe.Details = fmt.Sprintf("%s validate signatures with fulcio: bad signature", digest.String()) errs = errs.Also(fe) fe2 := apis.ErrGeneric("failed policy: cluster-image-policy-keyless", "image").ViaFieldIndex("containers", 0) - fe2.Details = digest.String() + fe2.Details = fmt.Sprintf("%s validate signatures with fulcio: bad signature", digest.String()) errs = errs.Also(fe2) return errs }(), cvs: fail, + }, { + name: "simple, no error, authority keyless, good fulcio", + ps: &corev1.PodSpec{ + InitContainers: []corev1.Container{{ + Name: "setup-stuff", + Image: digest.String(), + }}, + Containers: []corev1.Container{{ + Name: "user-container", + Image: digest.String(), + }}, + }, + customContext: config.ToContext(context.Background(), + &config.Config{ + ImagePolicyConfig: &config.ImagePolicyConfig{ + Policies: map[string]webhookcip.ClusterImagePolicy{ + "cluster-image-policy-keyless": { + Images: []v1alpha1.ImagePattern{{ + Regex: ".*", + }}, + Authorities: []webhookcip.Authority{ + { + Keyless: &webhookcip.KeylessRef{ + URL: fulcioURL, + }, + }, + }, + }, + }, + }, + }, + ), + cvs: pass, }, { name: "simple, error, authority keyless, good fulcio, bad rekor", ps: &corev1.PodSpec{ @@ -375,10 +411,10 @@ UoJou2P8sbDxpLiE/v3yLw1/jyOrCPWYHWFXnyyeGlkgSVefG54tNoK7Uw== want: func() *apis.FieldError { var errs *apis.FieldError fe := apis.ErrGeneric("failed policy: cluster-image-policy-keyless", "image").ViaFieldIndex("initContainers", 0) - fe.Details = digest.String() + fe.Details = fmt.Sprintf("%s validate signatures with fulcio: bad signature", digest.String()) errs = errs.Also(fe) fe2 := apis.ErrGeneric("failed policy: cluster-image-policy-keyless", "image").ViaFieldIndex("containers", 0) - fe2.Details = digest.String() + fe2.Details = fmt.Sprintf("%s validate signatures with fulcio: bad signature", digest.String()) errs = errs.Also(fe2) return errs }(), @@ -1104,3 +1140,213 @@ UoJou2P8sbDxpLiE/v3yLw1/jyOrCPWYHWFXnyyeGlkgSVefG54tNoK7Uw== }) } } + +func TestValidatePolicy(t *testing.T) { + // Resolved via crane digest on 2021/09/25 + digest := name.MustParseReference("gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4") + + ctx, _ := rtesting.SetupFakeContext(t) + si := fakesecret.Get(ctx) + + secretName := "blah" + + // Non-existent URL for testing complete failure + badURL := apis.HTTP("http://example.com/") + t.Logf("badURL: %s", badURL.String()) + + // Spin up a Fulcio that responds with a Root Cert + fulcioServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.Write([]byte(fulcioRootCert)) + })) + t.Cleanup(fulcioServer.Close) + fulcioURL, err := apis.ParseURL(fulcioServer.URL) + if err != nil { + t.Fatalf("Failed to parse fake Fulcio URL") + } + t.Logf("fulcioURL: %s", fulcioURL.String()) + + rekorServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.Write([]byte(rekorResponse)) + })) + t.Cleanup(rekorServer.Close) + rekorURL, err := apis.ParseURL(rekorServer.URL) + if err != nil { + t.Fatalf("Failed to parse fake Rekor URL") + } + t.Logf("rekorURL: %s", rekorURL.String()) + var authorityKeyCosignPub *ecdsa.PublicKey + // Random public key (cosign generate-key-pair) 2022-03-18 + authorityKeyCosignPubString := `-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENAyijLvRu5QpCPp2uOj8C79ZW1VJ +SID/4H61ZiRzN4nqONzp+ZF22qQTk3MFO3D0/ZKmWHAosIf2pf2GHH7myA== +-----END PUBLIC KEY-----` + + pems := parsePems([]byte(authorityKeyCosignPubString)) + if len(pems) > 0 { + key, _ := x509.ParsePKIXPublicKey(pems[0].Bytes) + authorityKeyCosignPub = key.(*ecdsa.PublicKey) + } else { + t.Errorf("Error parsing authority key from string") + } + + si.Informer().GetIndexer().Add(&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: system.Namespace(), + Name: secretName, + }, + Data: map[string][]byte{ + // Random public key (cosign generate-key-pair) 2021-09-25 + "cosign.pub": []byte(`-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEapTW568kniCbL0OXBFIhuhOboeox +UoJou2P8sbDxpLiE/v3yLw1/jyOrCPWYHWFXnyyeGlkgSVefG54tNoK7Uw== +-----END PUBLIC KEY----- +`), + }, + }) + + cvs := cosignVerifySignatures + defer func() { + cosignVerifySignatures = cvs + }() + // Let's just say that everything is verified. + pass := func(_ context.Context, _ name.Reference, _ *cosign.CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { + sig, err := static.NewSignature(nil, "") + if err != nil { + return nil, false, err + } + return []oci.Signature{sig}, true, nil + } + // Let's just say that everything is not verified. + fail := func(_ context.Context, _ name.Reference, _ *cosign.CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { + return nil, false, errors.New("bad signature") + } + + // Let's say it is verified if it is the expected Public Key + authorityPublicKeyCVS := func(ctx context.Context, signedImgRef name.Reference, co *cosign.CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { + actualPublicKey, _ := co.SigVerifier.PublicKey() + actualECDSAPubkey := actualPublicKey.(*ecdsa.PublicKey) + actualKeyData := elliptic.Marshal(actualECDSAPubkey, actualECDSAPubkey.X, actualECDSAPubkey.Y) + + expectedKeyData := elliptic.Marshal(authorityKeyCosignPub, authorityKeyCosignPub.X, authorityKeyCosignPub.Y) + + if bytes.Equal(actualKeyData, expectedKeyData) { + return pass(ctx, signedImgRef, co) + } + + return fail(ctx, signedImgRef, co) + } + + tests := []struct { + name string + policy webhookcip.ClusterImagePolicy + want *PolicyResult + wantErrs []string + cva func(context.Context, name.Reference, *cosign.CheckOpts) ([]oci.Signature, bool, error) + cvs func(context.Context, name.Reference, *cosign.CheckOpts) ([]oci.Signature, bool, error) + customContext context.Context + }{{ + name: "simple, public key, no matches", + policy: webhookcip.ClusterImagePolicy{ + Authorities: []webhookcip.Authority{{ + Name: "authority-0", + Key: &webhookcip.KeyRef{ + PublicKeys: []crypto.PublicKey{authorityKeyCosignPub}, + }, + }}, + }, + wantErrs: []string{"failed to validate public keys with authority authority-0 for gcr.io/distroless/static@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4: bad signature"}, + cvs: fail, + }, { + name: "simple, public key, works", + policy: webhookcip.ClusterImagePolicy{ + Authorities: []webhookcip.Authority{{ + Name: "authority-0", + Key: &webhookcip.KeyRef{ + PublicKeys: []crypto.PublicKey{authorityKeyCosignPub}, + }, + }}, + }, + want: &PolicyResult{ + AuthorityMatches: map[string]AuthorityMatch{ + "authority-0": { + Signatures: []PolicySignature{{ + Subject: "PLACEHOLDER", + Issuer: "PLACEHOLDER"}}, + }}, + }, + cvs: pass, + }, { + name: "simple, public key, no error", + policy: webhookcip.ClusterImagePolicy{ + Authorities: []webhookcip.Authority{{ + Name: "authority-0", + Key: &webhookcip.KeyRef{ + PublicKeys: []crypto.PublicKey{authorityKeyCosignPub}, + }, + }}, + }, + want: &PolicyResult{ + AuthorityMatches: map[string]AuthorityMatch{ + "authority-0": { + Signatures: []PolicySignature{{ + Subject: "PLACEHOLDER", + Issuer: "PLACEHOLDER"}}, + }}, + }, + cvs: authorityPublicKeyCVS, + }, { + name: "simple, keyless attestation, works", + policy: webhookcip.ClusterImagePolicy{ + Authorities: []webhookcip.Authority{{ + Name: "authority-0", + Keyless: &webhookcip.KeylessRef{ + URL: fulcioURL, + }, + Attestations: []webhookcip.AttestationPolicy{{ + Name: "test-att", + PredicateType: "custom", + }}, + }, + }, + }, + want: &PolicyResult{ + AuthorityMatches: map[string]AuthorityMatch{ + "authority-0": { + Attestations: map[string][]PolicySignature{"test-att": {{ + Subject: "PLACEHOLDER", + Issuer: "PLACEHOLDER"}}, + }}}, + }, + cva: pass, + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cosignVerifySignatures = test.cvs + cosignVerifyAttestations = test.cva + testContext := context.Background() + + if test.customContext != nil { + testContext = test.customContext + } + got, gotErrs := ValidatePolicy(testContext, digest, test.policy) + validateErrors(t, test.wantErrs, gotErrs) + if !reflect.DeepEqual(test.want, got) { + t.Errorf("unexpected PolicyResult, want: %+v got: %+v", test.want, got) + } + }) + } +} + +func validateErrors(t *testing.T, wantErr []string, got []error) { + t.Helper() + if len(wantErr) != len(got) { + t.Errorf("Wanted %d errors got %d", len(wantErr), len(got)) + } else { + for i, want := range wantErr { + if !strings.Contains(got[i].Error(), want) { + t.Errorf("Unwanted error at %d want: %s got: %s", i, want, got[i]) + } + } + } +} diff --git a/pkg/policy/eval.go b/pkg/policy/eval.go new file mode 100644 index 00000000000..9ef6e82fd02 --- /dev/null +++ b/pkg/policy/eval.go @@ -0,0 +1,69 @@ +// +// 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 policy + +import ( + "context" + "fmt" + + "cuelang.org/go/cue/cuecontext" + cuejson "cuelang.org/go/encoding/json" + + "knative.dev/pkg/logging" +) + +// EvaluatePolicyAgainstJson is used to run a policy engine against JSON bytes. +// These bytes can be for example Attestations, or ClusterImagePolicy result +// types. +// name - which attestation are we evaluating +// policyType - cue|rego +// policyBody - String representing either cue or rego language +// jsonBytes - Bytes to evaluate against the policyBody in the given language +func EvaluatePolicyAgainstJSON(ctx context.Context, name, policyType string, policyBody string, jsonBytes []byte) error { + logging.FromContext(ctx).Debugf("Evaluating JSON: %s against policy: %s", string(jsonBytes), policyBody) + switch policyType { + case "cue": + cueValidationErr := evaluateCue(ctx, jsonBytes, policyBody) + if cueValidationErr != nil { + return fmt.Errorf("failed evaluating cue policy for %s : %s", name, cueValidationErr.Error()) // nolint + } + case "rego": + regoValidationErr := evaluateRego(ctx, jsonBytes, policyBody) + if regoValidationErr != nil { + return fmt.Errorf("failed evaluating rego policy for type %s", name) + } + default: + return fmt.Errorf("sorry Type %s is not supported yet", policyType) + } + return nil +} + +// evaluateCue evaluates a cue policy `evaluator` against `attestation` +func evaluateCue(ctx context.Context, attestation []byte, evaluator string) error { + logging.FromContext(ctx).Infof("Evaluating attestation: %s", string(attestation)) + cueCtx := cuecontext.New() + v := cueCtx.CompileString(evaluator) + return cuejson.Validate(attestation, v) +} + +// evaluateRego evaluates a rego policy `evaluator` against `attestation` +func evaluateRego(ctx context.Context, attestation []byte, evaluator string) error { + // TODO(vaikas) Fix this + // The existing stuff wants files, and it doesn't work. There must be + // a way to load it from a []byte like we can do with cue. Tomorrows problem + // regoValidationErrs := rego.ValidateJSON(payload, regoPolicies) + return fmt.Errorf("TODO(vaikas): Don't know how to this from bytes yet") +} diff --git a/pkg/policy/eval_test.go b/pkg/policy/eval_test.go new file mode 100644 index 00000000000..9c1a3cae74f --- /dev/null +++ b/pkg/policy/eval_test.go @@ -0,0 +1,143 @@ +// +// 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 policy + +import ( + "context" + "strings" + "testing" +) + +const ( + customAttestation = ` + { + "_type": "https://in-toto.io/Statement/v0.1", + "predicateType": "cosign.sigstore.dev/attestation/v1", + "subject": [ + { + "name": "registry.local:5000/cosigned/demo", + "digest": { + "sha256": "416cc82c76114b1744ea58bcbf2f411a0f2de4b0456703bf1bb83d33656951bc" + } + } + ], + "predicate": { + "Data": "foobar e2e test", + "Timestamp": "2022-04-20T18:17:19Z" + } + }` + + vulnAttestation = ` + { + "_type": "https://in-toto.io/Statement/v0.1", + "predicateType": "cosign.sigstore.dev/attestation/vuln/v1", + "subject": [ + { + "name": "registry.local:5000/cosigned/demo", + "digest": { + "sha256": "416cc82c76114b1744ea58bcbf2f411a0f2de4b0456703bf1bb83d33656951bc" + } + } + ], + "predicate": { + "invocation": { + "parameters": null, + "uri": "invocation.example.com/cosign-testing", + "event_id": "", + "builder.id": "" + }, + "scanner": { + "uri": "fakescanner.example.com/cosign-testing", + "version": "", + "db": { + "uri": "", + "version": "" + }, + "result": null + }, + "metadata": { + "scanStartedOn": "2022-04-12T00:00:00Z", + "scanFinishedOn": "2022-04-12T00:10:00Z" + } + } + }` + + // TODO(vaikas): Enable tests once we sort this out. + // cipAttestation = "authorityMatches:{\"key-att\":{\"signatures\":null,\"attestations\":{\"custom-match-predicate\":[{\"subject\":\"PLACEHOLDER\",\"issuer\":\"PLACEHOLDER\"}]}},\"key-signature\":{\"signatures\":[{\"subject\":\"PLACEHOLDER\",\"issuer\":\"PLACEHOLDER\"}],\"attestations\":null},\"keyless-att\":{\"signatures\":null,\"attestations\":{\"custom-keyless\":[{\"subject\":\"PLACEHOLDER\",\"issuer\":\"PLACEHOLDER\"}]}},\"keyless-signature\":{\"signatures\":[{\"subject\":\"PLACEHOLDER\",\"issuer\":\"PLACEHOLDER\"}],\"attestations\":null}}" +) + +func TestEvalPolicy(t *testing.T) { + // TODO(vaikas): Consider moving the attestations/cue files into testdata + // directory. + tests := []struct { + name string + json string + policyType string + policyFile string + wantErr bool + wantErrSub string + }{{ + name: "custom attestation, mismatched predicateType", + json: customAttestation, + policyType: "cue", + policyFile: `predicateType: "cosign.sigstore.dev/attestation/vuln/v1"`, + wantErr: true, + wantErrSub: `conflicting values "cosign.sigstore.dev/attestation/v1" and "cosign.sigstore.dev/attestation/vuln/v1"`, + }, { + name: "custom attestation, predicateType and data checks out", + json: customAttestation, + policyType: "cue", + policyFile: `predicateType: "cosign.sigstore.dev/attestation/v1" + predicate: Data: "foobar e2e test"`, + }, { + name: "custom attestation, data mismatch", + json: customAttestation, + policyType: "cue", + policyFile: `predicateType: "cosign.sigstore.dev/attestation/v1" + predicate: Data: "invalid data here"`, + wantErr: true, + wantErrSub: `predicate.Data: conflicting values "foobar e2e test" and "invalid data here"`, + }, { + name: "vuln attestation, wrong invocation url", + json: vulnAttestation, + policyType: "cue", + policyFile: `predicateType: "cosign.sigstore.dev/attestation/vuln/v1" + predicate: invocation: uri: "invocation.example.com/wrong-url-here"`, + wantErr: true, + wantErrSub: `conflicting values "invocation.example.com/cosign-testing" and "invocation.example.com/wrong-url-here"`, + }, { + name: "vuln attestation, checks out", + json: vulnAttestation, + policyType: "cue", + policyFile: `predicateType: "cosign.sigstore.dev/attestation/vuln/v1" + predicate: invocation: uri: "invocation.example.com/cosign-testing"`, + }} + for _, tc := range tests { + ctx := context.Background() + err := EvaluatePolicyAgainstJSON(ctx, tc.name, tc.policyType, tc.policyFile, []byte(tc.json)) + if tc.wantErr { + if err == nil { + t.Errorf("Did not get an error, wanted %s", tc.wantErrSub) + } else if !strings.Contains(err.Error(), tc.wantErrSub) { + t.Errorf("Unexpected error, want: %s got: %s", tc.wantErrSub, err.Error()) + } + } else { + if !tc.wantErr && err != nil { + t.Errorf("Unexpected error, wanted none, got: %s", err.Error()) + } + } + } +} diff --git a/pkg/reconciler/clusterimagepolicy/clusterimagepolicy_test.go b/pkg/reconciler/clusterimagepolicy/clusterimagepolicy_test.go index 1220f8304bc..ee6e667be52 100644 --- a/pkg/reconciler/clusterimagepolicy/clusterimagepolicy_test.go +++ b/pkg/reconciler/clusterimagepolicy/clusterimagepolicy_test.go @@ -64,10 +64,10 @@ RCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ== -----END PUBLIC KEY-----` // This is the patch for replacing a single entry in the ConfigMap - replaceCIPPatch = `[{"op":"replace","path":"/data/test-cip","value":"{\"images\":[{\"glob\":\"ghcr.io/example/*\"}],\"authorities\":[{\"key\":{\"data\":\"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\\n-----END PUBLIC KEY-----\"}}]}"}]` + replaceCIPPatch = `[{"op":"replace","path":"/data/test-cip","value":"{\"images\":[{\"glob\":\"ghcr.io/example/*\"}],\"authorities\":[{\"name\":\"authority-0\",\"key\":{\"data\":\"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\\n-----END PUBLIC KEY-----\"}}]}"}]` // This is the patch for adding an entry for non-existing KMS for cipName2 - addCIP2Patch = `[{"op":"add","path":"/data/test-cip-2","value":"{\"images\":[{\"glob\":\"ghcr.io/example/*\"}],\"authorities\":[{\"key\":{\"data\":\"azure-kms://foo/bar\"}}]}"}]` + addCIP2Patch = `[{"op":"add","path":"/data/test-cip-2","value":"{\"images\":[{\"glob\":\"ghcr.io/example/*\"}],\"authorities\":[{\"name\":\"authority-0\",\"key\":{\"data\":\"azure-kms://foo/bar\"}}]}"}]` // This is the patch for removing the last entry, leaving just the // configmap objectmeta, no data. @@ -82,7 +82,7 @@ RCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ== removeSingleEntryKeylessPatch = `[{"op":"remove","path":"/data/test-cip-2"}]` // This is the patch for inlined secret for keyless cakey ref data - inlinedSecretKeylessPatch = `[{"op":"replace","path":"/data/test-cip-2","value":"{\"images\":[{\"glob\":\"ghcr.io/example/*\"}],\"authorities\":[{\"keyless\":{\"ca-cert\":{\"data\":\"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\\n-----END PUBLIC KEY-----\"}}}]}"}]` + inlinedSecretKeylessPatch = `[{"op":"replace","path":"/data/test-cip-2","value":"{\"images\":[{\"glob\":\"ghcr.io/example/*\"}],\"authorities\":[{\"name\":\"authority-0\",\"keyless\":{\"ca-cert\":{\"data\":\"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\\n-----END PUBLIC KEY-----\"}}}]}"}]` ) func TestReconcile(t *testing.T) { @@ -576,7 +576,7 @@ func makeConfigMap() *corev1.ConfigMap { Name: config.ImagePoliciesConfigName, }, Data: map[string]string{ - cipName: `{"images":[{"glob":"ghcr.io/example/*"}],"authorities":[{"key":{"data":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\n-----END PUBLIC KEY-----"}}]}`, + cipName: `{"images":[{"glob":"ghcr.io/example/*"}],"authorities":[{"name":"authority-0","key":{"data":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\n-----END PUBLIC KEY-----"}}]}`, }, } } @@ -587,7 +587,7 @@ func patchKMS(ctx context.Context, t *testing.T, kmsKey string) clientgotesting. t.Fatalf("Failed to read KMS key ID %q: %v", kmsKey, err) } - patch := `[{"op":"add","path":"/data","value":{"test-kms-cip":"{\"images\":[{\"glob\":\"ghcr.io/example/*\"}],\"authorities\":[{\"key\":{\"data\":\"` + strings.ReplaceAll(pubKey, "\n", "\\\\n") + `\"}}]}"}}]` + patch := `[{"op":"add","path":"/data","value":{"test-kms-cip":"{\"images\":[{\"glob\":\"ghcr.io/example/*\"}],\"authorities\":[{\"name\":\"authority-0\",\"key\":{\"data\":\"` + strings.ReplaceAll(pubKey, "\n", "\\\\n") + `\"}}]}"}}]` return clientgotesting.PatchActionImpl{ ActionImpl: clientgotesting.ActionImpl{ @@ -606,7 +606,7 @@ func makeDifferentConfigMap() *corev1.ConfigMap { Name: config.ImagePoliciesConfigName, }, Data: map[string]string{ - cipName: `{"images":[{"glob":"ghcr.io/example/*"}],"authorities":[{"key":{"data":"-----BEGIN NOTPUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\n-----END NOTPUBLIC KEY-----"}}]}`, + cipName: `{"images":[{"glob":"ghcr.io/example/*"}],"authorities":[{"name":"authority-0","key":{"data":"-----BEGIN NOTPUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\n-----END NOTPUBLIC KEY-----"}}]}`, }, } } @@ -619,7 +619,7 @@ func makeConfigMapWithTwoEntries() *corev1.ConfigMap { Name: config.ImagePoliciesConfigName, }, Data: map[string]string{ - cipName: `{"images":[{"glob":"ghcr.io/example/*"}],"authorities":[{"key":{"data":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\n-----END PUBLIC KEY-----"}}]}`, + cipName: `{"images":[{"glob":"ghcr.io/example/*"}],"authorities":[{"name":"authority-0","key":{"data":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\n-----END PUBLIC KEY-----"}}]}`, cipName2: "remove me please", }, } diff --git a/test/e2e_test_cluster_image_policy.sh b/test/e2e_test_cluster_image_policy.sh index 45eb1712bb5..6753758526d 100755 --- a/test/e2e_test_cluster_image_policy.sh +++ b/test/e2e_test_cluster_image_policy.sh @@ -138,14 +138,14 @@ echo '::endgroup::' # This has correct issuer/subject, so should work echo '::group:: test job success with identities' if ! kubectl create -n demo-keyless-signing job demo-identities-works --image=${demoimage} ; then - echo Failed to create Job in namespace without label! + echo Failed to create Job in namespace with matching issuer/subject! exit 1 else - echo Succcessfully created Job with signed image + echo Succcessfully created Job with signed image keyless fi echo '::endgroup::' -echo '::group:: Add cip with identities that match issuer/subject' +echo '::group:: Add cip with identities that do not match issuer/subject' kubectl apply -f ./test/testdata/cosigned/e2e/cip-keyless-with-identities-mismatch.yaml # make sure the reconciler has enough time to update the configmap sleep 5 @@ -160,8 +160,8 @@ else fi echo '::endgroup::' -echo '::group:: Remove mismatching cip' -kubectl delete cip image-policy-keyless-with-identities-mismatch +echo '::group:: Remove mismatching cip, start fresh for key' +kubectl delete cip --all sleep 5 echo '::endgroup::' @@ -189,17 +189,17 @@ fi echo '::endgroup::' echo '::group:: Sign demoimage with cosign key' -COSIGN_PASSWORD="" ./cosign sign --key cosign-colocated-signing.key --force --allow-insecure-registry ${demoimage} +COSIGN_PASSWORD="" ./cosign sign --key cosign-colocated-signing.key --force --allow-insecure-registry --rekor-url ${REKOR_URL} ${demoimage} echo '::endgroup::' echo '::group:: Verify demoimage with cosign key' -./cosign verify --key cosign-colocated-signing.pub --allow-insecure-registry ${demoimage} +./cosign verify --key cosign-colocated-signing.pub --allow-insecure-registry --rekor-url ${REKOR_URL} ${demoimage} echo '::endgroup::' echo '::group:: test job success' # We signed this above, this should work if ! kubectl create -n demo-key-signing job demo --image=${demoimage} ; then - echo Failed to create Job in namespace without label! + echo Failed to create Job in namespace after signing with key! exit 1 else echo Succcessfully created Job with signed image @@ -230,7 +230,7 @@ yq '. | .metadata.name = "image-policy-remote-source" echo '::endgroup::' echo '::group:: Sign demoimage with cosign remote key' -COSIGN_REPOSITORY="${KO_DOCKER_REPO}/remote-signature" ./cosign sign --key cosign-remote-signing.key --force --allow-insecure-registry ${demoimage} +COSIGN_PASSWORD="" COSIGN_REPOSITORY="${KO_DOCKER_REPO}/remote-signature" ./cosign sign --key cosign-remote-signing.key --force --allow-insecure-registry ${demoimage} echo '::endgroup::' echo '::group:: Verify demoimage with cosign remote key' @@ -277,5 +277,6 @@ echo '::endgroup::' echo '::group::' Cleanup kubectl delete cip --all -kubectl delete ns demo-key-signing demo-keyless-signing +kubectl delete ns demo-key-signing demo-keyless-signing demo-key-remote rm cosign*.key cosign*.pub +echo '::endgroup::' diff --git a/test/e2e_test_cluster_image_policy_with_attestations.sh b/test/e2e_test_cluster_image_policy_with_attestations.sh new file mode 100755 index 00000000000..e16f535fcbd --- /dev/null +++ b/test/e2e_test_cluster_image_policy_with_attestations.sh @@ -0,0 +1,243 @@ +#!/usr/bin/env bash +# +# 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. + + +set -ex + +if [[ -z "${OIDC_TOKEN}" ]]; then + if [[ -z "${TOKEN_ISSUER}" ]]; then + echo "Must specify either env variable OIDC_TOKEN or TOKEN_ISSUER" + exit 1 + else + export OIDC_TOKEN=`curl -s ${ISSUER_URL}` + fi +fi + +if [[ -z "${KO_DOCKER_REPO}" ]]; then + echo "Must specify env variable KO_DOCKER_REPO" + exit 1 +fi + +if [[ -z "${FULCIO_URL}" ]]; then + echo "Must specify env variable FULCIO_URL" + exit 1 +fi + +if [[ -z "${REKOR_URL}" ]]; then + echo "Must specify env variable REKOR_URL" + exit 1 +fi + +if [[ -z "${SIGSTORE_CT_LOG_PUBLIC_KEY_FILE}" ]]; then + echo "must specify env variable SIGSTORE_CT_LOG_PUBLIC_KEY_FILE" + exit 1 +fi + +if [[ "${NON_REPRODUCIBLE}"=="1" ]]; then + echo "creating non-reproducible build by adding a timestamp" + export TIMESTAMP=`date +%s` +else + export TIMESTAMP="TIMESTAMP" +fi + +# Trust our own custom Rekor API +export SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY=1 + +# To simplify testing failures, use this function to execute a kubectl to create +# our job and verify that the failure is expected. +assert_error() { + local KUBECTL_OUT_FILE="/tmp/kubectl.failure.out" + match="$@" + echo looking for ${match} + if kubectl create -n ${NS} job demo --image=${demoimage} 2> ${KUBECTL_OUT_FILE} ; then + echo Failed to block unsigned Job creation! + exit 1 + else + echo Successfully blocked Job creation with expected error: "${match}" + if ! grep -q "${match}" ${KUBECTL_OUT_FILE} ; then + echo Did not get expected failure message, wanted "${match}", got + cat ${KUBECTL_OUT_FILE} + exit 1 + fi + fi +} + +# Publish test image +echo '::group:: publish test image demoimage' +pushd $(mktemp -d) +go mod init example.com/demo +cat < main.go +package main +import "fmt" +func main() { + fmt.Println("hello world TIMESTAMP") +} +EOF + +sed -i'' -e "s@TIMESTAMP@${TIMESTAMP}@g" main.go +cat main.go +export demoimage=`ko publish -B example.com/demo` +echo Created image $demoimage +popd +echo '::endgroup::' + +echo '::group:: Create and label new namespace for verification' +kubectl create namespace demo-attestations +kubectl label namespace demo-attestations cosigned.sigstore.dev/include=true +export NS=demo-attestations +echo '::endgroup::' + +echo '::group:: Create CIP that requires keyless signature and custom attestation with policy' +kubectl apply -f ./test/testdata/cosigned/e2e/cip-keyless-with-attestations.yaml +# allow things to propagate +sleep 5 +echo '::endgroup::' + +# This image has not been signed at all, so should get auto-reject +echo '::group:: test job rejection' +expected_error='no matching signatures' +assert_error ${expected_error} +echo '::endgroup::' + +echo '::group:: Sign demoimage with keyless' +COSIGN_EXPERIMENTAL=1 ./cosign sign --rekor-url ${REKOR_URL} --fulcio-url ${FULCIO_URL} --force --allow-insecure-registry ${demoimage} --identity-token ${OIDC_TOKEN} +echo '::endgroup::' + +# This image has been signed, but does not have an attestation, so should fail. +echo '::group:: test job rejection' +expected_error='no matching attestations' +assert_error ${expected_error} +echo '::endgroup::' + +# Ok, cool. So attest and it should pass. +echo '::group:: Create one keyless attestation and verify it' +echo -n 'foobar e2e test' > ./predicate-file-custom +COSIGN_EXPERIMENTAL=1 ./cosign attest --predicate ./predicate-file-custom --fulcio-url ${FULCIO_URL} --rekor-url ${REKOR_URL} --allow-insecure-registry --force ${demoimage} --identity-token ${OIDC_TOKEN} + +COSIGN_EXPERIMENTAL=1 ./cosign verify-attestation --type=custom --rekor-url ${REKOR_URL} --allow-insecure-registry ${demoimage} +echo '::endgroup::' + +echo '::group:: test job success' +# We signed this with keyless and it has a keyless attestation, so should +# pass. +export KUBECTL_SUCCESS_FILE="/tmp/kubectl.success.out" +if ! kubectl create -n ${NS} job demo --image=${demoimage} 2> ${KUBECTL_SUCCESS_FILE} ; then + echo Failed to create job with keyless signature and an attestation + cat ${KUBECTL_SUCCESS_FILE} + exit 1 +else + echo Created the job with keyless signature and an attestation +fi +echo '::endgroup::' + +echo '::group:: Generate New Signing Key that we use for key-ful signing' +COSIGN_PASSWORD="" ./cosign generate-key-pair +echo '::endgroup::' + +# Ok, so now we have satisfied the keyless requirements, one signature, one +# custom attestation. Let's now do it for 'keyful' one. +echo '::group:: Create CIP that requires a keyful signature and an attestation' +yq '. | .spec.authorities[0].key.data |= load_str("cosign.pub") | .spec.authorities[1].key.data |= load_str("cosign.pub")' ./test/testdata/cosigned/e2e/cip-key-with-attestations.yaml | kubectl apply -f - +# allow things to propagate +sleep 5 +echo '::endgroup::' + +# This image has been signed with keyless, but does not have a keyful signature +# so should fail +echo '::group:: test job rejection' +expected_error='no matching signatures' +assert_error ${expected_error} +echo '::endgroup::' + +# Sign it with key +echo '::group:: Sign demoimage with key, and add to rekor' +COSIGN_EXPERIMENTAL=1 COSIGN_PASSWORD="" ./cosign sign --key cosign.key --force --allow-insecure-registry --rekor-url ${REKOR_URL} ${demoimage} +echo '::endgroup::' + +echo '::group:: Verify demoimage with cosign key' +COSIGN_EXPERIMENTAL=1 ./cosign verify --key cosign.pub --rekor-url ${REKOR_URL} --allow-insecure-registry ${demoimage} +echo '::endgroup::' + +# This image has been signed with key, but does not have a key attestation +# so should fail +echo '::group:: test job rejection' +expected_error='no matching attestations' +assert_error ${expected_error} +echo '::endgroup::' + +# Fine, so create an attestation for it that's different from the keyless one +echo '::group:: create keyful attestation, add add to rekor' +echo -n 'foobar key e2e test' > ./predicate-file-key-custom +COSIGN_EXPERIMENTAL=1 COSIGN_PASSWORD="" ./cosign attest --predicate ./predicate-file-key-custom --rekor-url ${REKOR_URL} --key ./cosign.key --allow-insecure-registry --force ${demoimage} + +COSIGN_EXPERIMENTAL=1 ./cosign verify-attestation --key ./cosign.pub --allow-insecure-registry --rekor-url ${REKOR_URL} ${demoimage} +echo '::endgroup::' + +echo '::group:: test job success with key / keyless' +# We signed this with keyless and key and it has a key/keyless attestation, so +# should pass. +if ! kubectl create -n ${NS} job demo2 --image=${demoimage} 2> ${KUBECTL_SUCCESS_FILE} ; then + echo Failed to create job with both key/keyless signatures and attestations + cat ${KUBECTL_SUCCESS_FILE} + exit 1 +else + echo Created the job with keyless/key signature and an attestations +fi +echo '::endgroup::' + +# So at this point, we have two CIP, one that requires keyless/key sig +# and attestations with both. Let's take it up a notch. +# Let's create a policy that requires both a keyless and keyful +# signature on the image, as well as two attestations signed by the keyless and +# one custom attestation that's signed by key. +# Note we have to bake in the inline data from the keys above +echo '::group:: Add cip for two signatures and two attestations' +yq '. | .spec.authorities[1].key.data |= load_str("cosign.pub") | .spec.authorities[3].key.data |= load_str("cosign.pub")' ./test/testdata/cosigned/e2e/cip-requires-two-signatures-and-two-attestations.yaml | kubectl apply -f - +echo '::endgroup::' + +# TODO(vaikas): Enable the remaining tests once we sort out how to write +# a valid CUE policy, or once #1787 goes in try implementing a Rego one. +echo 'Not testing the CIP policy evaluation yet' +exit 0 + +# The CIP policy is the one that should fail now because it doesn't have enough +# attestations +echo '::group:: test job rejection' +expected_error='no matching attestations' +assert_error ${expected_error} +echo '::endgroup::' + +echo '::group:: Create vuln keyless attestation and verify it' +COSIGN_EXPERIMENTAL=1 ./cosign attest --predicate ./test/testdata/attestations/vuln-predicate.json --type=vuln --fulcio-url ${FULCIO_URL} --rekor-url ${REKOR_URL} --allow-insecure-registry --force ${demoimage} --identity-token ${OIDC_TOKEN} + +COSIGN_EXPERIMENTAL=1 ./cosign verify-attestation --type=vuln --rekor-url ${REKOR_URL} --allow-insecure-registry ${demoimage} +echo '::endgroup::' + +echo '::group:: test job success' +# We signed this with key and keyless and it has two keyless attestations and +# it has one key attestation, so it should succeed. +if ! kubectl create -n ${NS} job demo3 --image=${demoimage} 2> ./${KUBECTL_OUT_FILE} ; then + echo Failed to create job that has two signatures and 3 attestations + cat ${KUBECTL_OUT_FILE} + exit 1 +fi +echo '::endgroup::' + +echo '::group::' Cleanup +kubectl delete cip --all +kubectl delete ns demo-attestations +rm cosign.key cosign.pub +echo '::endgroup::' diff --git a/test/testdata/cosigned/e2e/cip-key-with-attestations.yaml b/test/testdata/cosigned/e2e/cip-key-with-attestations.yaml new file mode 100644 index 00000000000..089dade05ab --- /dev/null +++ b/test/testdata/cosigned/e2e/cip-key-with-attestations.yaml @@ -0,0 +1,48 @@ +# 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 +# +# https://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. + +apiVersion: cosigned.sigstore.dev/v1alpha1 +kind: ClusterImagePolicy +metadata: + name: image-policy-key-with-attestations +spec: + images: + - glob: registry.local:5000/cosigned/demo* + authorities: + - name: verify custom attestation + key: + data: | + -----BEGIN PUBLIC KEY----- + MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZxAfzrQG1EbWyCI8LiSB7YgSFXoI + FNGTyQGKHFc6/H8TQumT9VLS78pUwtv3w7EfKoyFZoP32KrO7nzUy2q6Cw== + -----END PUBLIC KEY----- + ctlog: + url: http://rekor.rekor-system.svc + attestations: + - name: custom-match-predicate + predicateType: custom + policy: + type: cue + data: | + predicateType: "cosign.sigstore.dev/attestation/v1" + predicate: Data: "foobar key e2e test" + - name: verify signature + key: + data: | + -----BEGIN PUBLIC KEY----- + MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZxAfzrQG1EbWyCI8LiSB7YgSFXoI + FNGTyQGKHFc6/H8TQumT9VLS78pUwtv3w7EfKoyFZoP32KrO7nzUy2q6Cw== + -----END PUBLIC KEY----- + ctlog: + url: http://rekor.rekor-system.svc diff --git a/test/testdata/cosigned/e2e/cip-key.yaml b/test/testdata/cosigned/e2e/cip-key.yaml index d4d8334905d..7b7784bacdf 100644 --- a/test/testdata/cosigned/e2e/cip-key.yaml +++ b/test/testdata/cosigned/e2e/cip-key.yaml @@ -26,4 +26,5 @@ spec: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZxAfzrQG1EbWyCI8LiSB7YgSFXoI FNGTyQGKHFc6/H8TQumT9VLS78pUwtv3w7EfKoyFZoP32KrO7nzUy2q6Cw== -----END PUBLIC KEY----- - + ctlog: + url: http://rekor.rekor-system.svc diff --git a/test/testdata/cosigned/e2e/cip-keyless-with-attestations.yaml b/test/testdata/cosigned/e2e/cip-keyless-with-attestations.yaml new file mode 100644 index 00000000000..77999559fdd --- /dev/null +++ b/test/testdata/cosigned/e2e/cip-keyless-with-attestations.yaml @@ -0,0 +1,40 @@ +# 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 +# +# https://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. + +apiVersion: cosigned.sigstore.dev/v1alpha1 +kind: ClusterImagePolicy +metadata: + name: image-policy-keyless-with-attestations +spec: + images: + - glob: registry.local:5000/cosigned/demo* + authorities: + - name: verify custom attestation + keyless: + url: http://fulcio.fulcio-system.svc + ctlog: + url: http://rekor.rekor-system.svc + attestations: + - name: custom-match-predicate + predicateType: custom + policy: + type: cue + data: | + predicateType: "cosign.sigstore.dev/attestation/v1" + predicate: Data: "foobar e2e test" + - name: verify signature + keyless: + url: http://fulcio.fulcio-system.svc + ctlog: + url: http://rekor.rekor-system.svc diff --git a/test/testdata/cosigned/e2e/cip-requires-two-signatures-and-two-attestations.yaml b/test/testdata/cosigned/e2e/cip-requires-two-signatures-and-two-attestations.yaml new file mode 100644 index 00000000000..80b44ece51a --- /dev/null +++ b/test/testdata/cosigned/e2e/cip-requires-two-signatures-and-two-attestations.yaml @@ -0,0 +1,142 @@ +# 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 +# +# https://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. + +apiVersion: cosigned.sigstore.dev/v1alpha1 +kind: ClusterImagePolicy +metadata: + name: image-policy-requires-two-signatures-two-attestations +spec: + images: + - glob: registry.local:5000/cosigned/demo* + authorities: + - name: keyless-att + keyless: + url: http://fulcio.fulcio-system.svc + ctlog: + url: http://rekor.rekor-system.svc + attestations: + - predicateType: custom + name: custom-keyless + policy: + type: cue + data: | + import "time" + before: time.Parse(time.RFC3339, "2049-10-09T17:10:27Z") + predicateType: "cosign.sigstore.dev/attestation/v1" + predicate: { + Data: "foobar e2e test" + Timestamp: after + scanFinishedOn: after + } + } + - name: key-att + key: + data: | + -----BEGIN PUBLIC KEY----- + MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOz9FcbJM/oOkC26Wfo9paG2tYGBL + usDLHze93DzgLaAPDsyJrygpVnL9M6SOyfyXEsjpBTUu6uFZqHua8hwAlA== + -----END PUBLIC KEY----- + ctlog: + url: http://rekor.rekor-system.svc + attestations: + - name: custom-match-predicate + predicateType: custom + policy: + type: cue + data: | + predicateType: "cosign.sigstore.dev/attestation/v1" + predicate: Data: "foobar key e2e test" + - name: keyless-signature + keyless: + url: http://fulcio.fulcio-system.svc + ctlog: + url: http://rekor.rekor-system.svc + - name: key-signature + key: + data: | + -----BEGIN PUBLIC KEY----- + MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOz9FcbJM/oOkC26Wfo9paG2tYGBL + usDLHze93DzgLaAPDsyJrygpVnL9M6SOyfyXEsjpBTUu6uFZqHua8hwAlA== + -----END PUBLIC KEY----- + ctlog: + url: http://rekor.rekor-system.svc + policy: + type: cue + data: | + if len(authorityMatches."keyless-att".attestations) < 2 { + keylessAttestationsErr: "error" + keylessAttestationsErr: "Did not get both keyless attestations" + } + if len(authorityMatches."key-att".attestations) < 1 { + keyAttestationsErr: 1 + keyAttestationsErr: "Did not get key attestation" + } + if len(authorityMatches."keyless-signature".signatures) < 1 { + keylessSignatureErr: 1 + keylessSignatureErr: "Did not get keyless signature" + } + if len(authorityMatches."key-signature".signatures) < 1 { + keySignatureErr: 1 + keySignatureErr: "Did not get key signature" + } + authorityMatches: { + key-att: { + attestations: { + "vuln-key": [ + {subject: "PLACEHOLDER", issuer: "PLACEHOLDER"}, + ] + } + } + keyless-att: { + attestations: { + "vuln-keyless": [ + {subject: "PLACEHOLDER", issuer: "PLACEHOLDER"}, + ], + "custom-keyless": [ + {subject: "PLACEHOLDER", issuer: "PLACEHOLDER"}, + ], + } + } + keyless-signature: { + signatures: [ + {subject: "PLACEHOLDER", issuer: "PLACEHOLDER"}, + ] + } + key-signature: { + signatures: [ + {subject: "PLACEHOLDER", issuer: "PLACEHOLDER"}, + ] + } + } diff --git a/test/testdata/cosigned/valid/valid-policy-regex.yaml b/test/testdata/cosigned/valid/valid-policy-regex.yaml index 804ff4127ec..26f46c05391 100644 --- a/test/testdata/cosigned/valid/valid-policy-regex.yaml +++ b/test/testdata/cosigned/valid/valid-policy-regex.yaml @@ -26,10 +26,51 @@ spec: secretRef: name: ca-cert-secret namespace: some-namespacemak - - keyless: + - name: "keyless signatures" + keyless: + identities: + - issuer: "issue-details" + subject: "subject-details" + - name: "keyless attestations" + keyless: identities: - issuer: "issue-details" subject: "subject-details" + attestations: + - name: custom-predicate-type-validation + predicateType: custom + policy: + type: cue + data: | + import "time" + before: time.Parse(time.RFC3339, "2049-10-09T17:10:27Z") + predicateType: "cosign.sigstore.dev/attestation/v1" + predicate: { + Timestamp: after + scanFinishedOn: after + } + } - keyless: identities: - issuer: "issue-details1" From 8cac6458a1d007dff94914fe82cd9ca680be41dd Mon Sep 17 00:00:00 2001 From: Hector Fernandez Date: Sat, 23 Apr 2022 21:13:46 +0300 Subject: [PATCH 39/53] chore: add rego function to consume modules (#1787) Signed-off-by: hectorj2f --- pkg/cosign/rego/rego.go | 46 ++++++++++++++ pkg/cosign/rego/rego_test.go | 115 +++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+) diff --git a/pkg/cosign/rego/rego.go b/pkg/cosign/rego/rego.go index f3def4dcc76..2f4a6e0513a 100644 --- a/pkg/cosign/rego/rego.go +++ b/pkg/cosign/rego/rego.go @@ -22,6 +22,7 @@ import ( "fmt" "github.com/open-policy-agent/opa/rego" + "knative.dev/pkg/logging" ) // The query below should meet the following requirements: @@ -29,6 +30,12 @@ import ( // * Queries for a single value. const QUERY = "data.signature.allow" +// CosignRegoPackageName defines the expected package name of a provided rego module +const CosignRegoPackageName = "sigstore" + +// CosignEvaluationRule defines the expected evaluation role of a provided rego module +const CosignEvaluationRule = "isCompliant" + func ValidateJSON(jsonBody []byte, entrypoints []string) []error { ctx := context.Background() @@ -73,3 +80,42 @@ func ValidateJSON(jsonBody []byte, entrypoints []string) []error { } return errs } + +// ValidateJSONWithModuleInput takes the body of the results to evaluate and the defined module +// in a policy to validate against the input data +func ValidateJSONWithModuleInput(jsonBody []byte, moduleInput string) error { + ctx := context.Background() + query := fmt.Sprintf("%s = data.%s.%s", CosignEvaluationRule, CosignRegoPackageName, CosignEvaluationRule) + module := fmt.Sprintf("%s.rego", CosignRegoPackageName) + + r := rego.New( + rego.Query(query), + rego.Module(module, moduleInput)) + + evalQuery, err := r.PrepareForEval(ctx) + if err != nil { + return err + } + + var input interface{} + dec := json.NewDecoder(bytes.NewBuffer(jsonBody)) + dec.UseNumber() + if err := dec.Decode(&input); err != nil { + return err + } + + rs, err := evalQuery.Eval(ctx, rego.EvalInput(input)) + if err != nil { + return err + } + + for _, result := range rs { + isCompliant, ok := result.Bindings[CosignEvaluationRule].(bool) + if ok && isCompliant { + logging.FromContext(ctx).Info("Validated policy is compliant") + return nil + } + } + + return fmt.Errorf("policy is not compliant for query '%s'", query) +} diff --git a/pkg/cosign/rego/rego_test.go b/pkg/cosign/rego/rego_test.go index 875249594a1..a1fb4e3541a 100644 --- a/pkg/cosign/rego/rego_test.go +++ b/pkg/cosign/rego/rego_test.go @@ -98,3 +98,118 @@ func TestValidationJSON(t *testing.T) { }) } } + +const attestationsJSONBody = `{ + "authorityMatches": { + "keyatt": { + "signatures": null, + "attestations": { + "vuln-key": [ + { + "subject": "PLACEHOLDER", + "issuer": "PLACEHOLDER" + } + ] + } + }, + "keysignature": { + "signatures": [ + { + "subject": "PLACEHOLDER", + "issuer": "PLACEHOLDER" + } + ], + "attestations": null + }, + "keylessatt": { + "signatures": null, + "attestations": { + "custom-keyless": [ + { + "subject": "PLACEHOLDER", + "issuer": "PLACEHOLDER" + } + ] + } + }, + "keylesssignature": { + "signatures": [ + { + "subject": "PLACEHOLDER", + "issuer": "PLACEHOLDER" + } + ], + "attestations": null + } + } + }` + +func TestValidateJSONWithModuleInput(t *testing.T) { + cases := []struct { + name string + jsonBody string + policy string + pass bool + errorMsg string + }{ + { + name: "passing policy attestations", + jsonBody: attestationsJSONBody, + policy: ` + package sigstore + default isCompliant = false + isCompliant { + attestationsKeylessATT := input.authorityMatches.keylessatt.attestations + count(attestationsKeylessATT) == 1 + + attestationsKeyATT := input.authorityMatches.keyatt.attestations + count(attestationsKeyATT) == 1 + + keylessSignature := input.authorityMatches.keylesssignature.signatures + count(keylessSignature) == 1 + + keySignature := input.authorityMatches.keysignature.signatures + count(keySignature) == 1 + } + `, + pass: true, + }, + { + name: "not passing policy attestations", + jsonBody: attestationsJSONBody, + policy: ` + package sigstore + + default isCompliant = false + + isCompliant { + attestationsKeylessATT := input.authorityMatches.keylessatt.attestations + count(attestationsKeylessATT) == 0 + + attestationsKeyATT := input.authorityMatches.keyatt.attestations + count(attestationsKeyATT) == 1 + + keylessSignature := input.authorityMatches.keylesssignature.signatures + count(keylessSignature) == 1 + + keySignature := input.authorityMatches.keysignature.signatures + count(keySignature) == 1 + } + `, + pass: false, + errorMsg: "policy is not compliant for query 'isCompliant = data.sigstore.isCompliant'", + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + if err := ValidateJSONWithModuleInput([]byte(tt.jsonBody), tt.policy); (err == nil) != tt.pass { + t.Fatalf("Unexpected result: %v", err) + } else if err != nil { + if fmt.Sprintf("%s", err) != tt.errorMsg { + t.Errorf("Expected error %q, got %q", tt.errorMsg, err) + } + } + }) + } +} From 683a6c0c75e69c416f46df29db1389feac20b0cd Mon Sep 17 00:00:00 2001 From: Ville Aikas <11279988+vaikas@users.noreply.github.com> Date: Sat, 23 Apr 2022 21:30:55 -0700 Subject: [PATCH 40/53] Add parallelization for processing policies / authorities. (#1795) * Add parallelization for processing policies / authorities. Signed-off-by: Ville Aikas * Simplify by removing the wg. If error encountered, create an internal error. Signed-off-by: Ville Aikas --- pkg/cosign/kubernetes/webhook/validator.go | 151 ++++++++++++++------- 1 file changed, 103 insertions(+), 48 deletions(-) diff --git a/pkg/cosign/kubernetes/webhook/validator.go b/pkg/cosign/kubernetes/webhook/validator.go index 7590892a589..e9578ba4aa5 100644 --- a/pkg/cosign/kubernetes/webhook/validator.go +++ b/pkg/cosign/kubernetes/webhook/validator.go @@ -236,11 +236,13 @@ func (v *Validator) validatePodSpec(ctx context.Context, ps *corev1.PodSpec, opt // reasonable that the return value is 0, nil since there were no errors, but // the image was not validated against any matching policy and hence authority. func validatePolicies(ctx context.Context, ref name.Reference, policies map[string]webhookcip.ClusterImagePolicy, remoteOpts ...ociremote.Option) (map[string]*PolicyResult, map[string][]error) { - // Gather all validated policies here. - policyResults := make(map[string]*PolicyResult) - // For a policy that does not pass at least one authority, gather errors - // here so that we can give meaningful errors to the user. - ret := map[string][]error{} + type retChannelType struct { + name string + policyResult *PolicyResult + errors []error + } + results := make(chan retChannelType, len(policies)) + // For each matching policy it must validate at least one Authority within // it. // From the Design document, the part about multiple Policies matching: @@ -249,31 +251,53 @@ func validatePolicies(ctx context.Context, ref name.Reference, policies map[stri // If none of the Authorities for a given policy pass the checks, gather // the errors here. If one passes, do not return the errors. for cipName, cip := range policies { + // Due to running in gofunc + cipName := cipName + cip := cip logging.FromContext(ctx).Debugf("Checking Policy: %s", cipName) - policyResult, errs := ValidatePolicy(ctx, ref, cip, remoteOpts...) - if len(errs) > 0 { - ret[cipName] = append(ret[cipName], errs...) - } else { - // Ok, at least one Authority on the policy passed. If there's a CIP level - // policy, apply it against the results of the successful Authorities - // outputs. - if cip.Policy != nil { - logging.FromContext(ctx).Infof("Validating CIP level policy for %s", cipName) - policyJSON, err := json.Marshal(policyResult) - if err != nil { - ret[cipName] = append(ret[cipName], errors.Wrap(err, "marshaling policyresult")) - } else { - logging.FromContext(ctx).Infof("Validating CIP level policy against %s", string(policyJSON)) - err = policy.EvaluatePolicyAgainstJSON(ctx, "ClusterImagePolicy", cip.Policy.Type, cip.Policy.Data, policyJSON) + go func() { + result := retChannelType{name: cipName} + + result.policyResult, result.errors = ValidatePolicy(ctx, ref, cip, remoteOpts...) + if len(result.errors) == 0 { + // Ok, at least one Authority on the policy passed. If there's a CIP level + // policy, apply it against the results of the successful Authorities + // outputs. + if cip.Policy != nil { + logging.FromContext(ctx).Infof("Validating CIP level policy for %s", cipName) + policyJSON, err := json.Marshal(result.policyResult) if err != nil { - ret[cipName] = append(ret[cipName], err) + results <- result } else { - policyResults[cipName] = policyResult + logging.FromContext(ctx).Infof("Validating CIP level policy against %s", string(policyJSON)) + err = policy.EvaluatePolicyAgainstJSON(ctx, "ClusterImagePolicy", cip.Policy.Type, cip.Policy.Data, policyJSON) + if err != nil { + result.errors = append(result.errors, err) + } } } - } else { - policyResults[cipName] = policyResult } + results <- result + }() + } + // Gather all validated policies here. + policyResults := make(map[string]*PolicyResult) + // For a policy that does not pass at least one authority, gather errors + // here so that we can give meaningful errors to the user. + ret := map[string][]error{} + + for i := 0; i < len(policies); i++ { + result, ok := <-results + if !ok { + ret["internalerror"] = append(ret["internalerror"], fmt.Errorf("results channel failed to produce a result")) + } + switch { + case len(result.errors) > 0: + ret[result.name] = append(ret[result.name], result.errors...) + case len(result.policyResult.AuthorityMatches) > 0: + policyResults[result.name] = result.policyResult + default: + ret[result.name] = append(ret[result.name], fmt.Errorf("failed to process policy: %s", result.name)) } } return policyResults, ret @@ -285,40 +309,71 @@ func validatePolicies(ctx context.Context, ref name.Reference, policies map[stri // Returns PolicyResult, or errors encountered if none of the authorities // passed. func ValidatePolicy(ctx context.Context, ref name.Reference, cip webhookcip.ClusterImagePolicy, remoteOpts ...ociremote.Option) (*PolicyResult, []error) { - // If none of the Authorities for a given policy pass the checks, gather - // the errors here. If one passes, do not return the errors. - authorityErrors := []error{} - // We collect all the successfully satisfied Authorities into this and - // return it. - policyResult := PolicyResult{AuthorityMatches: make(map[string]AuthorityMatch)} + // Each gofunc creates and puts one of these into a results channel. + // Once each gofunc finishes, we go through the channel and pull out + // the results. + type retChannelType struct { + name string + attestations map[string][]PolicySignature + signatures []PolicySignature + err error + } + results := make(chan retChannelType, len(cip.Authorities)) for _, authority := range cip.Authorities { + authority := authority // due to gofunc logging.FromContext(ctx).Debugf("Checking Authority: %s", authority.Name) - // Assignment for appendAssign lint error - authorityRemoteOpts := remoteOpts - authorityRemoteOpts = append(authorityRemoteOpts, authority.RemoteOpts...) - if len(authority.Attestations) > 0 { - // We're doing the verify-attestations path, so validate (.att) - validatedAttestations, err := ValidatePolicyAttestationsForAuthority(ctx, ref, authority, authorityRemoteOpts...) - if err != nil { - authorityErrors = append(authorityErrors, err) - } else { - policyResult.AuthorityMatches[authority.Name] = AuthorityMatch{Attestations: validatedAttestations} - } - } else { - // We're doing the verify path, so validate image signatures (.sig) - validatedSignatures, err := ValidatePolicySignaturesForAuthority(ctx, ref, authority, authorityRemoteOpts...) - if err != nil { - authorityErrors = append(authorityErrors, err) + go func() { + result := retChannelType{name: authority.Name} + // Assignment for appendAssign lint error + authorityRemoteOpts := remoteOpts + authorityRemoteOpts = append(authorityRemoteOpts, authority.RemoteOpts...) + + if len(authority.Attestations) > 0 { + // We're doing the verify-attestations path, so validate (.att) + validatedAttestations, err := ValidatePolicyAttestationsForAuthority(ctx, ref, authority, authorityRemoteOpts...) + if err != nil { + result.err = err + } else { + result.attestations = validatedAttestations + } } else { - policyResult.AuthorityMatches[authority.Name] = AuthorityMatch{Signatures: validatedSignatures} + validatedSignatures, err := ValidatePolicySignaturesForAuthority(ctx, ref, authority, authorityRemoteOpts...) + if err != nil { + result.err = err + } else { + result.signatures = validatedSignatures + } } + results <- result + }() + } + // If none of the Authorities for a given policy pass the checks, gather + // the errors here. If one passes, do not return the errors. + authorityErrors := []error{} + // We collect all the successfully satisfied Authorities into this and + // return it. + policyResult := &PolicyResult{AuthorityMatches: make(map[string]AuthorityMatch)} + for i := 0; i < len(cip.Authorities); i++ { + result, ok := <-results + if !ok { + authorityErrors = append(authorityErrors, fmt.Errorf("results channel failed to produce a result")) + } + switch { + case result.err != nil: + authorityErrors = append(authorityErrors, result.err) + case len(result.signatures) > 0: + policyResult.AuthorityMatches[result.name] = AuthorityMatch{Signatures: result.signatures} + case len(result.attestations) > 0: + policyResult.AuthorityMatches[result.name] = AuthorityMatch{Attestations: result.attestations} + default: + authorityErrors = append(authorityErrors, fmt.Errorf("failed to process authority: %s", result.name)) } } if len(authorityErrors) > 0 { return nil, authorityErrors } - return &policyResult, authorityErrors + return policyResult, authorityErrors } func ociSignatureToPolicySignature(ctx context.Context, sigs []oci.Signature) []PolicySignature { From 0c4cf2e9bb7dac8f8ed57d600075e47f79cd087d Mon Sep 17 00:00:00 2001 From: Zack Newman Date: Sun, 24 Apr 2022 12:01:05 -0400 Subject: [PATCH 41/53] Allow passing keys via environment variables (`env://` refs) (#1794) * Bump github.com/sigstore/sigstore Signed-off-by: Zachary Newman * test: add test for blob.LoadFileOrURL Signed-off-by: Zachary Newman * refactor: break up LoadFileOrURL by scheme Signed-off-by: Zachary Newman * feat: add "env://" scheme for blob load Signed-off-by: Zachary Newman * feat: add "env://" scheme for key lookup Tested manually as well: ```shell $ export COSIGN_PASSWORD=foo $ cosign generate-key-pair Enter password for private key: Enter password for private key again: Private key written to cosign.key Public key written to cosign.pub $ export MYPRIVKEY="$(cat cosign.key)" $ export MYPUBKEY="$(cat cosign.pub)" $ cosign verify-blob --key env://MYPUBKEY /dev/null --signature <(cosign sign-blob --key env://MYPRIVKEY /dev/null) Using payload from: /dev/null tlog entry created with index: 2095539 tlog entry verified with uuid: dd55086556d7ac0cded8f50961b68f7740e1435fbc5bb47460a8d78321313c7d index: 2095539 Verified OK ``` Signed-off-by: Zachary Newman * test: skip test that flakes on Windows Signed-off-by: Zachary Newman --- cmd/cosign/cli/sign.go | 3 ++ cmd/cosign/cli/verify.go | 3 ++ doc/cosign_sign.md | 3 ++ doc/cosign_verify.md | 3 ++ go.mod | 8 +-- go.sum | 34 ++++++------- pkg/blob/load.go | 37 ++++++++++---- pkg/blob/load_test.go | 99 ++++++++++++++++++++++++++++++++++++++ pkg/signature/keys.go | 12 +++-- pkg/signature/keys_test.go | 29 +++++++++++ 10 files changed, 194 insertions(+), 37 deletions(-) create mode 100644 pkg/blob/load_test.go diff --git a/cmd/cosign/cli/sign.go b/cmd/cosign/cli/sign.go index 3acdc1b2b61..f0f2ea19129 100644 --- a/cmd/cosign/cli/sign.go +++ b/cmd/cosign/cli/sign.go @@ -47,6 +47,9 @@ func Sign() *cobra.Command { # sign a container image and add annotations cosign sign --key cosign.key -a key1=value1 -a key2=value2 + # sign a container image with a key stored in an environment variable + cosign sign --key env://[ENV_VAR] + # sign a container image with a key pair stored in Azure Key Vault cosign sign --key azurekms://[VAULT_NAME][VAULT_URI]/[KEY] diff --git a/cmd/cosign/cli/verify.go b/cmd/cosign/cli/verify.go index e48b4175e6c..6f180e7c549 100644 --- a/cmd/cosign/cli/verify.go +++ b/cmd/cosign/cli/verify.go @@ -61,6 +61,9 @@ against the transparency log.`, # verify image with public key provided by URL cosign verify --key https://host.for/[FILE] + # verify image with a key stored in an environment variable + cosign verify --key env://[ENV_VAR] + # verify image with public key stored in Google Cloud KMS cosign verify --key gcpkms://projects/[PROJECT]/locations/global/keyRings/[KEYRING]/cryptoKeys/[KEY] diff --git a/doc/cosign_sign.md b/doc/cosign_sign.md index c4d3f26e0ff..88230c10524 100644 --- a/doc/cosign_sign.md +++ b/doc/cosign_sign.md @@ -27,6 +27,9 @@ cosign sign [flags] # sign a container image and add annotations cosign sign --key cosign.key -a key1=value1 -a key2=value2 + # sign a container image with a key stored in an environment variable + cosign sign --key env://[ENV_VAR] + # sign a container image with a key pair stored in Azure Key Vault cosign sign --key azurekms://[VAULT_NAME][VAULT_URI]/[KEY] diff --git a/doc/cosign_verify.md b/doc/cosign_verify.md index 68bf94f6d90..9de4dbad45f 100644 --- a/doc/cosign_verify.md +++ b/doc/cosign_verify.md @@ -44,6 +44,9 @@ cosign verify [flags] # verify image with public key provided by URL cosign verify --key https://host.for/[FILE] + # verify image with a key stored in an environment variable + cosign verify --key env://[ENV_VAR] + # verify image with public key stored in Google Cloud KMS cosign verify --key gcpkms://projects/[PROJECT]/locations/global/keyRings/[KEYRING]/cryptoKeys/[KEY] diff --git a/go.mod b/go.mod index 0bf5857d0fa..eaed295cbb3 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,7 @@ require ( github.com/secure-systems-lab/go-securesystemslib v0.3.1 github.com/sigstore/fulcio v0.1.2-0.20220114150912-86a2036f9bc7 github.com/sigstore/rekor v0.4.1-0.20220114213500-23f583409af3 - github.com/sigstore/sigstore v1.2.1-0.20220401110139-0e610e39782f + github.com/sigstore/sigstore v1.2.1-0.20220424143412-3d41663116d5 github.com/spf13/cobra v1.4.0 github.com/spf13/viper v1.11.0 github.com/spiffe/go-spiffe/v2 v2.0.0 @@ -113,9 +113,9 @@ require ( cloud.google.com/go/kms v1.4.0 // indirect contrib.go.opencensus.io/exporter/ocagent v0.7.1-0.20200907061046-05415f1de66d // indirect contrib.go.opencensus.io/exporter/prometheus v0.4.0 // indirect - github.com/Azure/azure-sdk-for-go v63.2.0+incompatible // indirect + github.com/Azure/azure-sdk-for-go v63.3.0+incompatible // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect - github.com/Azure/go-autorest/autorest v0.11.25 // indirect + github.com/Azure/go-autorest/autorest v0.11.27 // indirect github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 // indirect github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect @@ -131,7 +131,7 @@ require ( github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/ReneKroon/ttlcache/v2 v2.11.0 // indirect github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect - github.com/aws/aws-sdk-go v1.43.37 // indirect + github.com/aws/aws-sdk-go v1.43.45 // indirect github.com/aws/aws-sdk-go-v2 v1.14.0 // indirect github.com/aws/aws-sdk-go-v2/config v1.14.0 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.9.0 // indirect diff --git a/go.sum b/go.sum index 3fa87f6e4f9..4e331a4adaa 100644 --- a/go.sum +++ b/go.sum @@ -131,9 +131,8 @@ github.com/Azure/azure-sdk-for-go v59.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9mo github.com/Azure/azure-sdk-for-go v60.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v60.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v62.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go v63.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go v63.2.0+incompatible h1:OIqkK/zTGqVUuzpEvY0B1YSYDRAFC/j+y0w2GovCggI= -github.com/Azure/azure-sdk-for-go v63.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v63.3.0+incompatible h1:INepVujzUrmArRZjDLHbtER+FkvCoEwyRCXGqOlmDII= +github.com/Azure/azure-sdk-for-go v63.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-service-bus-go v0.9.1/go.mod h1:yzBx6/BUGfjfeqbRZny9AQIbIe3AcV9WZbAdpkoXOa0= github.com/Azure/azure-service-bus-go v0.11.5/go.mod h1:MI6ge2CuQWBVq+ly456MY7XqNLJip5LO1iSFodbNLbU= github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0= @@ -154,8 +153,8 @@ github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgq github.com/Azure/go-autorest/autorest v0.11.19/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= github.com/Azure/go-autorest/autorest v0.11.22/go.mod h1:BAWYUWGPEtKPzjVkp0Q6an0MJcJDsoh5Z1BFAEFs4Xs= github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc= -github.com/Azure/go-autorest/autorest v0.11.25 h1:yp+V8DGur2aIUE87ebP8twPLz6k68jtJTlg61mEoByA= -github.com/Azure/go-autorest/autorest v0.11.25/go.mod h1:7l8ybrIdUmGqZMTD0sRtAr8NvbHjfofbf8RSP2q7w7U= +github.com/Azure/go-autorest/autorest v0.11.27 h1:F3R3q42aWytozkV8ihzcgMO4OA4cuqr3bNlsEuF6//A= +github.com/Azure/go-autorest/autorest v0.11.27/go.mod h1:7l8ybrIdUmGqZMTD0sRtAr8NvbHjfofbf8RSP2q7w7U= github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= github.com/Azure/go-autorest/autorest/adal v0.9.4/go.mod h1:/3SMAM86bP6wC9Ev35peQDUeqFZBMH07vvUOmg4z/fE= github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= @@ -325,9 +324,8 @@ github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zK github.com/aws/aws-sdk-go v1.42.8/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/aws/aws-sdk-go v1.42.22/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/aws/aws-sdk-go v1.42.25/go.mod h1:gyRszuZ/icHmHAVE4gc/r+cfCmhA1AD+vqfWbgI+eHs= -github.com/aws/aws-sdk-go v1.43.30/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= -github.com/aws/aws-sdk-go v1.43.37 h1:kyZ7UjaPZaCik+asF33UFOOYSwr9liDRr/UM/vuw8yY= -github.com/aws/aws-sdk-go v1.43.37/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.43.45 h1:2708Bj4uV+ym62MOtBnErm/CDX61C4mFe9V2gXy1caE= +github.com/aws/aws-sdk-go v1.43.45/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/aws/aws-sdk-go-v2 v1.7.1/go.mod h1:L5LuPC1ZgDr2xQS7AmIec/Jlc7O/Y1u2KxJyNVab250= github.com/aws/aws-sdk-go-v2 v1.11.0/go.mod h1:SQfA+m2ltnu1cA0soUkj4dRSsmITiVQUJvBIZjzfPyQ= @@ -951,8 +949,8 @@ github.com/go-redis/redis v6.15.8+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8w github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= github.com/go-rod/rod v0.101.8/go.mod h1:N/zlT53CfSpq74nb6rOR0K8UF0SPUPBmzBnArrms+mY= -github.com/go-rod/rod v0.104.4 h1:sQR35AFo9ceR7ksh+Ld81bQzIbrXlQH/IO46iCWqxts= -github.com/go-rod/rod v0.104.4/go.mod h1:trmrxxg+qUodIIQiYeyJbW5ZMo0FSajmdEGw2tHzlM4= +github.com/go-rod/rod v0.106.1 h1:+9YdoTT56KI3KrFfWVr3I13wh0qbhm/Aq+7JvCBA6AQ= +github.com/go-rod/rod v0.106.1/go.mod h1:+YLe2X+nAuEGpYWs7rKPZr9SMX100FbxYZaeU1Dofpc= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= @@ -2034,8 +2032,8 @@ github.com/sigstore/rekor v0.4.1-0.20220114213500-23f583409af3 h1:mbqXrm8YZXN/cJ github.com/sigstore/rekor v0.4.1-0.20220114213500-23f583409af3/go.mod h1:u9clLqaVjqV9pExVL1XkM37dGyMCOX/LMocS9nsnWDY= github.com/sigstore/sigstore v1.0.2-0.20211210190220-04746d994282/go.mod h1:SuM+QIHtnnR9eGsURRLv5JfxM6KeaU0XKA1O7FmLs4Q= github.com/sigstore/sigstore v1.1.0/go.mod h1:gDpcHw4VwpoL5C6N1Ud1YtBsc+ikRDwDelDlWRyYoE8= -github.com/sigstore/sigstore v1.2.1-0.20220401110139-0e610e39782f h1:JPD9q1718mub78ILVcTqOZ/q4ECKCQ7JQfUX/q+nEJ4= -github.com/sigstore/sigstore v1.2.1-0.20220401110139-0e610e39782f/go.mod h1:9wYagRiKz/8KgK/YFPM6FA8WrNjv3Y6rQUQWBLqJXs0= +github.com/sigstore/sigstore v1.2.1-0.20220424143412-3d41663116d5 h1:8OL06Knchax4CMtdfquC3ASWQPtYMJgyeQImWQPw6XE= +github.com/sigstore/sigstore v1.2.1-0.20220424143412-3d41663116d5/go.mod h1:OvpZniSE9oRPnW7+mhxljRt2RAQU+TwcnhYbqQsPwPc= github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -2255,15 +2253,15 @@ github.com/yashtewari/glob-intersection v0.0.0-20180916065949-5c77d914dd0b/go.mo github.com/yeya24/promlinter v0.1.0/go.mod h1:rs5vtZzeBHqqMwXqFScncpCF6u06lezhZepno9AB1Oc= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/ysmood/goob v0.3.0/go.mod h1:S3lq113Y91y1UBf1wj1pFOxeahvfKkCk6mTWTWbDdWs= -github.com/ysmood/goob v0.3.1 h1:qMp5364BGS1DLJVrAqUxTF6KOFt0YDot8GC70u/0jbI= -github.com/ysmood/goob v0.3.1/go.mod h1:S3lq113Y91y1UBf1wj1pFOxeahvfKkCk6mTWTWbDdWs= +github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= +github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18= github.com/ysmood/got v0.15.1/go.mod h1:pE1l4LOwOBhQg6A/8IAatkGp7uZjnalzrZolnlhhMgY= -github.com/ysmood/got v0.19.1/go.mod h1:pE1l4LOwOBhQg6A/8IAatkGp7uZjnalzrZolnlhhMgY= +github.com/ysmood/got v0.23.3/go.mod h1:pE1l4LOwOBhQg6A/8IAatkGp7uZjnalzrZolnlhhMgY= github.com/ysmood/gotrace v0.2.2/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= -github.com/ysmood/gotrace v0.4.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= +github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= github.com/ysmood/gson v0.6.4/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= -github.com/ysmood/gson v0.7.0 h1:oQhY2FQtfy3+bgaNeqopd7NGAB6Me+UpG0n7oO4VDko= -github.com/ysmood/gson v0.7.0/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= +github.com/ysmood/gson v0.7.1 h1:zKL2MTGtynxdBdlZjyGsvEOZ7dkxaY5TH6QhAbTgz0Q= +github.com/ysmood/gson v0.7.1/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= github.com/ysmood/leakless v0.7.0 h1:XCGdaPExyoreoQd+H5qgxM3ReNbSPFsEXpSKwbXbwQw= github.com/ysmood/leakless v0.7.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= diff --git a/pkg/blob/load.go b/pkg/blob/load.go index 9c93fccc779..c92d49c4ca3 100644 --- a/pkg/blob/load.go +++ b/pkg/blob/load.go @@ -15,6 +15,7 @@ package blob import ( + "fmt" "io" "net/http" "os" @@ -25,16 +26,32 @@ import ( func LoadFileOrURL(fileRef string) ([]byte, error) { var raw []byte var err error - if strings.HasPrefix(fileRef, "http://") || strings.HasPrefix(fileRef, "https://") { - // #nosec G107 - resp, err := http.Get(fileRef) - if err != nil { - return nil, err - } - defer resp.Body.Close() - raw, err = io.ReadAll(resp.Body) - if err != nil { - return nil, err + parts := strings.SplitAfterN(fileRef, "://", 2) + if len(parts) == 2 { + scheme := parts[0] + switch scheme { + case "http://": + fallthrough + case "https://": + // #nosec G107 + resp, err := http.Get(fileRef) + if err != nil { + return nil, err + } + defer resp.Body.Close() + raw, err = io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + case "env://": + envVar := parts[1] + value, found := os.LookupEnv(envVar) + if !found { + return nil, fmt.Errorf("loading URL: env var $%s not found", envVar) + } + raw = []byte(value) + default: + return nil, fmt.Errorf("loading URL: unrecognized scheme: %s", scheme) } } else { raw, err = os.ReadFile(filepath.Clean(fileRef)) diff --git a/pkg/blob/load_test.go b/pkg/blob/load_test.go new file mode 100644 index 00000000000..add9db7bd62 --- /dev/null +++ b/pkg/blob/load_test.go @@ -0,0 +1,99 @@ +// +// 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 blob + +import ( + "bytes" + "net/http" + "net/http/httptest" + "os" + "path" + "runtime" + "testing" +) + +func TestLoadFile(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Skipping on Windows due to https://github.com/golang/go/issues/51442") + } + temp := t.TempDir() + fname := "filename.txt" + path := path.Join(temp, fname) + data := []byte("test") + defer os.Remove(path) + os.WriteFile(path, data, 0400) + + // absolute path + actual, err := LoadFileOrURL(path) + if err != nil { + t.Errorf("Reading from absolute path %s failed: %v", path, err) + } else if !bytes.Equal(actual, data) { + t.Errorf("LoadFileOrURL(absolute path) = '%s'; want '%s'", actual, data) + } + + if err = os.Chdir(temp); err != nil { + t.Fatalf("Chdir('%s'): %v", temp, err) + } + actual, err = LoadFileOrURL(fname) + if err != nil { + t.Errorf("Reading from relative path %s failed: %v", fname, err) + } else if !bytes.Equal(actual, data) { + t.Errorf("LoadFileOrURL(relative path) = '%s'; want '%s'", actual, data) + } +} + +func TestLoadURL(t *testing.T) { + data := []byte("test") + + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.Write(data) + })) + defer server.Close() + + actual, err := LoadFileOrURL(server.URL) + if err != nil { + t.Errorf("Reading from HTTP failed: %v", err) + } else if !bytes.Equal(actual, data) { + t.Errorf("LoadFileOrURL(HTTP) = '%s'; want '%s'", actual, data) + } + + os.Setenv("MY_ENV_VAR", string(data)) + actual, err = LoadFileOrURL("env://MY_ENV_VAR") + if err != nil { + t.Errorf("Reading from environment failed: %v", err) + } else if !bytes.Equal(actual, data) { + t.Errorf("LoadFileOrURL(env) = '%s'; want '%s'", actual, data) + } + + os.Setenv("MY_ENV_VAR", "") + actual, err = LoadFileOrURL("env://MY_ENV_VAR") + if err != nil { + t.Errorf("Reading from environment failed: %v", err) + } else if !bytes.Equal(actual, make([]byte, 0)) { + t.Errorf("LoadFileOrURL(env) = '%s'; should be empty", actual) + } + + os.Unsetenv("MY_ENV_VAR") + _, err = LoadFileOrURL("env://MY_ENV_VAR") + if err == nil { + t.Error("LoadFileOrURL(): expected error for unset env var") + } + + _, err = LoadFileOrURL("invalid://url") + if err == nil { + t.Error("LoadFileOrURL(): expected error for invalid scheme") + } +} diff --git a/pkg/signature/keys.go b/pkg/signature/keys.go index 775be117d15..610d0c25b3a 100644 --- a/pkg/signature/keys.go +++ b/pkg/signature/keys.go @@ -19,8 +19,6 @@ import ( "crypto" "crypto/x509" "fmt" - "os" - "path/filepath" "strings" "github.com/pkg/errors" @@ -86,7 +84,7 @@ func VerifierForKeyRef(ctx context.Context, keyRef string, hashAlgorithm crypto. } func loadKey(keyPath string, pf cosign.PassFunc) (signature.SignerVerifier, error) { - kb, err := os.ReadFile(filepath.Clean(keyPath)) + kb, err := blob.LoadFileOrURL(keyPath) if err != nil { return nil, err } @@ -169,10 +167,14 @@ func SignerVerifierFromKeyRef(ctx context.Context, keyRef string, pf cosign.Pass if strings.Contains(keyRef, "://") { sv, err := kms.Get(ctx, keyRef, crypto.SHA256) - if err != nil { + if err == nil { + return sv, nil + } + var e *kms.ProviderNotFoundError + if !errors.As(err, &e) { return nil, fmt.Errorf("kms get: %w", err) } - return sv, nil + // ProviderNotFoundError is okay; loadKey handles other URL schemes } return loadKey(keyRef, pf) diff --git a/pkg/signature/keys_test.go b/pkg/signature/keys_test.go index e95d335639e..5f1c0ac4389 100644 --- a/pkg/signature/keys_test.go +++ b/pkg/signature/keys_test.go @@ -108,6 +108,35 @@ func TestPublicKeyFromFileRef(t *testing.T) { } } +func TestPublicKeyFromEnvVar(t *testing.T) { + keys, err := cosign.GenerateKeyPair(pass("whatever")) + if err != nil { + t.Fatalf("failed to generate keypair: %v", err) + } + ctx := context.Background() + + os.Setenv("MY_ENV_VAR", string(keys.PublicBytes)) + defer os.Unsetenv("MY_ENV_VAR") + if _, err := PublicKeyFromKeyRef(ctx, "env://MY_ENV_VAR"); err != nil { + t.Fatalf("PublicKeyFromKeyRef returned error: %v", err) + } +} + +func TestSignerVerifierFromEnvVar(t *testing.T) { + passFunc := pass("whatever") + keys, err := cosign.GenerateKeyPair(passFunc) + if err != nil { + t.Fatalf("failed to generate keypair: %v", err) + } + ctx := context.Background() + + os.Setenv("MY_ENV_VAR", string(keys.PrivateBytes)) + defer os.Unsetenv("MY_ENV_VAR") + if _, err := SignerVerifierFromKeyRef(ctx, "env://MY_ENV_VAR", passFunc); err != nil { + t.Fatalf("SignerVerifierFromKeyRef returned error: %v", err) + } +} + func pass(s string) cosign.PassFunc { return func(_ bool) ([]byte, error) { return []byte(s), nil From f08ae46b05d7cda7486c7bd65f1c04d8ad5f3b78 Mon Sep 17 00:00:00 2001 From: Ville Aikas <11279988+vaikas@users.noreply.github.com> Date: Sun, 24 Apr 2022 12:09:23 -0700 Subject: [PATCH 42/53] Handle context cancelled properly + tests. (#1796) Signed-off-by: Ville Aikas --- pkg/cosign/kubernetes/webhook/validator.go | 59 +++++++++------- .../kubernetes/webhook/validator_test.go | 70 ++++++++++++++++--- 2 files changed, 95 insertions(+), 34 deletions(-) diff --git a/pkg/cosign/kubernetes/webhook/validator.go b/pkg/cosign/kubernetes/webhook/validator.go index e9578ba4aa5..6e2590eb0d4 100644 --- a/pkg/cosign/kubernetes/webhook/validator.go +++ b/pkg/cosign/kubernetes/webhook/validator.go @@ -287,19 +287,25 @@ func validatePolicies(ctx context.Context, ref name.Reference, policies map[stri ret := map[string][]error{} for i := 0; i < len(policies); i++ { - result, ok := <-results - if !ok { - ret["internalerror"] = append(ret["internalerror"], fmt.Errorf("results channel failed to produce a result")) - } - switch { - case len(result.errors) > 0: - ret[result.name] = append(ret[result.name], result.errors...) - case len(result.policyResult.AuthorityMatches) > 0: - policyResults[result.name] = result.policyResult - default: - ret[result.name] = append(ret[result.name], fmt.Errorf("failed to process policy: %s", result.name)) + select { + case <-ctx.Done(): + ret["internalerror"] = append(ret["internalerror"], fmt.Errorf("context was canceled before validation completed")) + case result, ok := <-results: + if !ok { + ret["internalerror"] = append(ret["internalerror"], fmt.Errorf("results channel failed to produce a result")) + continue + } + switch { + case len(result.errors) > 0: + ret[result.name] = append(ret[result.name], result.errors...) + case len(result.policyResult.AuthorityMatches) > 0: + policyResults[result.name] = result.policyResult + default: + ret[result.name] = append(ret[result.name], fmt.Errorf("failed to process policy: %s", result.name)) + } } } + return policyResults, ret } @@ -355,19 +361,24 @@ func ValidatePolicy(ctx context.Context, ref name.Reference, cip webhookcip.Clus // return it. policyResult := &PolicyResult{AuthorityMatches: make(map[string]AuthorityMatch)} for i := 0; i < len(cip.Authorities); i++ { - result, ok := <-results - if !ok { - authorityErrors = append(authorityErrors, fmt.Errorf("results channel failed to produce a result")) - } - switch { - case result.err != nil: - authorityErrors = append(authorityErrors, result.err) - case len(result.signatures) > 0: - policyResult.AuthorityMatches[result.name] = AuthorityMatch{Signatures: result.signatures} - case len(result.attestations) > 0: - policyResult.AuthorityMatches[result.name] = AuthorityMatch{Attestations: result.attestations} - default: - authorityErrors = append(authorityErrors, fmt.Errorf("failed to process authority: %s", result.name)) + select { + case <-ctx.Done(): + authorityErrors = append(authorityErrors, fmt.Errorf("context was canceled before validation completed")) + case result, ok := <-results: + if !ok { + authorityErrors = append(authorityErrors, fmt.Errorf("results channel failed to produce a result")) + continue + } + switch { + case result.err != nil: + authorityErrors = append(authorityErrors, result.err) + case len(result.signatures) > 0: + policyResult.AuthorityMatches[result.name] = AuthorityMatch{Signatures: result.signatures} + case len(result.attestations) > 0: + policyResult.AuthorityMatches[result.name] = AuthorityMatch{Attestations: result.attestations} + default: + authorityErrors = append(authorityErrors, fmt.Errorf("failed to process authority: %s", result.name)) + } } } if len(authorityErrors) > 0 { diff --git a/pkg/cosign/kubernetes/webhook/validator_test.go b/pkg/cosign/kubernetes/webhook/validator_test.go index 79593ef39cd..fe9b996da86 100644 --- a/pkg/cosign/kubernetes/webhook/validator_test.go +++ b/pkg/cosign/kubernetes/webhook/validator_test.go @@ -56,6 +56,12 @@ import ( const ( fulcioRootCert = "-----BEGIN CERTIFICATE-----\nMIICNzCCAd2gAwIBAgITPLBoBQhl1hqFND9S+SGWbfzaRTAKBggqhkjOPQQDAjBo\nMQswCQYDVQQGEwJVSzESMBAGA1UECBMJV2lsdHNoaXJlMRMwEQYDVQQHEwpDaGlw\ncGVuaGFtMQ8wDQYDVQQKEwZSZWRIYXQxDDAKBgNVBAsTA0NUTzERMA8GA1UEAxMI\ndGVzdGNlcnQwHhcNMjEwMzEyMjMyNDQ5WhcNMzEwMjI4MjMyNDQ5WjBoMQswCQYD\nVQQGEwJVSzESMBAGA1UECBMJV2lsdHNoaXJlMRMwEQYDVQQHEwpDaGlwcGVuaGFt\nMQ8wDQYDVQQKEwZSZWRIYXQxDDAKBgNVBAsTA0NUTzERMA8GA1UEAxMIdGVzdGNl\ncnQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQRn+Alyof6xP3GQClSwgV0NFuY\nYEwmKP/WLWr/LwB6LUYzt5v49RlqG83KuaJSpeOj7G7MVABdpIZYWwqAiZV3o2Yw\nZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBATAdBgNVHQ4EFgQU\nT8Jwm6JuVb0dsiuHUROiHOOVHVkwHwYDVR0jBBgwFoAUT8Jwm6JuVb0dsiuHUROi\nHOOVHVkwCgYIKoZIzj0EAwIDSAAwRQIhAJkNZmP6sKA+8EebRXFkBa9DPjacBpTc\nOljJotvKidRhAiAuNrIazKEw2G4dw8x1z6EYk9G+7fJP5m93bjm/JfMBtA==\n-----END CERTIFICATE-----" rekorResponse = "bad response" + + // Random public key (cosign generate-key-pair) 2022-03-18 + authorityKeyCosignPubString = `-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENAyijLvRu5QpCPp2uOj8C79ZW1VJ +SID/4H61ZiRzN4nqONzp+ZF22qQTk3MFO3D0/ZKmWHAosIf2pf2GHH7myA== +-----END PUBLIC KEY-----` ) func TestValidatePodSpec(t *testing.T) { @@ -91,11 +97,6 @@ func TestValidatePodSpec(t *testing.T) { } var authorityKeyCosignPub *ecdsa.PublicKey - // Random public key (cosign generate-key-pair) 2022-03-18 - authorityKeyCosignPubString := `-----BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENAyijLvRu5QpCPp2uOj8C79ZW1VJ -SID/4H61ZiRzN4nqONzp+ZF22qQTk3MFO3D0/ZKmWHAosIf2pf2GHH7myA== ------END PUBLIC KEY-----` pems := parsePems([]byte(authorityKeyCosignPubString)) if len(pems) > 0 { @@ -1175,11 +1176,6 @@ func TestValidatePolicy(t *testing.T) { } t.Logf("rekorURL: %s", rekorURL.String()) var authorityKeyCosignPub *ecdsa.PublicKey - // Random public key (cosign generate-key-pair) 2022-03-18 - authorityKeyCosignPubString := `-----BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENAyijLvRu5QpCPp2uOj8C79ZW1VJ -SID/4H61ZiRzN4nqONzp+ZF22qQTk3MFO3D0/ZKmWHAosIf2pf2GHH7myA== ------END PUBLIC KEY-----` pems := parsePems([]byte(authorityKeyCosignPubString)) if len(pems) > 0 { @@ -1350,3 +1346,57 @@ func validateErrors(t *testing.T, wantErr []string, got []error) { } } } + +func TestValidatePolicyCancelled(t *testing.T) { + var authorityKeyCosignPub *ecdsa.PublicKey + pems := parsePems([]byte(authorityKeyCosignPubString)) + if len(pems) > 0 { + key, _ := x509.ParsePKIXPublicKey(pems[0].Bytes) + authorityKeyCosignPub = key.(*ecdsa.PublicKey) + } else { + t.Errorf("Error parsing authority key from string") + } + // Resolved via crane digest on 2021/09/25 + digest := name.MustParseReference("gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4") + + testContext, cancelFunc := context.WithCancel(context.Background()) + cip := webhookcip.ClusterImagePolicy{ + Authorities: []webhookcip.Authority{{ + Name: "authority-0", + Key: &webhookcip.KeyRef{ + PublicKeys: []crypto.PublicKey{authorityKeyCosignPub}, + }, + }}, + } + wantErrs := []string{"context was canceled before validation completed"} + cancelFunc() + _, gotErrs := ValidatePolicy(testContext, digest, cip) + validateErrors(t, wantErrs, gotErrs) +} + +func TestValidatePoliciesCancelled(t *testing.T) { + var authorityKeyCosignPub *ecdsa.PublicKey + pems := parsePems([]byte(authorityKeyCosignPubString)) + if len(pems) > 0 { + key, _ := x509.ParsePKIXPublicKey(pems[0].Bytes) + authorityKeyCosignPub = key.(*ecdsa.PublicKey) + } else { + t.Errorf("Error parsing authority key from string") + } + // Resolved via crane digest on 2021/09/25 + digest := name.MustParseReference("gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4") + + testContext, cancelFunc := context.WithCancel(context.Background()) + cip := webhookcip.ClusterImagePolicy{ + Authorities: []webhookcip.Authority{{ + Name: "authority-0", + Key: &webhookcip.KeyRef{ + PublicKeys: []crypto.PublicKey{authorityKeyCosignPub}, + }, + }}, + } + wantErrs := []string{"context was canceled before validation completed"} + cancelFunc() + _, gotErrs := validatePolicies(testContext, digest, map[string]webhookcip.ClusterImagePolicy{"testcip": cip}) + validateErrors(t, wantErrs, gotErrs["internalerror"]) +} From e71ae68e44fe3098f48d420e5ae8f90f5a217701 Mon Sep 17 00:00:00 2001 From: Ville Aikas <11279988+vaikas@users.noreply.github.com> Date: Sun, 24 Apr 2022 14:55:07 -0700 Subject: [PATCH 43/53] Fix a bug where an error would send duplicate results. (#1797) Signed-off-by: Ville Aikas --- pkg/cosign/kubernetes/webhook/validator.go | 4 +- .../kubernetes/webhook/validator_test.go | 48 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/pkg/cosign/kubernetes/webhook/validator.go b/pkg/cosign/kubernetes/webhook/validator.go index 6e2590eb0d4..54440b133fd 100644 --- a/pkg/cosign/kubernetes/webhook/validator.go +++ b/pkg/cosign/kubernetes/webhook/validator.go @@ -267,9 +267,9 @@ func validatePolicies(ctx context.Context, ref name.Reference, policies map[stri logging.FromContext(ctx).Infof("Validating CIP level policy for %s", cipName) policyJSON, err := json.Marshal(result.policyResult) if err != nil { - results <- result + result.errors = append(result.errors, err) } else { - logging.FromContext(ctx).Infof("Validating CIP level policy against %s", string(policyJSON)) + logging.FromContext(ctx).Debugf("Validating CIP level policy against %s", string(policyJSON)) err = policy.EvaluatePolicyAgainstJSON(ctx, "ClusterImagePolicy", cip.Policy.Type, cip.Policy.Data, policyJSON) if err != nil { result.errors = append(result.errors, err) diff --git a/pkg/cosign/kubernetes/webhook/validator_test.go b/pkg/cosign/kubernetes/webhook/validator_test.go index fe9b996da86..7b7ea96513b 100644 --- a/pkg/cosign/kubernetes/webhook/validator_test.go +++ b/pkg/cosign/kubernetes/webhook/validator_test.go @@ -341,6 +341,54 @@ UoJou2P8sbDxpLiE/v3yLw1/jyOrCPWYHWFXnyyeGlkgSVefG54tNoK7Uw== return errs }(), cvs: fail, + }, { + name: "simple, authority keyless checks out, good fulcio, bad cip policy", + ps: &corev1.PodSpec{ + InitContainers: []corev1.Container{{ + Name: "setup-stuff", + Image: digest.String(), + }}, + Containers: []corev1.Container{{ + Name: "user-container", + Image: digest.String(), + }}, + }, + customContext: config.ToContext(context.Background(), + &config.Config{ + ImagePolicyConfig: &config.ImagePolicyConfig{ + Policies: map[string]webhookcip.ClusterImagePolicy{ + "cluster-image-policy-keyless": { + Images: []v1alpha1.ImagePattern{{ + Regex: ".*", + }}, + Authorities: []webhookcip.Authority{ + { + Keyless: &webhookcip.KeylessRef{ + URL: fulcioURL, + }, + }, + }, + Policy: &webhookcip.AttestationPolicy{ + Name: "invalid json policy", + Type: "cue", + Data: `{"wontgo}`, + }, + }, + }, + }, + }, + ), + want: func() *apis.FieldError { + var errs *apis.FieldError + fe := apis.ErrGeneric("failed policy: cluster-image-policy-keyless", "image").ViaFieldIndex("initContainers", 0) + fe.Details = fmt.Sprintf("%s failed evaluating cue policy for ClusterImagePolicy : string literal not terminated", digest.String()) + errs = errs.Also(fe) + fe2 := apis.ErrGeneric("failed policy: cluster-image-policy-keyless", "image").ViaFieldIndex("containers", 0) + fe2.Details = fmt.Sprintf("%s failed evaluating cue policy for ClusterImagePolicy : string literal not terminated", digest.String()) + errs = errs.Also(fe2) + return errs + }(), + cvs: pass, }, { name: "simple, no error, authority keyless, good fulcio", ps: &corev1.PodSpec{ From 87b06ef7611dd711ca3b35a41eb66816cf1437cd Mon Sep 17 00:00:00 2001 From: Billy Lynch Date: Mon, 25 Apr 2022 11:56:00 -0400 Subject: [PATCH 44/53] Revert "Refactor fulcio signer to take in KeyOpts. (#1788)" (#1798) This reverts commit 8368baddf688c6eac4a47adee1ed42a4fcd0f83d. Signed-off-by: Billy Lynch --- cmd/cosign/cli/attest.go | 3 +- cmd/cosign/cli/attest/attest.go | 2 +- cmd/cosign/cli/fulcio/fulcio.go | 23 ++--------- .../fulcio/fulcioverifier/fulcioverifier.go | 6 +-- cmd/cosign/cli/options/key.go | 37 ------------------ cmd/cosign/cli/policy_init.go | 2 +- cmd/cosign/cli/sign.go | 2 +- cmd/cosign/cli/sign/sign.go | 32 +++++++++++----- cmd/cosign/cli/sign/sign_blob.go | 21 +++++++++- cmd/cosign/cli/sign/sign_test.go | 2 +- cmd/cosign/cli/signblob.go | 2 +- cmd/cosign/cli/verify.go | 3 +- cmd/cosign/cli/verify/verify_blob.go | 7 ++-- test/e2e_test.go | 38 +++++++++---------- 14 files changed, 81 insertions(+), 99 deletions(-) delete mode 100644 cmd/cosign/cli/options/key.go diff --git a/cmd/cosign/cli/attest.go b/cmd/cosign/cli/attest.go index ffc30866681..aabb1c54b4c 100644 --- a/cmd/cosign/cli/attest.go +++ b/cmd/cosign/cli/attest.go @@ -22,6 +22,7 @@ import ( "github.com/sigstore/cosign/cmd/cosign/cli/attest" "github.com/sigstore/cosign/cmd/cosign/cli/generate" "github.com/sigstore/cosign/cmd/cosign/cli/options" + "github.com/sigstore/cosign/cmd/cosign/cli/sign" ) func Attest() *cobra.Command { @@ -62,7 +63,7 @@ func Attest() *cobra.Command { if err != nil { return err } - ko := options.KeyOpts{ + ko := sign.KeyOpts{ KeyRef: o.Key, PassFunc: generate.GetPass, Sk: o.SecurityKey.Use, diff --git a/cmd/cosign/cli/attest/attest.go b/cmd/cosign/cli/attest/attest.go index 473064b376f..bef095b5ba1 100644 --- a/cmd/cosign/cli/attest/attest.go +++ b/cmd/cosign/cli/attest/attest.go @@ -74,7 +74,7 @@ func uploadToTlog(ctx context.Context, sv *sign.SignerVerifier, rekorURL string, } //nolint -func AttestCmd(ctx context.Context, ko options.KeyOpts, regOpts options.RegistryOptions, imageRef string, certPath string, certChainPath string, +func AttestCmd(ctx context.Context, ko sign.KeyOpts, regOpts options.RegistryOptions, imageRef string, certPath string, certChainPath string, noUpload bool, predicatePath string, force bool, predicateType string, replace bool, timeout time.Duration) error { // A key file or token is required unless we're in experimental mode! if options.EnableExperimental() { diff --git a/cmd/cosign/cli/fulcio/fulcio.go b/cmd/cosign/cli/fulcio/fulcio.go index eed76b39496..d7eedabafb8 100644 --- a/cmd/cosign/cli/fulcio/fulcio.go +++ b/cmd/cosign/cli/fulcio/fulcio.go @@ -30,9 +30,8 @@ import ( "golang.org/x/term" "github.com/sigstore/cosign/cmd/cosign/cli/fulcio/fulcioroots" - "github.com/sigstore/cosign/cmd/cosign/cli/options" + clioptions "github.com/sigstore/cosign/cmd/cosign/cli/options" "github.com/sigstore/cosign/pkg/cosign" - "github.com/sigstore/cosign/pkg/providers" "github.com/sigstore/fulcio/pkg/api" "github.com/sigstore/sigstore/pkg/oauthflow" "github.com/sigstore/sigstore/pkg/signature" @@ -111,21 +110,7 @@ type Signer struct { *signature.ECDSASignerVerifier } -func NewSigner(ctx context.Context, ko options.KeyOpts) (*Signer, error) { - fClient, err := NewClient(ko.FulcioURL) - if err != nil { - return nil, errors.Wrap(err, "creating Fulcio client") - } - - idToken := ko.IDToken - // If token is not set in the options, get one from the provders - if idToken == "" && providers.Enabled(ctx) { - idToken, err = providers.Provide(ctx, "sigstore") - if err != nil { - return nil, errors.Wrap(err, "fetching ambient OIDC credentials") - } - } - +func NewSigner(ctx context.Context, idToken, oidcIssuer, oidcClientID, oidcClientSecret, oidcRedirectURL string, fClient api.Client) (*Signer, error) { priv, err := cosign.GeneratePrivateKey() if err != nil { return nil, errors.Wrap(err, "generating cert") @@ -146,7 +131,7 @@ func NewSigner(ctx context.Context, ko options.KeyOpts) (*Signer, error) { default: flow = FlowNormal } - Resp, err := GetCert(ctx, priv, idToken, flow, ko.OIDCIssuer, ko.OIDCClientID, ko.OIDCClientSecret, ko.OIDCRedirectURL, fClient) // TODO, use the chain. + Resp, err := GetCert(ctx, priv, idToken, flow, oidcIssuer, oidcClientID, oidcClientSecret, oidcRedirectURL, fClient) // TODO, use the chain. if err != nil { return nil, errors.Wrap(err, "retrieving cert") } @@ -181,6 +166,6 @@ func NewClient(fulcioURL string) (api.Client, error) { if err != nil { return nil, err } - fClient := api.NewClient(fulcioServer, api.WithUserAgent(options.UserAgent())) + fClient := api.NewClient(fulcioServer, api.WithUserAgent(clioptions.UserAgent())) return fClient, nil } diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go b/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go index c2905ec12a9..3687f5db01f 100644 --- a/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go +++ b/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go @@ -24,11 +24,11 @@ import ( "github.com/sigstore/cosign/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/cmd/cosign/cli/fulcio/fulcioverifier/ctl" - "github.com/sigstore/cosign/cmd/cosign/cli/options" + "github.com/sigstore/fulcio/pkg/api" ) -func NewSigner(ctx context.Context, ko options.KeyOpts) (*fulcio.Signer, error) { - fs, err := fulcio.NewSigner(ctx, ko) +func NewSigner(ctx context.Context, idToken, oidcIssuer, oidcClientID, oidcClientSecret, oidcRedirectURL string, fClient api.Client) (*fulcio.Signer, error) { + fs, err := fulcio.NewSigner(ctx, idToken, oidcIssuer, oidcClientID, oidcClientSecret, oidcRedirectURL, fClient) if err != nil { return nil, err } diff --git a/cmd/cosign/cli/options/key.go b/cmd/cosign/cli/options/key.go deleted file mode 100644 index db36e9235b7..00000000000 --- a/cmd/cosign/cli/options/key.go +++ /dev/null @@ -1,37 +0,0 @@ -// -// 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 options - -import "github.com/sigstore/cosign/pkg/cosign" - -type KeyOpts struct { - Sk bool - Slot string - KeyRef string - FulcioURL string - RekorURL string - IDToken string - PassFunc cosign.PassFunc - OIDCIssuer string - OIDCClientID string - OIDCClientSecret string - OIDCRedirectURL string - BundlePath string - - // Modeled after InsecureSkipVerify in tls.Config, this disables - // verifying the SCT. - InsecureSkipFulcioVerify bool -} diff --git a/cmd/cosign/cli/policy_init.go b/cmd/cosign/cli/policy_init.go index 89e100c36d0..9e9c0f0bae0 100644 --- a/cmd/cosign/cli/policy_init.go +++ b/cmd/cosign/cli/policy_init.go @@ -179,7 +179,7 @@ func signPolicy() *cobra.Command { if err != nil { return err } - sv, err := sign.SignerFromKeyOpts(ctx, "", "", options.KeyOpts{ + sv, err := sign.SignerFromKeyOpts(ctx, "", "", sign.KeyOpts{ FulcioURL: o.Fulcio.URL, IDToken: o.Fulcio.IdentityToken, InsecureSkipFulcioVerify: o.Fulcio.InsecureSkipFulcioVerify, diff --git a/cmd/cosign/cli/sign.go b/cmd/cosign/cli/sign.go index f0f2ea19129..a039fe3402a 100644 --- a/cmd/cosign/cli/sign.go +++ b/cmd/cosign/cli/sign.go @@ -82,7 +82,7 @@ func Sign() *cobra.Command { if err != nil { return err } - ko := options.KeyOpts{ + ko := sign.KeyOpts{ KeyRef: o.Key, PassFunc: generate.GetPass, Sk: o.SecurityKey.Use, diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go index adff00bad11..44da104baf8 100644 --- a/cmd/cosign/cli/sign/sign.go +++ b/cmd/cosign/cli/sign/sign.go @@ -47,6 +47,7 @@ import ( "github.com/sigstore/cosign/pkg/oci/mutate" ociremote "github.com/sigstore/cosign/pkg/oci/remote" "github.com/sigstore/cosign/pkg/oci/walk" + providers "github.com/sigstore/cosign/pkg/providers/all" sigs "github.com/sigstore/cosign/pkg/signature" "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" @@ -92,7 +93,7 @@ func GetAttachedImageRef(ref name.Reference, attachment string, opts ...ociremot } // nolint -func SignCmd(ro *options.RootOptions, ko options.KeyOpts, regOpts options.RegistryOptions, annotations map[string]interface{}, +func SignCmd(ro *options.RootOptions, ko KeyOpts, regOpts options.RegistryOptions, annotations map[string]interface{}, imgs []string, certPath string, certChainPath string, upload bool, outputSignature, outputCertificate string, payloadPath string, force bool, recursive bool, attachment string) error { if options.EnableExperimental() { @@ -182,7 +183,7 @@ func SignCmd(ro *options.RootOptions, ko options.KeyOpts, regOpts options.Regist return nil } -func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko options.KeyOpts, +func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko KeyOpts, regOpts options.RegistryOptions, annotations map[string]interface{}, upload bool, outputSignature, outputCertificate string, force bool, recursive bool, dd mutate.DupeDetector, sv *SignerVerifier, se oci.SignedEntity) error { var err error @@ -435,18 +436,29 @@ func signerFromKeyRef(ctx context.Context, certPath, certChainPath, keyRef strin return certSigner, nil } -func keylessSigner(ctx context.Context, ko options.KeyOpts) (*SignerVerifier, error) { - var ( - k *fulcio.Signer - err error - ) +func keylessSigner(ctx context.Context, ko KeyOpts) (*SignerVerifier, error) { + fClient, err := fulcio.NewClient(ko.FulcioURL) + if err != nil { + return nil, errors.Wrap(err, "creating Fulcio client") + } + + tok := ko.IDToken + // If token is not set in the options, get one from the provders + if tok == "" && providers.Enabled(ctx) { + tok, err = providers.Provide(ctx, "sigstore") + if err != nil { + return nil, errors.Wrap(err, "fetching ambient OIDC credentials") + } + } + + var k *fulcio.Signer if ko.InsecureSkipFulcioVerify { - if k, err = fulcio.NewSigner(ctx, ko); err != nil { + if k, err = fulcio.NewSigner(ctx, tok, ko.OIDCIssuer, ko.OIDCClientID, ko.OIDCClientSecret, ko.OIDCRedirectURL, fClient); err != nil { return nil, errors.Wrap(err, "getting key from Fulcio") } } else { - if k, err = fulcioverifier.NewSigner(ctx, ko); err != nil { + if k, err = fulcioverifier.NewSigner(ctx, tok, ko.OIDCIssuer, ko.OIDCClientID, ko.OIDCClientSecret, ko.OIDCRedirectURL, fClient); err != nil { return nil, errors.Wrap(err, "getting key from Fulcio") } } @@ -458,7 +470,7 @@ func keylessSigner(ctx context.Context, ko options.KeyOpts) (*SignerVerifier, er }, nil } -func SignerFromKeyOpts(ctx context.Context, certPath string, certChainPath string, ko options.KeyOpts) (*SignerVerifier, error) { +func SignerFromKeyOpts(ctx context.Context, certPath string, certChainPath string, ko KeyOpts) (*SignerVerifier, error) { if ko.Sk { return signerFromSecurityKey(ko.Slot) } diff --git a/cmd/cosign/cli/sign/sign_blob.go b/cmd/cosign/cli/sign/sign_blob.go index 401922b21d1..d21799ff9f3 100644 --- a/cmd/cosign/cli/sign/sign_blob.go +++ b/cmd/cosign/cli/sign/sign_blob.go @@ -34,8 +34,27 @@ import ( signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" ) +type KeyOpts struct { + Sk bool + Slot string + KeyRef string + FulcioURL string + RekorURL string + IDToken string + PassFunc cosign.PassFunc + OIDCIssuer string + OIDCClientID string + OIDCClientSecret string + OIDCRedirectURL string + BundlePath string + + // Modeled after InsecureSkipVerify in tls.Config, this disables + // verifying the SCT. + InsecureSkipFulcioVerify bool +} + // nolint -func SignBlobCmd(ro *options.RootOptions, ko options.KeyOpts, regOpts options.RegistryOptions, payloadPath string, b64 bool, outputSignature string, outputCertificate string) ([]byte, error) { +func SignBlobCmd(ro *options.RootOptions, ko KeyOpts, regOpts options.RegistryOptions, payloadPath string, b64 bool, outputSignature string, outputCertificate string) ([]byte, error) { var payload []byte var err error var rekorBytes []byte diff --git a/cmd/cosign/cli/sign/sign_test.go b/cmd/cosign/cli/sign/sign_test.go index bc0eabcf8a8..bb7255fd361 100644 --- a/cmd/cosign/cli/sign/sign_test.go +++ b/cmd/cosign/cli/sign/sign_test.go @@ -110,7 +110,7 @@ func generateCertificateFiles(t *testing.T, tmpDir string, pf cosign.PassFunc) ( func TestSignCmdLocalKeyAndSk(t *testing.T) { ro := &options.RootOptions{Timeout: options.DefaultTimeout} - for _, ko := range []options.KeyOpts{ + for _, ko := range []KeyOpts{ // local and sk keys { KeyRef: "testLocalPath", diff --git a/cmd/cosign/cli/signblob.go b/cmd/cosign/cli/signblob.go index e5e14b2cf08..fcb894c8369 100644 --- a/cmd/cosign/cli/signblob.go +++ b/cmd/cosign/cli/signblob.go @@ -68,7 +68,7 @@ func SignBlob() *cobra.Command { if err != nil { return err } - ko := options.KeyOpts{ + ko := sign.KeyOpts{ KeyRef: o.Key, PassFunc: generate.GetPass, Sk: o.SecurityKey.Use, diff --git a/cmd/cosign/cli/verify.go b/cmd/cosign/cli/verify.go index 6f180e7c549..dc75b8e3f45 100644 --- a/cmd/cosign/cli/verify.go +++ b/cmd/cosign/cli/verify.go @@ -20,6 +20,7 @@ import ( "github.com/spf13/cobra" "github.com/sigstore/cosign/cmd/cosign/cli/options" + "github.com/sigstore/cosign/cmd/cosign/cli/sign" "github.com/sigstore/cosign/cmd/cosign/cli/verify" ) @@ -248,7 +249,7 @@ The blob may be specified as a path to a file or - for stdin.`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - ko := options.KeyOpts{ + ko := sign.KeyOpts{ KeyRef: o.Key, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index 77bef0e92e1..0341ed0a025 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -35,6 +35,7 @@ import ( "github.com/sigstore/cosign/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/cmd/cosign/cli/options" "github.com/sigstore/cosign/cmd/cosign/cli/rekor" + "github.com/sigstore/cosign/cmd/cosign/cli/sign" "github.com/sigstore/cosign/pkg/blob" "github.com/sigstore/cosign/pkg/cosign" "github.com/sigstore/cosign/pkg/cosign/pivkey" @@ -60,7 +61,7 @@ func isb64(data []byte) bool { } // nolint -func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail, +func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, certOidcIssuer, certChain, sigRef, blobRef string, enforceSCT bool) error { var verifier signature.Verifier var cert *x509.Certificate @@ -185,7 +186,7 @@ func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail, return nil } -func verifySigByUUID(ctx context.Context, ko options.KeyOpts, rClient *client.Rekor, certEmail, certOidcIssuer, sig, b64sig string, +func verifySigByUUID(ctx context.Context, ko sign.KeyOpts, rClient *client.Rekor, certEmail, certOidcIssuer, sig, b64sig string, uuids []string, blobBytes []byte, enforceSCT bool) error { var validSigExists bool for _, u := range uuids { @@ -288,7 +289,7 @@ func payloadBytes(blobRef string) ([]byte, error) { return blobBytes, nil } -func verifyRekorEntry(ctx context.Context, ko options.KeyOpts, e *models.LogEntryAnon, pubKey signature.Verifier, cert *x509.Certificate, b64sig string, blobBytes []byte) error { +func verifyRekorEntry(ctx context.Context, ko sign.KeyOpts, e *models.LogEntryAnon, pubKey signature.Verifier, cert *x509.Certificate, b64sig string, blobBytes []byte) error { // If we have a bundle with a rekor entry, let's first try to verify offline if ko.BundlePath != "" { if err := verifyRekorBundle(ctx, ko.BundlePath, cert); err == nil { diff --git a/test/e2e_test.go b/test/e2e_test.go index 75c7701f864..1302b9d8478 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -125,7 +125,7 @@ func TestSignVerify(t *testing.T) { mustErr(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName), t) // Now sign the image - ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + ko := sign.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} must(sign.SignCmd(ro, ko, options.RegistryOptions{}, nil, []string{imgName}, "", "", true, "", "", "", false, false, ""), t) // Now verify and download should work! @@ -160,7 +160,7 @@ func TestSignVerifyClean(t *testing.T) { ctx := context.Background() // Now sign the image - ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + ko := sign.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} must(sign.SignCmd(ro, ko, options.RegistryOptions{}, nil, []string{imgName}, "", "", true, "", "", "", false, false, ""), t) // Now verify and download should work! @@ -189,7 +189,7 @@ func TestImportSignVerifyClean(t *testing.T) { ctx := context.Background() // Now sign the image - ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + ko := sign.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} must(sign.SignCmd(ro, ko, options.RegistryOptions{}, nil, []string{imgName}, "", "", true, "", "", "", false, false, ""), t) // Now verify and download should work! @@ -232,7 +232,7 @@ func TestAttestVerify(t *testing.T) { } // Now attest the image - ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + ko := sign.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} must(attest.AttestCmd(ctx, ko, options.RegistryOptions{}, imgName, "", "", false, slsaAttestationPath, false, "slsaprovenance", false, 30*time.Second), t) @@ -273,7 +273,7 @@ func TestAttestationReplace(t *testing.T) { defer cleanup() _, privKeyPath, _ := keypair(t, td) - ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + ko := sign.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} ctx := context.Background() @@ -327,7 +327,7 @@ func TestRekorBundle(t *testing.T) { _, privKeyPath, pubKeyPath := keypair(t, td) - ko := options.KeyOpts{ + ko := sign.KeyOpts{ KeyRef: privKeyPath, PassFunc: passFunc, RekorURL: rekorURL, @@ -363,7 +363,7 @@ func TestDuplicateSign(t *testing.T) { mustErr(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName), t) // Now sign the image - ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + ko := sign.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} must(sign.SignCmd(ro, ko, options.RegistryOptions{}, nil, []string{imgName}, "", "", true, "", "", "", false, false, ""), t) // Now verify and download should work! @@ -460,7 +460,7 @@ func TestMultipleSignatures(t *testing.T) { mustErr(verify(pub2, imgName, true, nil, ""), t) // Now sign the image with one key - ko := options.KeyOpts{KeyRef: priv1, PassFunc: passFunc} + ko := sign.KeyOpts{KeyRef: priv1, PassFunc: passFunc} must(sign.SignCmd(ro, ko, options.RegistryOptions{}, nil, []string{imgName}, "", "", true, "", "", "", false, false, ""), t) // Now verify should work with that one, but not the other must(verify(pub1, imgName, true, nil, ""), t) @@ -494,10 +494,10 @@ func TestSignBlob(t *testing.T) { ctx := context.Background() - ko1 := options.KeyOpts{ + ko1 := sign.KeyOpts{ KeyRef: pubKeyPath1, } - ko2 := options.KeyOpts{ + ko2 := sign.KeyOpts{ KeyRef: pubKeyPath2, } // Verify should fail on a bad input @@ -505,7 +505,7 @@ func TestSignBlob(t *testing.T) { mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, "badsig", blob, false), t) // Now sign the blob with one key - ko := options.KeyOpts{ + ko := sign.KeyOpts{ KeyRef: privKeyPath1, PassFunc: passFunc, } @@ -535,7 +535,7 @@ func TestSignBlobBundle(t *testing.T) { ctx := context.Background() - ko1 := options.KeyOpts{ + ko1 := sign.KeyOpts{ KeyRef: pubKeyPath1, BundlePath: bundlePath, } @@ -543,7 +543,7 @@ func TestSignBlobBundle(t *testing.T) { mustErr(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", blob, false), t) // Now sign the blob with one key - ko := options.KeyOpts{ + ko := sign.KeyOpts{ KeyRef: privKeyPath1, PassFunc: passFunc, BundlePath: bundlePath, @@ -849,7 +849,7 @@ func TestSaveLoad(t *testing.T) { ctx := context.Background() // Now sign the image and verify it - ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + ko := sign.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} must(sign.SignCmd(ro, ko, options.RegistryOptions{}, nil, []string{imgName}, "", "", true, "", "", "", false, false, ""), t) must(verify(pubKeyPath, imgName, true, nil, ""), t) @@ -882,7 +882,7 @@ func TestSaveLoadAttestation(t *testing.T) { ctx := context.Background() // Now sign the image and verify it - ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + ko := sign.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} must(sign.SignCmd(ro, ko, options.RegistryOptions{}, nil, []string{imgName}, "", "", true, "", "", "", false, false, ""), t) must(verify(pubKeyPath, imgName, true, nil, ""), t) @@ -894,7 +894,7 @@ func TestSaveLoadAttestation(t *testing.T) { } // Now attest the image - ko = options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + ko = sign.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} must(attest.AttestCmd(ctx, ko, options.RegistryOptions{}, imgName, "", "", false, slsaAttestationPath, false, "custom", false, 30*time.Second), t) @@ -971,7 +971,7 @@ func TestAttachSBOM(t *testing.T) { mustErr(verify(pubKeyPath2, imgName, true, nil, "sbom"), t) // Now sign the sbom with one key - ko1 := options.KeyOpts{KeyRef: privKeyPath1, PassFunc: passFunc} + ko1 := sign.KeyOpts{KeyRef: privKeyPath1, PassFunc: passFunc} must(sign.SignCmd(ro, ko1, options.RegistryOptions{}, nil, []string{imgName}, "", "", true, "", "", "", false, false, "sbom"), t) // Now verify should work with that one, but not the other @@ -1004,7 +1004,7 @@ func TestTlog(t *testing.T) { mustErr(verify(pubKeyPath, imgName, true, nil, ""), t) // Now sign the image without the tlog - ko := options.KeyOpts{ + ko := sign.KeyOpts{ KeyRef: privKeyPath, PassFunc: passFunc, RekorURL: rekorURL, @@ -1182,7 +1182,7 @@ func TestInvalidBundle(t *testing.T) { // (we're just using it for its bundle) defer setenv(t, options.ExperimentalEnv, "1")() remoteOpts := ociremote.WithRemoteOptions(registryClientOpts(ctx)...) - ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc, RekorURL: rekorURL} + ko := sign.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc, RekorURL: rekorURL} regOpts := options.RegistryOptions{} must(sign.SignCmd(ro, ko, regOpts, nil, []string{img1}, "", "", true, "", "", "", true, false, ""), t) From e4c68cba2ffd2e3e84f51a261698769f5ce55475 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Apr 2022 11:39:02 -0500 Subject: [PATCH 45/53] Bump github.com/xanzy/go-gitlab from 0.63.0 to 0.64.0 (#1799) Bumps [github.com/xanzy/go-gitlab](https://github.com/xanzy/go-gitlab) from 0.63.0 to 0.64.0. - [Release notes](https://github.com/xanzy/go-gitlab/releases) - [Changelog](https://github.com/xanzy/go-gitlab/blob/master/releases_test.go) - [Commits](https://github.com/xanzy/go-gitlab/compare/v0.63.0...v0.64.0) --- updated-dependencies: - dependency-name: github.com/xanzy/go-gitlab dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index eaed295cbb3..e0277800c2b 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,7 @@ require ( github.com/stretchr/testify v1.7.1 github.com/theupdateframework/go-tuf v0.0.0-20220211205608-f0c3294f63b9 github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 - github.com/xanzy/go-gitlab v0.63.0 + github.com/xanzy/go-gitlab v0.64.0 golang.org/x/net v0.0.0-20220412020605-290c469a71a5 golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad diff --git a/go.sum b/go.sum index 4e331a4adaa..b3179bdb3b2 100644 --- a/go.sum +++ b/go.sum @@ -2228,8 +2228,8 @@ github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr github.com/withfig/autocomplete-tools/packages/cobra v0.0.0-20220122124547-31d3821a6898 h1:2Z+iziYPiyWk5hVJ3EYLn/i33Tj5ukytaJA0Th9tbgc= github.com/withfig/autocomplete-tools/packages/cobra v0.0.0-20220122124547-31d3821a6898/go.mod h1:cKObXQ6PVFO7bHUd5jpApXvMIt55Ewz7UdMiC05ONxI= github.com/xanzy/go-gitlab v0.31.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= -github.com/xanzy/go-gitlab v0.63.0 h1:a9fXpKWykUS6dowapFej/2Wjf4aOAEFC1q2ZIcz4IpI= -github.com/xanzy/go-gitlab v0.63.0/go.mod h1:F0QEXwmqiBUxCgJm8fE9S+1veX4XC9Z4cfaAbqwk4YM= +github.com/xanzy/go-gitlab v0.64.0 h1:rMgQdW9S1w3qvNAH2LYpFd2xh7KNLk+JWJd7sorNuTc= +github.com/xanzy/go-gitlab v0.64.0/go.mod h1:F0QEXwmqiBUxCgJm8fE9S+1veX4XC9Z4cfaAbqwk4YM= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= From 133ce881ae7b16a92ad8321bbb688c29d7be6d91 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Apr 2022 11:39:16 -0500 Subject: [PATCH 46/53] Bump google.golang.org/grpc from 1.45.0 to 1.46.0 (#1800) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.45.0 to 1.46.0. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.45.0...v1.46.0) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index e0277800c2b..242179d2153 100644 --- a/go.mod +++ b/go.mod @@ -97,7 +97,7 @@ require ( go.uber.org/atomic v1.9.0 go.uber.org/zap v1.21.0 golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 - google.golang.org/grpc v1.45.0 + google.golang.org/grpc v1.46.0 google.golang.org/protobuf v1.28.0 k8s.io/code-generator v0.23.6 k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf @@ -172,7 +172,7 @@ require ( github.com/dustin/go-humanize v1.0.0 // indirect github.com/emicklei/go-restful v2.9.5+incompatible // indirect github.com/emicklei/proto v1.6.15 // indirect - github.com/envoyproxy/go-control-plane v0.10.1 // indirect + github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 // indirect github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect diff --git a/go.sum b/go.sum index b3179bdb3b2..51de62677c0 100644 --- a/go.sum +++ b/go.sum @@ -714,8 +714,9 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/go-control-plane v0.10.1 h1:cgDRLG7bs59Zd+apAWuzLQL95obVYAymNJek76W3mgw= github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 h1:xvqufLtNVwAhN8NMyWklVgxnWohi+wtMGQMhtxexlm0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.0.14/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.3.0-java/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= @@ -3195,8 +3196,9 @@ google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzI google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0 h1:oCjezcn6g6A75TGoKYBPgKmVBLexhYLM6MebdrPApP8= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/grpc/examples v0.0.0-20201130180447-c456688b1860/go.mod h1:Ly7ZA/ARzg8fnPU9TyZIxoz33sEUuWX7txiqs8lPTgE= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= From 376b1b70c25300529ec29924752f3251d2e82841 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Apr 2022 13:36:45 -0500 Subject: [PATCH 47/53] Bump google-github-actions/auth from 0.7.0 to 0.7.1 (#1801) Bumps [google-github-actions/auth](https://github.com/google-github-actions/auth) from 0.7.0 to 0.7.1. - [Release notes](https://github.com/google-github-actions/auth/releases) - [Changelog](https://github.com/google-github-actions/auth/blob/main/CHANGELOG.md) - [Commits](https://github.com/google-github-actions/auth/compare/50dbfd0907520dcccbd51e965728eb32e592b8fa...b258a9f230b36c9fa86dfaa43d1906bd76399edb) --- updated-dependencies: - dependency-name: google-github-actions/auth dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 7fe33260229..45f60548dbe 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -48,7 +48,7 @@ jobs: run: go get golang.org/x/tools/cmd/goimports - name: Set up Cloud SDK - uses: google-github-actions/auth@50dbfd0907520dcccbd51e965728eb32e592b8fa # v0.6.0 + uses: google-github-actions/auth@b258a9f230b36c9fa86dfaa43d1906bd76399edb # v0.6.0 with: workload_identity_provider: 'projects/498091336538/locations/global/workloadIdentityPools/githubactions/providers/sigstore-cosign' service_account: 'github-actions@projectsigstore.iam.gserviceaccount.com' From cba2cfb8cef0a36e79f51ef60c2d42597f05a670 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Apr 2022 08:56:07 -0500 Subject: [PATCH 48/53] Bump github.com/hashicorp/go-retryablehttp from 0.7.0 to 0.7.1 (#1758) * Bump github.com/hashicorp/go-retryablehttp from 0.7.0 to 0.7.1 Bumps [github.com/hashicorp/go-retryablehttp](https://github.com/hashicorp/go-retryablehttp) from 0.7.0 to 0.7.1. - [Release notes](https://github.com/hashicorp/go-retryablehttp/releases) - [Commits](https://github.com/hashicorp/go-retryablehttp/compare/v0.7.0...v0.7.1) --- updated-dependencies: - dependency-name: github.com/hashicorp/go-retryablehttp dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * update codegen Signed-off-by: cpanato Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: cpanato --- go.mod | 2 +- go.sum | 3 +- .../hashicorp/go-retryablehttp/README.md | 19 +++++ .../hashicorp/go-retryablehttp/client.go | 85 ++++++++++++++----- 4 files changed, 85 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index 242179d2153..badbca06cd7 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/google/go-github/v42 v42.0.0 github.com/google/trillian v1.4.0 github.com/hashicorp/go-cleanhttp v0.5.2 - github.com/hashicorp/go-retryablehttp v0.7.0 + github.com/hashicorp/go-retryablehttp v0.7.1 github.com/hashicorp/go-rootcerts v1.0.2 github.com/hashicorp/go-secure-stdlib/parseutil v0.1.4 github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 diff --git a/go.sum b/go.sum index 51de62677c0..ba585bdd774 100644 --- a/go.sum +++ b/go.sum @@ -1326,8 +1326,9 @@ github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es github.com/hashicorp/go-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= -github.com/hashicorp/go-retryablehttp v0.7.0 h1:eu1EI/mbirUgP5C8hVsTNaGZreBDlYiwC1FZWkvQPQ4= github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ= +github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= diff --git a/third_party/VENDOR-LICENSE/github.com/hashicorp/go-retryablehttp/README.md b/third_party/VENDOR-LICENSE/github.com/hashicorp/go-retryablehttp/README.md index 8943becf19b..09f5eaf2217 100644 --- a/third_party/VENDOR-LICENSE/github.com/hashicorp/go-retryablehttp/README.md +++ b/third_party/VENDOR-LICENSE/github.com/hashicorp/go-retryablehttp/README.md @@ -45,6 +45,25 @@ The returned response object is an `*http.Response`, the same thing you would usually get from `net/http`. Had the request failed one or more times, the above call would block and retry with exponential backoff. +## Retrying cases that fail after a seeming success + +It's possible for a request to succeed in the sense that the expected response headers are received, but then to encounter network-level errors while reading the response body. In go-retryablehttp's most basic usage, this error would not be retryable, due to the out-of-band handling of the response body. In some cases it may be desirable to handle the response body as part of the retryable operation. + +A toy example (which will retry the full request and succeed on the second attempt) is shown below: + +```go +c := retryablehttp.NewClient() +r := retryablehttp.NewRequest("GET", "://foo", nil) +handlerShouldRetry := true +r.SetResponseHandler(func(*http.Response) error { + if !handlerShouldRetry { + return nil + } + handlerShouldRetry = false + return errors.New("retryable error") +}) +``` + ## Getting a stdlib `*http.Client` with retries It's possible to convert a `*retryablehttp.Client` directly to a `*http.Client`. diff --git a/third_party/VENDOR-LICENSE/github.com/hashicorp/go-retryablehttp/client.go b/third_party/VENDOR-LICENSE/github.com/hashicorp/go-retryablehttp/client.go index adbdd92e3ba..57116e96072 100644 --- a/third_party/VENDOR-LICENSE/github.com/hashicorp/go-retryablehttp/client.go +++ b/third_party/VENDOR-LICENSE/github.com/hashicorp/go-retryablehttp/client.go @@ -69,11 +69,21 @@ var ( // scheme specified in the URL is invalid. This error isn't typed // specifically so we resort to matching on the error string. schemeErrorRe = regexp.MustCompile(`unsupported protocol scheme`) + + // A regular expression to match the error returned by net/http when the + // TLS certificate is not trusted. This error isn't typed + // specifically so we resort to matching on the error string. + notTrustedErrorRe = regexp.MustCompile(`certificate is not trusted`) ) // ReaderFunc is the type of function that can be given natively to NewRequest type ReaderFunc func() (io.Reader, error) +// ResponseHandlerFunc is a type of function that takes in a Response, and does something with it. +// It only runs if the initial part of the request was successful. +// If an error is returned, the client's retry policy will be used to determine whether to retry the whole request. +type ResponseHandlerFunc func(*http.Response) error + // LenReader is an interface implemented by many in-memory io.Reader's. Used // for automatically sending the right Content-Length header when possible. type LenReader interface { @@ -86,6 +96,8 @@ type Request struct { // used to rewind the request data in between retries. body ReaderFunc + responseHandler ResponseHandlerFunc + // Embed an HTTP request directly. This makes a *Request act exactly // like an *http.Request so that all meta methods are supported. *http.Request @@ -94,8 +106,16 @@ type Request struct { // WithContext returns wrapped Request with a shallow copy of underlying *http.Request // with its context changed to ctx. The provided ctx must be non-nil. func (r *Request) WithContext(ctx context.Context) *Request { - r.Request = r.Request.WithContext(ctx) - return r + return &Request{ + body: r.body, + responseHandler: r.responseHandler, + Request: r.Request.WithContext(ctx), + } +} + +// SetResponseHandler allows setting the response handler. +func (r *Request) SetResponseHandler(fn ResponseHandlerFunc) { + r.responseHandler = fn } // BodyBytes allows accessing the request body. It is an analogue to @@ -252,23 +272,31 @@ func FromRequest(r *http.Request) (*Request, error) { return nil, err } // Could assert contentLength == r.ContentLength - return &Request{bodyReader, r}, nil + return &Request{body: bodyReader, Request: r}, nil } // NewRequest creates a new wrapped request. func NewRequest(method, url string, rawBody interface{}) (*Request, error) { + return NewRequestWithContext(context.Background(), method, url, rawBody) +} + +// NewRequestWithContext creates a new wrapped request with the provided context. +// +// The context controls the entire lifetime of a request and its response: +// obtaining a connection, sending the request, and reading the response headers and body. +func NewRequestWithContext(ctx context.Context, method, url string, rawBody interface{}) (*Request, error) { bodyReader, contentLength, err := getBodyReaderAndContentLength(rawBody) if err != nil { return nil, err } - httpReq, err := http.NewRequest(method, url, nil) + httpReq, err := http.NewRequestWithContext(ctx, method, url, nil) if err != nil { return nil, err } httpReq.ContentLength = contentLength - return &Request{bodyReader, httpReq}, nil + return &Request{body: bodyReader, Request: httpReq}, nil } // Logger interface allows to use other loggers than @@ -435,6 +463,9 @@ func baseRetryPolicy(resp *http.Response, err error) (bool, error) { } // Don't retry if the error was due to TLS cert verification failure. + if notTrustedErrorRe.MatchString(v.Error()) { + return false, v + } if _, ok := v.Err.(x509.UnknownAuthorityError); ok { return false, v } @@ -455,7 +486,7 @@ func baseRetryPolicy(resp *http.Response, err error) (bool, error) { // the server time to recover, as 500's are typically not permanent // errors and may relate to outages on the server side. This will catch // invalid response codes as well, like 0 and 999. - if resp.StatusCode == 0 || (resp.StatusCode >= 500 && resp.StatusCode != 501) { + if resp.StatusCode == 0 || (resp.StatusCode >= 500 && resp.StatusCode != http.StatusNotImplemented) { return true, fmt.Errorf("unexpected HTTP status %s", resp.Status) } @@ -555,13 +586,12 @@ func (c *Client) Do(req *Request) (*http.Response, error) { var resp *http.Response var attempt int var shouldRetry bool - var doErr, checkErr error + var doErr, respErr, checkErr error for i := 0; ; i++ { + doErr, respErr = nil, nil attempt++ - var code int // HTTP response code - // Always rewind the request body when non-nil. if req.body != nil { body, err := req.body() @@ -589,19 +619,24 @@ func (c *Client) Do(req *Request) (*http.Response, error) { // Attempt the request resp, doErr = c.HTTPClient.Do(req.Request) - if resp != nil { - code = resp.StatusCode - } // Check if we should continue with retries. shouldRetry, checkErr = c.CheckRetry(req.Context(), resp, doErr) + if !shouldRetry && doErr == nil && req.responseHandler != nil { + respErr = req.responseHandler(resp) + shouldRetry, checkErr = c.CheckRetry(req.Context(), resp, respErr) + } - if doErr != nil { + err := doErr + if respErr != nil { + err = respErr + } + if err != nil { switch v := logger.(type) { case LeveledLogger: - v.Error("request failed", "error", doErr, "method", req.Method, "url", req.URL) + v.Error("request failed", "error", err, "method", req.Method, "url", req.URL) case Logger: - v.Printf("[ERR] %s %s request failed: %v", req.Method, req.URL, doErr) + v.Printf("[ERR] %s %s request failed: %v", req.Method, req.URL, err) } } else { // Call this here to maintain the behavior of logging all requests, @@ -636,11 +671,11 @@ func (c *Client) Do(req *Request) (*http.Response, error) { } wait := c.Backoff(c.RetryWaitMin, c.RetryWaitMax, i, resp) - desc := fmt.Sprintf("%s %s", req.Method, req.URL) - if code > 0 { - desc = fmt.Sprintf("%s (status: %d)", desc, code) - } if logger != nil { + desc := fmt.Sprintf("%s %s", req.Method, req.URL) + if resp != nil { + desc = fmt.Sprintf("%s (status: %d)", desc, resp.StatusCode) + } switch v := logger.(type) { case LeveledLogger: v.Debug("retrying request", "request", desc, "timeout", wait, "remaining", remain) @@ -648,11 +683,13 @@ func (c *Client) Do(req *Request) (*http.Response, error) { v.Printf("[DEBUG] %s: retrying in %s (%d left)", desc, wait, remain) } } + timer := time.NewTimer(wait) select { case <-req.Context().Done(): + timer.Stop() c.HTTPClient.CloseIdleConnections() return nil, req.Context().Err() - case <-time.After(wait): + case <-timer.C: } // Make shallow copy of http Request so that we can modify its body @@ -662,15 +699,19 @@ func (c *Client) Do(req *Request) (*http.Response, error) { } // this is the closest we have to success criteria - if doErr == nil && checkErr == nil && !shouldRetry { + if doErr == nil && respErr == nil && checkErr == nil && !shouldRetry { return resp, nil } defer c.HTTPClient.CloseIdleConnections() - err := doErr + var err error if checkErr != nil { err = checkErr + } else if respErr != nil { + err = respErr + } else { + err = doErr } if c.ErrorHandler != nil { From db323cd8167e5f7c154732b158342819275247aa Mon Sep 17 00:00:00 2001 From: Hector Fernandez Date: Tue, 26 Apr 2022 17:26:06 +0300 Subject: [PATCH 49/53] cosigned: Unify cue data and policy before evaluating it (#1793) * chore: update cue policy evaluation Signed-off-by: hectorj2f * chore: change cue policy for the cip Signed-off-by: hectorj2f * chore: avoid using names with hyphens Signed-off-by: hectorj2f * test: add unit tests for the eval policy func Signed-off-by: hectorj2f * test: delete job before creating it Signed-off-by: hectorj2f * test: add statement to check the length of a struct Signed-off-by: hectorj2f * test: add more unit tests for eval policy Signed-off-by: hectorj2f * fix: wrong redirected file directory Signed-off-by: hectorj2f --- .../kubernetes/webhook/validator_test.go | 4 +- pkg/policy/eval.go | 18 ++++- pkg/policy/eval_test.go | 48 ++++++++++++- ..._cluster_image_policy_with_attestations.sh | 14 ++-- ...s-two-signatures-and-two-attestations.yaml | 69 ++++++------------- 5 files changed, 90 insertions(+), 63 deletions(-) diff --git a/pkg/cosign/kubernetes/webhook/validator_test.go b/pkg/cosign/kubernetes/webhook/validator_test.go index 7b7ea96513b..4dedd275ac5 100644 --- a/pkg/cosign/kubernetes/webhook/validator_test.go +++ b/pkg/cosign/kubernetes/webhook/validator_test.go @@ -381,10 +381,10 @@ UoJou2P8sbDxpLiE/v3yLw1/jyOrCPWYHWFXnyyeGlkgSVefG54tNoK7Uw== want: func() *apis.FieldError { var errs *apis.FieldError fe := apis.ErrGeneric("failed policy: cluster-image-policy-keyless", "image").ViaFieldIndex("initContainers", 0) - fe.Details = fmt.Sprintf("%s failed evaluating cue policy for ClusterImagePolicy : string literal not terminated", digest.String()) + fe.Details = fmt.Sprintf("%s failed evaluating cue policy for ClusterImagePolicy : failed to compile the cue policy with error: string literal not terminated", digest.String()) errs = errs.Also(fe) fe2 := apis.ErrGeneric("failed policy: cluster-image-policy-keyless", "image").ViaFieldIndex("containers", 0) - fe2.Details = fmt.Sprintf("%s failed evaluating cue policy for ClusterImagePolicy : string literal not terminated", digest.String()) + fe2.Details = fmt.Sprintf("%s failed evaluating cue policy for ClusterImagePolicy : failed to compile the cue policy with error: string literal not terminated", digest.String()) errs = errs.Also(fe2) return errs }(), diff --git a/pkg/policy/eval.go b/pkg/policy/eval.go index 9ef6e82fd02..27861b4329d 100644 --- a/pkg/policy/eval.go +++ b/pkg/policy/eval.go @@ -20,7 +20,6 @@ import ( "fmt" "cuelang.org/go/cue/cuecontext" - cuejson "cuelang.org/go/encoding/json" "knative.dev/pkg/logging" ) @@ -54,9 +53,22 @@ func EvaluatePolicyAgainstJSON(ctx context.Context, name, policyType string, pol // evaluateCue evaluates a cue policy `evaluator` against `attestation` func evaluateCue(ctx context.Context, attestation []byte, evaluator string) error { logging.FromContext(ctx).Infof("Evaluating attestation: %s", string(attestation)) + logging.FromContext(ctx).Infof("Evaluator: %s", evaluator) + cueCtx := cuecontext.New() - v := cueCtx.CompileString(evaluator) - return cuejson.Validate(attestation, v) + cueEvaluator := cueCtx.CompileString(evaluator) + if cueEvaluator.Err() != nil { + return fmt.Errorf("failed to compile the cue policy with error: %w", cueEvaluator.Err()) + } + cueAtt := cueCtx.CompileBytes(attestation) + if cueAtt.Err() != nil { + return fmt.Errorf("failed to compile the attestation data with error: %w", cueAtt.Err()) + } + result := cueEvaluator.Unify(cueAtt) + if err := result.Validate(); err != nil { + return fmt.Errorf("failed to evaluate the policy with error: %w", err) + } + return nil } // evaluateRego evaluates a rego policy `evaluator` against `attestation` diff --git a/pkg/policy/eval_test.go b/pkg/policy/eval_test.go index 9c1a3cae74f..c44729bc265 100644 --- a/pkg/policy/eval_test.go +++ b/pkg/policy/eval_test.go @@ -75,8 +75,7 @@ const ( } }` - // TODO(vaikas): Enable tests once we sort this out. - // cipAttestation = "authorityMatches:{\"key-att\":{\"signatures\":null,\"attestations\":{\"custom-match-predicate\":[{\"subject\":\"PLACEHOLDER\",\"issuer\":\"PLACEHOLDER\"}]}},\"key-signature\":{\"signatures\":[{\"subject\":\"PLACEHOLDER\",\"issuer\":\"PLACEHOLDER\"}],\"attestations\":null},\"keyless-att\":{\"signatures\":null,\"attestations\":{\"custom-keyless\":[{\"subject\":\"PLACEHOLDER\",\"issuer\":\"PLACEHOLDER\"}]}},\"keyless-signature\":{\"signatures\":[{\"subject\":\"PLACEHOLDER\",\"issuer\":\"PLACEHOLDER\"}],\"attestations\":null}}" + cipAttestation = "{\"authorityMatches\":{\"keyatt\":{\"signatures\":null,\"attestations\":{\"vuln-key\":[{\"subject\":\"PLACEHOLDER\",\"issuer\":\"PLACEHOLDER\"}]}},\"keysignature\":{\"signatures\":[{\"subject\":\"PLACEHOLDER\",\"issuer\":\"PLACEHOLDER\"}],\"attestations\":null},\"keylessatt\":{\"signatures\":null,\"attestations\":{\"custom-keyless\":[{\"subject\":\"PLACEHOLDER\",\"issuer\":\"PLACEHOLDER\"}]}}}}" ) func TestEvalPolicy(t *testing.T) { @@ -124,6 +123,51 @@ func TestEvalPolicy(t *testing.T) { policyType: "cue", policyFile: `predicateType: "cosign.sigstore.dev/attestation/vuln/v1" predicate: invocation: uri: "invocation.example.com/cosign-testing"`, + }, { + name: "cluster image policy main policy, checks out", + json: cipAttestation, + policyType: "cue", + policyFile: `package sigstore + import "struct" + import "list" + authorityMatches: { + keyatt: { + attestations: struct.MaxFields(1) & struct.MinFields(1) + }, + keysignature: { + signatures: list.MaxItems(1) & list.MinItems(1) + }, + keylessatt: { + attestations: struct.MaxFields(1) & struct.MinFields(1) + }, + keylesssignature: { + signatures: list.MaxItems(1) & list.MinItems(1) + } + }`, + }, { + name: "cluster image policy main policy, fails", + json: cipAttestation, + policyType: "cue", + wantErr: true, + wantErrSub: `failed evaluating cue policy for cluster image policy main policy, fails : failed to evaluate the policy with error: authorityMatches.keylessattMinAttestations: conflicting values 2 and "Error" (mismatched types int and string)`, + policyFile: `package sigstore + import "struct" + import "list" + authorityMatches: { + keyatt: { + attestations: struct.MaxFields(1) & struct.MinFields(1) + }, + keysignature: { + signatures: list.MaxItems(1) & list.MinItems(1) + }, + if( len(authorityMatches.keylessatt.attestations) < 2) { + keylessattMinAttestations: 2 + keylessattMinAttestations: "Error" + }, + keylesssignature: { + signatures: list.MaxItems(1) & list.MinItems(1) + } + }`, }} for _, tc := range tests { ctx := context.Background() diff --git a/test/e2e_test_cluster_image_policy_with_attestations.sh b/test/e2e_test_cluster_image_policy_with_attestations.sh index e16f535fcbd..ce95ff76f2a 100755 --- a/test/e2e_test_cluster_image_policy_with_attestations.sh +++ b/test/e2e_test_cluster_image_policy_with_attestations.sh @@ -62,6 +62,7 @@ assert_error() { local KUBECTL_OUT_FILE="/tmp/kubectl.failure.out" match="$@" echo looking for ${match} + kubectl delete job demo -n ${NS} --ignore-not-found=true if kubectl create -n ${NS} job demo --image=${demoimage} 2> ${KUBECTL_OUT_FILE} ; then echo Failed to block unsigned Job creation! exit 1 @@ -206,17 +207,14 @@ echo '::endgroup::' # Note we have to bake in the inline data from the keys above echo '::group:: Add cip for two signatures and two attestations' yq '. | .spec.authorities[1].key.data |= load_str("cosign.pub") | .spec.authorities[3].key.data |= load_str("cosign.pub")' ./test/testdata/cosigned/e2e/cip-requires-two-signatures-and-two-attestations.yaml | kubectl apply -f - +# allow things to propagate +sleep 5 echo '::endgroup::' -# TODO(vaikas): Enable the remaining tests once we sort out how to write -# a valid CUE policy, or once #1787 goes in try implementing a Rego one. -echo 'Not testing the CIP policy evaluation yet' -exit 0 - # The CIP policy is the one that should fail now because it doesn't have enough # attestations echo '::group:: test job rejection' -expected_error='no matching attestations' +expected_error='failed to evaluate the policy with error: authorityMatches.keylessattMinAttestations' assert_error ${expected_error} echo '::endgroup::' @@ -229,9 +227,9 @@ echo '::endgroup::' echo '::group:: test job success' # We signed this with key and keyless and it has two keyless attestations and # it has one key attestation, so it should succeed. -if ! kubectl create -n ${NS} job demo3 --image=${demoimage} 2> ./${KUBECTL_OUT_FILE} ; then +if ! kubectl create -n ${NS} job demo3 --image=${demoimage} 2> ${KUBECTL_SUCCESS_FILE} ; then echo Failed to create job that has two signatures and 3 attestations - cat ${KUBECTL_OUT_FILE} + cat ${KUBECTL_SUCCESS_FILE} exit 1 fi echo '::endgroup::' diff --git a/test/testdata/cosigned/e2e/cip-requires-two-signatures-and-two-attestations.yaml b/test/testdata/cosigned/e2e/cip-requires-two-signatures-and-two-attestations.yaml index 80b44ece51a..6e0d32f8866 100644 --- a/test/testdata/cosigned/e2e/cip-requires-two-signatures-and-two-attestations.yaml +++ b/test/testdata/cosigned/e2e/cip-requires-two-signatures-and-two-attestations.yaml @@ -20,14 +20,14 @@ spec: images: - glob: registry.local:5000/cosigned/demo* authorities: - - name: keyless-att + - name: keylessatt keyless: url: http://fulcio.fulcio-system.svc ctlog: url: http://rekor.rekor-system.svc attestations: - predicateType: custom - name: custom-keyless + name: customkeyless policy: type: cue data: | @@ -39,7 +39,7 @@ spec: Timestamp: after } } - - name: key-att + - name: keyatt key: data: | -----BEGIN PUBLIC KEY----- @@ -78,12 +78,12 @@ spec: data: | predicateType: "cosign.sigstore.dev/attestation/v1" predicate: Data: "foobar key e2e test" - - name: keyless-signature + - name: keylesssignature keyless: url: http://fulcio.fulcio-system.svc ctlog: url: http://rekor.rekor-system.svc - - name: key-signature + - name: keysignature key: data: | -----BEGIN PUBLIC KEY----- @@ -95,48 +95,21 @@ spec: policy: type: cue data: | - if len(authorityMatches."keyless-att".attestations) < 2 { - keylessAttestationsErr: "error" - keylessAttestationsErr: "Did not get both keyless attestations" - } - if len(authorityMatches."key-att".attestations) < 1 { - keyAttestationsErr: 1 - keyAttestationsErr: "Did not get key attestation" - } - if len(authorityMatches."keyless-signature".signatures) < 1 { - keylessSignatureErr: 1 - keylessSignatureErr: "Did not get keyless signature" - } - if len(authorityMatches."key-signature".signatures) < 1 { - keySignatureErr: 1 - keySignatureErr: "Did not get key signature" - } + package sigstore + import "struct" + import "list" authorityMatches: { - key-att: { - attestations: { - "vuln-key": [ - {subject: "PLACEHOLDER", issuer: "PLACEHOLDER"}, - ] - } - } - keyless-att: { - attestations: { - "vuln-keyless": [ - {subject: "PLACEHOLDER", issuer: "PLACEHOLDER"}, - ], - "custom-keyless": [ - {subject: "PLACEHOLDER", issuer: "PLACEHOLDER"}, - ], - } - } - keyless-signature: { - signatures: [ - {subject: "PLACEHOLDER", issuer: "PLACEHOLDER"}, - ] - } - key-signature: { - signatures: [ - {subject: "PLACEHOLDER", issuer: "PLACEHOLDER"}, - ] + keyatt: { + attestations: struct.MaxFields(1) & struct.MinFields(1) + }, + keysignature: { + signatures: list.MaxItems(1) & list.MinItems(1) + }, + if (len(authorityMatches.keylessatt.attestations) < 2) { + keylessattMinAttestations: 2 + keylessattMinAttestations: "Error" + }, + keylesssignature: { + signatures: list.MaxItems(1) & list.MinItems(1) } } From d104fc40dc673ea5316b6978f6f1b63384e29b1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Tue, 26 Apr 2022 20:24:26 +0200 Subject: [PATCH 50/53] Don't fail open in VerifyBundle (#1648) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We do need to accept a missing certificate here (to accept raw signatures which are uploaded in a transparency log), but that's not a reason to bypass all other checks in this function. Signed-off-by: Miloslav Trmač --- pkg/cosign/verify.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index f60c40ead31..ff1adb3ebc0 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -757,13 +757,14 @@ func VerifyBundle(ctx context.Context, sig oci.Signature) (bool, error) { cert, err := sig.Cert() if err != nil { return false, err - } else if cert == nil { - return true, nil } - // verify the cert against the integrated time - if err := CheckExpiry(cert, time.Unix(bundle.Payload.IntegratedTime, 0)); err != nil { - return false, errors.Wrap(err, "checking expiry on cert") + if cert != nil { + // Verify the cert against the integrated time. + // Note that if the caller requires the certificate to be present, it has to ensure that itself. + if err := CheckExpiry(cert, time.Unix(bundle.Payload.IntegratedTime, 0)); err != nil { + return false, errors.Wrap(err, "checking expiry on cert") + } } payload, err := sig.Payload() From 367c79e61db25a203f4a07dbf400b67745ce209c Mon Sep 17 00:00:00 2001 From: Hayden B Date: Tue, 26 Apr 2022 11:29:01 -0700 Subject: [PATCH 51/53] Load in intermediate cert pool from TUF (#1804) With the v3 TUF root, the intermediate CA certificate will be included, so that if the intermediate signing key was compromised, the intermediate certificate could be revoked by removing it from the TUF targets and replacing it with a trusted certificate. This change loads the intermediate certificate from TUF. However, we don't want to force all users to follow this structure - They may choose to use CRLs to detect revoked intermediates. Also, I don't want to enforce TUF usage in the Verify package. Therefore, for TUF, we lazily create a certificate pool only if an intermediate certificate is found, and if it's not found, then VerifyImageSignature will create a pool using the chain provided in the annotation. Signed-off-by: Hayden Blauzvern --- .../cli/fulcio/fulcioroots/fulcioroots.go | 19 ++++++++-- cmd/cosign/cli/verify/verify.go | 1 + cmd/cosign/cli/verify/verify_attestation.go | 1 + pkg/cosign/verify.go | 6 ++-- pkg/cosign/verify_test.go | 36 +++++++++++++++++++ pkg/sget/sget.go | 1 + 6 files changed, 60 insertions(+), 4 deletions(-) diff --git a/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots.go b/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots.go index db1fc460c7e..c0890bd77c2 100644 --- a/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots.go +++ b/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots.go @@ -83,8 +83,8 @@ func GetIntermediates() *x509.CertPool { } func initRoots() (*x509.CertPool, *x509.CertPool, error) { - rootPool := x509.NewCertPool() - intermediatePool := x509.NewCertPool() + var rootPool *x509.CertPool + var intermediatePool *x509.CertPool rootEnv := os.Getenv(altRoot) if rootEnv != "" { @@ -99,8 +99,14 @@ func initRoots() (*x509.CertPool, *x509.CertPool, error) { for _, cert := range certs { // root certificates are self-signed if bytes.Equal(cert.RawSubject, cert.RawIssuer) { + if rootPool == nil { + rootPool = x509.NewCertPool() + } rootPool.AddCert(cert) } else { + if intermediatePool == nil { + intermediatePool = x509.NewCertPool() + } intermediatePool.AddCert(cert) } } @@ -127,12 +133,21 @@ func initRoots() (*x509.CertPool, *x509.CertPool, error) { for _, cert := range certs { // root certificates are self-signed if bytes.Equal(cert.RawSubject, cert.RawIssuer) { + if rootPool == nil { + rootPool = x509.NewCertPool() + } rootPool.AddCert(cert) } else { + if intermediatePool == nil { + intermediatePool = x509.NewCertPool() + } intermediatePool.AddCert(cert) } } } + if intermediatePool == nil { + intermediatePool = x509.NewCertPool() + } intermediatePool.AppendCertsFromPEM([]byte(fulcioIntermediateV1)) } return rootPool, intermediatePool, nil diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index e55381e0b1d..fb9595fd54a 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -111,6 +111,7 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { co.RekorClient = rekorClient } co.RootCerts = fulcio.GetRoots() + co.IntermediateCerts = fulcio.GetIntermediates() } keyRef := c.KeyRef certRef := c.CertRef diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go index 8ab95fd7dac..93867633976 100644 --- a/cmd/cosign/cli/verify/verify_attestation.go +++ b/cmd/cosign/cli/verify/verify_attestation.go @@ -92,6 +92,7 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e co.RekorClient = rekorClient } co.RootCerts = fulcio.GetRoots() + co.IntermediateCerts = fulcio.GetIntermediates() } keyRef := c.KeyRef diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index ff1adb3ebc0..e2e9d98a368 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -475,7 +475,8 @@ func VerifyImageSignature(ctx context.Context, sig oci.Signature, h v1.Hash, co // If the chain annotation is not present or there is only a root if chain == nil || len(chain) <= 1 { co.IntermediateCerts = nil - } else { + } else if co.IntermediateCerts == nil { + // If the intermediate certs have not been loaded in by TUF pool := x509.NewCertPool() for _, cert := range chain[:len(chain)-1] { pool.AddCert(cert) @@ -653,7 +654,8 @@ func verifyImageAttestations(ctx context.Context, atts oci.Signatures, h v1.Hash // If the chain annotation is not present or there is only a root if chain == nil || len(chain) <= 1 { co.IntermediateCerts = nil - } else { + } else if co.IntermediateCerts == nil { + // If the intermediate certs have not been loaded in by TUF pool := x509.NewCertPool() for _, cert := range chain[:len(chain)-1] { pool.AddCert(cert) diff --git a/pkg/cosign/verify_test.go b/pkg/cosign/verify_test.go index 6208cd02032..2c38ccd0cc4 100644 --- a/pkg/cosign/verify_test.go +++ b/pkg/cosign/verify_test.go @@ -309,6 +309,42 @@ func TestVerifyImageSignatureWithMissingSub(t *testing.T) { } } +func TestVerifyImageSignatureWithExistingSub(t *testing.T) { + rootCert, rootKey, _ := test.GenerateRootCa() + subCert, subKey, _ := test.GenerateSubordinateCa(rootCert, rootKey) + leafCert, privKey, _ := test.GenerateLeafCert("subject", "oidc-issuer", subCert, subKey) + pemRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Raw}) + pemSub := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: subCert.Raw}) + pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) + + otherSubCert, _, _ := test.GenerateSubordinateCa(rootCert, rootKey) + + rootPool := x509.NewCertPool() + rootPool.AddCert(rootCert) + subPool := x509.NewCertPool() + // Load in different sub cert so the chain doesn't verify + rootPool.AddCert(otherSubCert) + + payload := []byte{1, 2, 3, 4} + h := sha256.Sum256(payload) + signature, _ := privKey.Sign(rand.Reader, h[:], crypto.SHA256) + + ociSig, _ := static.NewSignature(payload, + base64.StdEncoding.EncodeToString(signature), + static.WithCertChain(pemLeaf, appendSlices([][]byte{pemSub, pemRoot}))) + verified, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, &CheckOpts{RootCerts: rootPool, IntermediateCerts: subPool}) + if err == nil { + t.Fatal("expected error while verifying signature") + } + if !strings.Contains(err.Error(), "certificate signed by unknown authority") { + t.Fatal("expected error while verifying signature") + } + // TODO: Create fake bundle and test verification + if verified == true { + t.Fatalf("expected verified=false, got verified=true") + } +} + func TestValidateAndUnpackCertSuccess(t *testing.T) { subject := "email@email" oidcIssuer := "https://accounts.google.com" diff --git a/pkg/sget/sget.go b/pkg/sget/sget.go index f61e23b8080..2eba346d96f 100644 --- a/pkg/sget/sget.go +++ b/pkg/sget/sget.go @@ -91,6 +91,7 @@ func (sg *SecureGet) Do(ctx context.Context) error { fulcioVerified := (co.SigVerifier == nil) co.RootCerts = fulcio.GetRoots() + co.IntermediateCerts = fulcio.GetIntermediates() sp, bundleVerified, err := cosign.VerifyImageSignatures(ctx, ref, co) if err != nil { From 27caa983025da26d6e2fc18c608e3a5289394a8b Mon Sep 17 00:00:00 2001 From: Carlos Tadeu Panato Junior Date: Wed, 27 Apr 2022 08:18:55 -0500 Subject: [PATCH 52/53] add changelog for release v1.8.0 (#1803) Signed-off-by: cpanato --- CHANGELOG.md | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95351e05709..b738aa652a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,77 @@ +# v1.8.0 + +_NOTE_: If you use Fulcio to issue certificates you will need to use this release. + +## Enhancements + +* Handle context cancelled properly + tests. (https://github.com/sigstore/cosign/pull/1796) +* Allow passing keys via environment variables (`env://` refs) (https://github.com/sigstore/cosign/pull/1794) +* Add parallelization for processing policies / authorities. (https://github.com/sigstore/cosign/pull/1795) +* Attestations + policy in cip. (https://github.com/sigstore/cosign/pull/1772) +* Refactor fulcio signer to take in KeyOpts. (https://github.com/sigstore/cosign/pull/1788) +* Remove the dependency on v1alpha1.Identity which brings in (https://github.com/sigstore/cosign/pull/1790) +* Add Fulcio intermediate CA certificate to intermediate pool (https://github.com/sigstore/cosign/pull/1774) +* Cosigned validate against remote sig src (https://github.com/sigstore/cosign/pull/1754) +* tuf: add debug info if tuf update fails (https://github.com/sigstore/cosign/pull/1766) +* Break the CIP action tests into a sh script. (https://github.com/sigstore/cosign/pull/1767) +* [policy-webhook] The webhooks name is now configurable via --(validating|mutating)-webhook-name flags (https://github.com/sigstore/cosign/pull/1757) +* Verify embedded SCTs (https://github.com/sigstore/cosign/pull/1731) +* Validate issuer/subject regexp in validate webhook. (https://github.com/sigstore/cosign/pull/1761) +* Add intermediate CA certificate pool for Fulcio (https://github.com/sigstore/cosign/pull/1749) +* [cosigned] The webhook name is now configurable via --webhook-name flag (https://github.com/sigstore/cosign/pull/1726) +* Use bundle log ID to find verification key (https://github.com/sigstore/cosign/pull/1748) +* Refactor policy related code, add support for vuln verify (https://github.com/sigstore/cosign/pull/1747) +* Create convert functions for internal CIP (https://github.com/sigstore/cosign/pull/1736) +* Move the KMS integration imports into the binary entrypoints (https://github.com/sigstore/cosign/pull/1744) + +## Bug Fixes + +* Fix a bug where an error would send duplicate results. (https://github.com/sigstore/cosign/pull/1797) +* fix: more informative error (https://github.com/sigstore/cosign/pull/1778) +* fix: add support for rsa keys (https://github.com/sigstore/cosign/pull/1768) +* Implement identities, fix bug in webhook validation. (https://github.com/sigstore/cosign/pull/1759) + +## Others + +* Bump github.com/hashicorp/go-retryablehttp from 0.7.0 to 0.7.1 (https://github.com/sigstore/cosign/pull/1758) +* Bump google-github-actions/auth from 0.7.0 to 0.7.1 (https://github.com/sigstore/cosign/pull/1801) +* Bump google.golang.org/grpc from 1.45.0 to 1.46.0 (https://github.com/sigstore/cosign/pull/1800) +* Bump github.com/xanzy/go-gitlab from 0.63.0 to 0.64.0 (https://github.com/sigstore/cosign/pull/1799) +* Revert "Refactor fulcio signer to take in KeyOpts. (https://github.com/sigstore/cosign/pull/1788)" (https://github.com/sigstore/cosign/pull/1798) +* chore: add rego function to consume modules (https://github.com/sigstore/cosign/pull/1787) +* test: add cue unit tests (https://github.com/sigstore/cosign/pull/1791) +* Run update-codegen. (https://github.com/sigstore/cosign/pull/1789) +* Bump actions/checkout from 3.0.1 to 3.0.2 (https://github.com/sigstore/cosign/pull/1783) +* Bump github.com/mitchellh/mapstructure from 1.4.3 to 1.5.0 (https://github.com/sigstore/cosign/pull/1782) +* Bump k8s.io/code-generator from 0.23.5 to 0.23.6 (https://github.com/sigstore/cosign/pull/1781) +* Bump google.golang.org/api from 0.74.0 to 0.75.0 (https://github.com/sigstore/cosign/pull/1780) +* Bump cuelang.org/go from 0.4.2 to 0.4.3 (https://github.com/sigstore/cosign/pull/1779) +* Bump codecov/codecov-action from 3.0.0 to 3.1.0 (https://github.com/sigstore/cosign/pull/1784) +* Bump actions/checkout from 3.0.0 to 3.0.1 (https://github.com/sigstore/cosign/pull/1764) +* Bump mikefarah/yq from 4.24.4 to 4.24.5 (https://github.com/sigstore/cosign/pull/1765) +* chore: add warning when downloading a sBOM (https://github.com/sigstore/cosign/pull/1763) +* chore: add warn when attaching sBOM (https://github.com/sigstore/cosign/pull/1756) +* Bump sigstore/cosign-installer from 2.2.0 to 2.2.1 (https://github.com/sigstore/cosign/pull/1752) +* update go builder and cosign images (https://github.com/sigstore/cosign/pull/1755) +* test: create fake TUF test root and create test SETs for verification (https://github.com/sigstore/cosign/pull/1750) +* Bump github.com/spf13/viper from 1.10.1 to 1.11.0 (https://github.com/sigstore/cosign/pull/1751) +* Bump mikefarah/yq from 4.24.2 to 4.24.4 (https://github.com/sigstore/cosign/pull/1746) +* Bump github.com/xanzy/go-gitlab from 0.62.0 to 0.63.0 (https://github.com/sigstore/cosign/pull/1745) + +## Contributors + +* Asra Ali (@asraa) +* Billy Lynch (@wlynch) +* Carlos Tadeu Panato Junior (@cpanato) +* Denny (@DennyHoang) +* Hayden Blauzvern (@haydentherapper) +* Hector Fernandez (@hectorj2f) +* Matt Moore (@mattmoor) +* Ville Aikas (@vaikas) +* Vladimir Nachev (@vpnachev) +* Youssef Bel Mekki (@ybelMekk) +* Zack Newman (@znewman01) + # v1.7.2 ## Bug Fixes From 9ef6b207218572b3257a5b4251418d75569baaae Mon Sep 17 00:00:00 2001 From: Hayden B Date: Wed, 27 Apr 2022 06:40:34 -0700 Subject: [PATCH 53/53] Support PKCS1 encoded and non-ECDSA CT log public keys (#1806) * Support PKCS1 encoded CT log public keys This came up while testing out staging, which uses a PKCS1 encoded public key. We should be flexible on the supported key format. Signed-off-by: Hayden Blauzvern * Update comment Signed-off-by: Hayden Blauzvern * Remove requirement that key is ECDSA Signed-off-by: Hayden Blauzvern --- .../cli/fulcio/fulcioverifier/ctl/verify.go | 21 +++++++------------ .../fulcio/fulcioverifier/ctl/verify_test.go | 4 ++-- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/ctl/verify.go b/cmd/cosign/cli/fulcio/fulcioverifier/ctl/verify.go index 49ca970fb82..c2af470cc21 100644 --- a/cmd/cosign/cli/fulcio/fulcioverifier/ctl/verify.go +++ b/cmd/cosign/cli/fulcio/fulcioverifier/ctl/verify.go @@ -17,7 +17,6 @@ package ctl import ( "context" "crypto" - "crypto/ecdsa" "crypto/sha256" "crypto/x509" "encoding/json" @@ -89,19 +88,15 @@ func VerifySCT(ctx context.Context, certPEM, chainPEM, rawSCT []byte) error { return err } for _, t := range targets { - pub, err := cryptoutils.UnmarshalPEMToPublicKey(t.Target) + pub, err := getPublicKey(t.Target) if err != nil { return err } - ctPub, ok := pub.(*ecdsa.PublicKey) - if !ok { - return fmt.Errorf("invalid public key: was %T, require *ecdsa.PublicKey", pub) - } - keyID, err := ctutil.GetCTLogID(ctPub) + keyID, err := ctutil.GetCTLogID(pub) if err != nil { return errors.Wrap(err, "error getting CTFE public key hash") } - pubKeys[keyID] = logIDMetadata{ctPub, t.Status} + pubKeys[keyID] = logIDMetadata{pub, t.Status} } } else { fmt.Fprintf(os.Stderr, "**Warning** Using a non-standard public key for verifying SCT: %s\n", rootEnv) @@ -109,7 +104,7 @@ func VerifySCT(ctx context.Context, certPEM, chainPEM, rawSCT []byte) error { if err != nil { return errors.Wrap(err, "error reading alternate public key file") } - pubKey, err := getAlternatePublicKey(raw) + pubKey, err := getPublicKey(raw) if err != nil { return errors.Wrap(err, "error parsing alternate public key from the file") } @@ -204,9 +199,9 @@ func VerifyEmbeddedSCT(ctx context.Context, chain []*x509.Certificate) error { } // Given a byte array, try to construct a public key from it. -// Will try first to see if it's PEM formatted, if not, then it will -// try to parse it as der publics, and failing that -func getAlternatePublicKey(in []byte) (crypto.PublicKey, error) { +// Supports PEM encoded public keys, falling back to DER. Supports +// PKIX and PKCS1 encoded keys. +func getPublicKey(in []byte) (crypto.PublicKey, error) { var pubKey crypto.PublicKey var err error var derBytes []byte @@ -222,7 +217,7 @@ func getAlternatePublicKey(in []byte) (crypto.PublicKey, error) { // Try using the PKCS1 before giving up. pubKey, err = x509.ParsePKCS1PublicKey(derBytes) if err != nil { - return nil, errors.Wrap(err, "failed to parse alternate public key") + return nil, errors.Wrap(err, "failed to parse CT log public key") } } return pubKey, nil diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/ctl/verify_test.go b/cmd/cosign/cli/fulcio/fulcioverifier/ctl/verify_test.go index 751e1423551..6f6bd308784 100644 --- a/cmd/cosign/cli/fulcio/fulcioverifier/ctl/verify_test.go +++ b/cmd/cosign/cli/fulcio/fulcioverifier/ctl/verify_test.go @@ -33,7 +33,7 @@ import ( "github.com/sigstore/sigstore/pkg/cryptoutils" ) -func TestGetAlternatePublicKey(t *testing.T) { +func TestGetPublicKey(t *testing.T) { wd, err := os.Getwd() if err != nil { t.Fatalf("Failed to get cwd: %v", err) @@ -58,7 +58,7 @@ func TestGetAlternatePublicKey(t *testing.T) { if err != nil { t.Fatalf("Failed to read testfile %s : %v", tc.file, err) } - got, err := getAlternatePublicKey(bytes) + got, err := getPublicKey(bytes) switch { case err == nil && tc.wantErrSub != "": t.Errorf("Wanted Error for %s but got none", tc.file)