Skip to content

Commit

Permalink
Cosigned validate against remote sig src (#1754)
Browse files Browse the repository at this point in the history
Add github workflow steps for remote signatures

Signed-off-by: Denny Hoang <dhoang@vmware.com>
  • Loading branch information
DennyHoang committed Apr 18, 2022
1 parent 546ca56 commit f89d691
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 16 deletions.
14 changes: 14 additions & 0 deletions pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go
Expand Up @@ -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
}

Expand Down Expand Up @@ -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 == "" {
Expand Down
50 changes: 50 additions & 0 deletions pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go
Expand Up @@ -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())
Expand Down
Expand Up @@ -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"
)
Expand All @@ -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
Expand Down Expand Up @@ -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()

Expand Down
14 changes: 7 additions & 7 deletions pkg/cosign/kubernetes/webhook/validator.go
Expand Up @@ -257,23 +257,23 @@ 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:
// 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, remoteOpts...)
sps, err := valid(ctx, ref, authority.Key.PublicKeys, authorityRemoteOpts...)
if err != nil {
authorityErrors = append(authorityErrors, errors.Wrap(err, "failed to validate keys"))
continue
Expand Down Expand Up @@ -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"))
Expand Down
81 changes: 72 additions & 9 deletions test/e2e_test_cluster_image_policy.sh
Expand Up @@ -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!
Expand All @@ -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
Expand All @@ -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

0 comments on commit f89d691

Please sign in to comment.