Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Cosigned] Add signature pull secrets #1805

Merged
merged 10 commits into from Apr 29, 2022
9 changes: 9 additions & 0 deletions config/300-clusterimagepolicy.yaml
Expand Up @@ -139,6 +139,15 @@ spec:
properties:
oci:
type: string
signaturePullSecrets:
description: SignaturePullSecrets is an optional list of references to secrets in the same namespace as the deploying resource for pulling any of the signatures used by this Source.
type: array
items:
type: object
properties:
name:
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names'
type: string
images:
type: array
items:
Expand Down
47 changes: 47 additions & 0 deletions pkg/apis/config/image_policies_test.go
Expand Up @@ -154,6 +154,37 @@ func TestGetAuthorities(t *testing.T) {
if got := c[matchedPolicy].Authorities[0].Attestations[0].Data; got != want {
t.Errorf("Did not get what I wanted %q, got %+v", want, got)
}

// Test source oci
matchedPolicy = "cluster-image-policy-source-oci"
c, err = defaults.GetMatchingPolicies("sourceocionly")
checkGetMatches(t, c, err)
if len(c) != 1 {
t.Errorf("Wanted 1 match, got %d", len(c))
}

checkSourceOCI(t, c[matchedPolicy].Authorities)
want = "example.registry.com/alternative/signature"
if got := c[matchedPolicy].Authorities[0].Sources[0].OCI; got != want {
t.Errorf("Did not get what I wanted %q, got %+v", want, got)
}

// Test source signaturePullSecrets
matchedPolicy = "cluster-image-policy-source-oci-signature-pull-secrets"
c, err = defaults.GetMatchingPolicies("sourceocisignaturepullsecrets")
checkGetMatches(t, c, err)
if len(c) != 1 {
t.Errorf("Wanted 1 match, got %d", len(c))
}

checkSourceOCI(t, c[matchedPolicy].Authorities)
if got := len(c[matchedPolicy].Authorities[0].Sources[0].SignaturePullSecrets); got != 1 {
t.Errorf("Did not get what I wanted %d, got %d", 1, got)
}
want = "examplePullSecret"
if got := c[matchedPolicy].Authorities[0].Sources[0].SignaturePullSecrets[0].Name; got != want {
t.Errorf("Did not get what I wanted %q, got %+v", want, got)
}
}

func checkGetMatches(t *testing.T, c map[string]webhookcip.ClusterImagePolicy, err error) {
Expand Down Expand Up @@ -191,3 +222,19 @@ func checkPublicKey(t *testing.T, gotKey crypto.PublicKey) {
t.Errorf("Did not get what I wanted %s, got %s", inlineKeyData, string(pemBytes))
}
}

