Skip to content

Commit

Permalink
Attestations + policy in cip. (sigstore#1772)
Browse files Browse the repository at this point in the history
* Modify types, introduce name defaults, codegen.

Signed-off-by: Ville Aikas <vaikas@chainguard.dev>

* Start refactoring and adding verify-attestation pieces.

Signed-off-by: Ville Aikas <vaikas@chainguard.dev>

* Sort of works e2e.

Signed-off-by: Ville Aikas <vaikas@chainguard.dev>

* E2E tests. Plumb rekor through for key-ful signing too.

Signed-off-by: Ville Aikas <vaikas@chainguard.dev>

* bad rebase.

Signed-off-by: Ville Aikas <vaikas@chainguard.dev>

* more bad rebases.

Signed-off-by: Ville Aikas <vaikas@chainguard.dev>

* regen keys.
Signed-off-by: Ville Aikas <vaikas@chainguard.dev>

* at the right place.

Signed-off-by: Ville Aikas <vaikas@chainguard.dev>

* forgot one rekor-url.

Signed-off-by: Ville Aikas <vaikas@chainguard.dev>

* Lint.

Signed-off-by: Ville Aikas <vaikas@chainguard.dev>

* remove these manually.

Signed-off-by: Ville Aikas <vaikas@chainguard.dev>

* Cleanups, do not unnecessarily eval empty json bytes for attestation policy.

Signed-off-by: Ville Aikas <vaikas@chainguard.dev>

* Add name to attestations for easier referencing from cip policy.

Signed-off-by: Ville Aikas <vaikas@chainguard.dev>

* Do not return empty PolicyResult when errors.
Start adding UT for ValidatePolicy. Getting ready for rebase.

Signed-off-by: Ville Aikas <vaikas@chainguard.dev>

* fix: cue policy missing double-quotes

Signed-off-by: hectorj2f <hectorf@vmware.com>

* Add start of unit tests for policy validation stuff.
Add start of UT for ValidatePolicy.

Signed-off-by: Ville Aikas <vaikas@chainguard.dev>

* 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 <vaikas@chainguard.dev>

* loving yaml, thanks validation!

Signed-off-by: Ville Aikas <vaikas@chainguard.dev>

* Starting to break apart the attestation tests.

Signed-off-by: Ville Aikas <vaikas@chainguard.dev>

* checkpoint

Signed-off-by: Ville Aikas <vaikas@chainguard.dev>

* Remove unused keychain from the Validate* calls and use one
from the remoteopts instead consistently.
Address PR feedback.

Signed-off-by: Ville Aikas <vaikas@chainguard.dev>

* lint.

Signed-off-by: Ville Aikas <vaikas@chainguard.dev>

Co-authored-by: hectorj2f <hectorf@vmware.com>
  • Loading branch information
2 people authored and mlieberman85 committed May 6, 2022
1 parent 66ee73f commit 2f6fc54
Show file tree
Hide file tree
Showing 26 changed files with 1,955 additions and 139 deletions.
97 changes: 97 additions & 0 deletions .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
53 changes: 53 additions & 0 deletions config/300-clusterimagepolicy.yaml
Expand Up @@ -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:
Expand Down Expand Up @@ -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-<index in array>
type: string
source:
type: array
items:
Expand All @@ -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
12 changes: 6 additions & 6 deletions pkg/apis/config/image_policies.go
Expand Up @@ -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
Expand All @@ -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
}
}
}
Expand Down
61 changes: 45 additions & 16 deletions pkg/apis/config/image_policies_test.go
Expand Up @@ -51,61 +51,61 @@ 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*'
c, err = defaults.GetMatchingPolicies("randomstuffhere")
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.*"
c, err = defaults.GetMatchingPolicies("randomregexstringstuff")
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")
Expand All @@ -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)
Expand All @@ -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
}
}
Expand Down

0 comments on commit 2f6fc54

Please sign in to comment.