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
8 changes: 8 additions & 0 deletions config/300-clusterimagepolicy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,14 @@ spec:
properties:
oci:
type: string
signaturePullSecrets:
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
2 changes: 2 additions & 0 deletions pkg/apis/cosigned/v1alpha1/clusterimagepolicy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ type KeyRef struct {
type Source struct {
// +optional
OCI string `json:"oci,omitempty"`
// +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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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.

Original file line number Diff line number Diff line change
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
Loading