func checkSourceOCI(t *testing.T, authority []webhookcip.Authority) {
t.Helper()

if got := len(authority); got != 1 {
t.Errorf("Did not get what I wanted %d, got %d", 1, got)
}
if got := len(authority[0].Sources); got != 1 {
t.Errorf("Did not get what I wanted %d, got %d", 1, got)
}

want := len(authority[0].Sources)
if got := len(authority[0].RemoteOpts); got != want {
t.Errorf("Did not get what I wanted %d, got %d", want, got)
}
}
3 changes: 3 additions & 0 deletions pkg/apis/config/store_test.go
Expand Up @@ -20,6 +20,7 @@ import (

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/sigstore/cosign/pkg/oci/remote"
"k8s.io/apimachinery/pkg/api/resource"
logtesting "knative.dev/pkg/logging/testing"

Expand All @@ -28,6 +29,8 @@ import (

var ignoreStuff = cmp.Options{
cmpopts.IgnoreUnexported(resource.Quantity{}),
// Ignore functional remote options
cmpopts.IgnoreTypes((remote.Option)(nil)),
}

func TestStoreLoadWithContext(t *testing.T) {
Expand Down
20 changes: 20 additions & 0 deletions pkg/apis/config/testdata/config-image-policies.yaml
Expand Up @@ -105,3 +105,23 @@ data:
policy:
type: cue
data: "cip level cue here"
cluster-image-policy-source-oci: |
images:
- regex: .*sourceocionly.*
authorities:
- name: attestation-0
key:
data: inlinedata here
source:
- oci: "example.registry.com/alternative/signature"
cluster-image-policy-source-oci-signature-pull-secrets: |
images:
- regex: .*sourceocisignaturepullsecrets.*
authorities:
- name: attestation-0
key:
data: inlinedata here
source:
- oci: "example.registry.com/alternative/signature"
signaturePullSecrets:
- name: examplePullSecret
5 changes: 5 additions & 0 deletions pkg/apis/cosigned/v1alpha1/clusterimagepolicy_types.go
Expand Up @@ -115,6 +115,11 @@ type KeyRef struct {
type Source struct {
// +optional
OCI string `json:"oci,omitempty"`
// SignaturePullSecrets is an optional list of references to secrets in the
// same namespace as the deploying resource for pulling any of the signatures
// used by this Source.
// +optional
hectorj2f marked this conversation as resolved.
Show resolved Hide resolved
SignaturePullSecrets []v1.LocalObjectReference `json:"signaturePullSecrets,omitempty"`
}

// TLog specifies the URL to a transparency log that holds
Expand Down
12 changes: 10 additions & 2 deletions pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go
Expand Up @@ -84,8 +84,8 @@ func (authority *Authority) Validate(ctx context.Context) *apis.FieldError {
errs = errs.Also(authority.Keyless.Validate(ctx).ViaField("keyless"))
}

for _, source := range authority.Sources {
errs = errs.Also(source.Validate(ctx).ViaField("source"))
for i, source := range authority.Sources {
errs = errs.Also(source.Validate(ctx).ViaFieldIndex("source", i))
}

for _, att := range authority.Attestations {
Expand Down Expand Up @@ -144,6 +144,14 @@ func (source *Source) Validate(ctx context.Context) *apis.FieldError {
if source.OCI == "" {
errs = errs.Also(apis.ErrMissingField("oci"))
}

if len(source.SignaturePullSecrets) > 0 {
for i, secret := range source.SignaturePullSecrets {
if secret.Name == "" {
errs = errs.Also(apis.ErrMissingField("name")).ViaFieldIndex("signaturePullSecrets", i)
}
}
}
return errs
}

Expand Down
48 changes: 47 additions & 1 deletion pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go
Expand Up @@ -19,6 +19,7 @@ import (
"testing"

"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1"
"knative.dev/pkg/apis"
)

Expand Down Expand Up @@ -415,7 +416,7 @@ func TestAuthoritiesValidation(t *testing.T) {
{
name: "Should fail when source oci is empty",
expectErr: true,
errorString: "missing field(s): spec.authorities[0].source.oci",
errorString: "missing field(s): spec.authorities[0].source[0].oci",
policy: ClusterImagePolicy{
Spec: ClusterImagePolicySpec{
Images: []ImagePattern{{Regex: ".*"}},
Expand Down Expand Up @@ -486,6 +487,51 @@ func TestAuthoritiesValidation(t *testing.T) {
},
},
},
{
name: "Should fail with signaturePullSecret name empty",
expectErr: true,
errorString: "missing field(s): spec.authorities[0].source[0].signaturePullSecrets[0].name",
policy: ClusterImagePolicy{
Spec: ClusterImagePolicySpec{
Images: []ImagePattern{{Regex: ".*"}},
Authorities: []Authority{
{
Key: &KeyRef{KMS: "kms://key/path"},
Sources: []Source{
{
OCI: "registry1",
SignaturePullSecrets: []v1.LocalObjectReference{
{Name: ""},
},
},
},
},
},
},
},
},
{
name: "Should pass with signaturePullSecret name filled",
expectErr: false,
policy: ClusterImagePolicy{
Spec: ClusterImagePolicySpec{
Images: []ImagePattern{{Regex: ".*"}},
Authorities: []Authority{
{
Key: &KeyRef{KMS: "kms://key/path"},
Sources: []Source{
{
OCI: "registry1",
SignaturePullSecrets: []v1.LocalObjectReference{
{Name: "testPullSecrets"},
},
},
},
},
},
},
},
},
}

for _, test := range tests {
Expand Down
9 changes: 8 additions & 1 deletion pkg/apis/cosigned/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Expand Up @@ -15,17 +15,21 @@
package clusterimagepolicy

import (
"context"
"crypto"
"crypto/x509"
"encoding/json"
"encoding/pem"

"github.com/google/go-containerregistry/pkg/authn/k8schain"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/pkg/errors"
"github.com/sigstore/cosign/pkg/apis/cosigned/v1alpha1"
"github.com/sigstore/cosign/pkg/oci/remote"

ociremote "github.com/sigstore/cosign/pkg/oci/remote"
"knative.dev/pkg/apis"
kubeclient "knative.dev/pkg/client/injection/kube/client"
"knative.dev/pkg/logging"
)

// ClusterImagePolicy defines the images that go through verification
Expand Down Expand Up @@ -58,7 +62,7 @@ type Authority struct {
// RemoteOpts are not marshalled because they are an unsupported type
// RemoteOpts will be populated by the Authority UnmarshalJSON override
// +optional
RemoteOpts []remote.Option `json:"-"`
RemoteOpts []ociremote.Option `json:"-"`
// +optional
Attestations []AttestationPolicy `json:"attestations,omitempty"`
}
Expand Down Expand Up @@ -139,7 +143,7 @@ func (a *Authority) UnmarshalJSON(data []byte) error {
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))
rawAuthority.RemoteOpts = append(rawAuthority.RemoteOpts, ociremote.WithTargetRepository(targetRepoOverride))
}
}
}
Expand All @@ -149,6 +153,35 @@ func (a *Authority) UnmarshalJSON(data []byte) error {
return nil
}

// SourceSignaturePullSecretsOpts creates the signaturePullSecrets remoteOpts
// This is not stored in the Authority under RemoteOpts as the namespace can be different
func (a *Authority) SourceSignaturePullSecretsOpts(ctx context.Context, namespace string) ([]ociremote.Option, error) {
var ret []ociremote.Option
for _, source := range a.Sources {
if len(source.SignaturePullSecrets) > 0 {
signaturePullSecrets := make([]string, 0, len(source.SignaturePullSecrets))
for _, s := range source.SignaturePullSecrets {
signaturePullSecrets = append(signaturePullSecrets, s.Name)
}

opt := k8schain.Options{
Namespace: namespace,
ImagePullSecrets: signaturePullSecrets,
}

kc, err := k8schain.New(ctx, kubeclient.Get(ctx), opt)
if err != nil {
logging.FromContext(ctx).Errorf("failed creating keychain: %+v", err)
return nil, err
}

ret = append(ret, ociremote.WithRemoteOptions(remote.WithAuthFromKeychain(kc)))
}
}

return ret, nil
}

func ConvertClusterImagePolicyV1alpha1ToWebhook(in *v1alpha1.ClusterImagePolicy) *ClusterImagePolicy {
copyIn := in.DeepCopy()

Expand Down