Skip to content

Commit

Permalink
[Cosigned] Add signature pull secrets (#1805)
Browse files Browse the repository at this point in the history
* Add signaturePullSecrets support

Signed-off-by: Denny Hoang <dhoang@vmware.com>

* Abstract signaturePullSecrets remoteOpts

Signed-off-by: Denny Hoang <dhoang@vmware.com>

* Add validation and signaturePullSecrets test cases

Signed-off-by: Denny Hoang <dhoang@vmware.com>

* Test Authorities RemoteOpts count

Signed-off-by: Denny Hoang <dhoang@vmware.com>

* Comment on not storing in Authority RemoteOpts

Signed-off-by: Denny Hoang <dhoang@vmware.com>

* Fix lint issue

Signed-off-by: Denny Hoang <dhoang@vmware.com>

* Add podSpec signaturePullSecrets test

Signed-off-by: Denny Hoang <dhoang@vmware.com>

* Add valid signaturePullSecrets test

Signed-off-by: Denny Hoang <dhoang@vmware.com>

* early return err; add signaturePullSecrets comment

Signed-off-by: Denny Hoang <dhoang@vmware.com>

* codegen update

Signed-off-by: Denny Hoang <dhoang@vmware.com>
  • Loading branch information
DennyHoang committed Apr 29, 2022
1 parent de501c3 commit 4f02c2d
Show file tree
Hide file tree
Showing 12 changed files with 345 additions and 25 deletions.
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
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

0 comments on commit 4f02c2d

Please sign in to comment.