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

Refactor policy related code, add support for vuln verify #1747

Merged
merged 3 commits into from
Apr 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 40 additions & 11 deletions .github/workflows/kind-verify-attestation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ jobs:
GO111MODULE: on
GOFLAGS: -ldflags=-s -ldflags=-w
KOCACHE: ~/ko
COSIGN_EXPERIMENTAL: true
# Trust the custom Rekor API endpoint for fetching the Public Key from it.
SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY: "true"
# We are only testing keyless here, so set it.
COSIGN_EXPERIMENTAL: "true"

steps:
- uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # v2.4.0
Expand Down Expand Up @@ -89,28 +92,28 @@ jobs:
- name: Create attestation for it
run: |
echo -n 'foobar e2e test' > ./predicate-file
SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY=1 COSIGN_EXPERIMENTAL=1 ./cosign attest --predicate ./predicate-file --fulcio-url ${{ env.FULCIO_URL }} --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry --force ${{ env.demoimage }} --identity-token ${{ env.OIDC_TOKEN }}
./cosign attest --predicate ./predicate-file --fulcio-url ${{ env.FULCIO_URL }} --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry --force ${{ env.demoimage }} --identity-token ${{ env.OIDC_TOKEN }}

- name: Verify with cosign
run: |
SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY=1 COSIGN_EXPERIMENTAL=1 ./cosign verify --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry ${{ env.demoimage }}
./cosign verify --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry ${{ env.demoimage }}

- name: Verify attestation with cosign, works
- name: Verify custom attestation with cosign, works
run: |
echo '::group:: test verify-attestation success'
if ! SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY=1 COSIGN_EXPERIMENTAL=1 ./cosign verify-attestation --policy ./test/testdata/policies/cue-works.cue --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry ${{ env.demoimage }} ; then
echo '::group:: test custom verify-attestation success'
if ! ./cosign verify-attestation --policy ./test/testdata/policies/cue-works.cue --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry ${{ env.demoimage }} ; then
echo Failed to verify attestation with a valid policy
exit 1
else
echo Successfully validated attestation with a valid policy
echo Successfully validated custom attestation with a valid policy
fi
echo '::endgroup::'

- name: Verify attestation with cosign, fails
- name: Verify custom attestation with cosign, fails
run: |
echo '::group:: test verify-attestation success'
if SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY=1 COSIGN_EXPERIMENTAL=1 ./cosign verify-attestation --policy ./test/testdata/policies/cue-fails.cue --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry ${{ env.demoimage }} ; then
echo verify-attestation succeeded with cue policy that should not work
echo '::group:: test custom verify-attestation success'
if ./cosign verify-attestation --policy ./test/testdata/policies/cue-fails.cue --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry ${{ env.demoimage }} ; then
echo custom verify-attestation succeeded with cue policy that should not work
exit 1
else
echo Successfully failed a policy that should not work
Expand All @@ -120,3 +123,29 @@ jobs:
- name: Collect diagnostics
if: ${{ failure() }}
uses: chainguard-dev/actions/kind-diag@84c993eaf02da1c325854fb272a4df9184bd80fc # main

- name: Create vuln attestation for it
run: |
./cosign attest --predicate ./test/testdata/attestations/vuln-predicate.json --type vuln --fulcio-url ${{ env.FULCIO_URL }} --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry --force ${{ env.demoimage }} --identity-token ${{ env.OIDC_TOKEN }}

- name: Verify vuln attestation with cosign, works
run: |
echo '::group:: test vuln verify-attestation success'
if ! ./cosign verify-attestation --type vuln --policy ./test/testdata/policies/cue-vuln-works.cue --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry ${{ env.demoimage }} ; then
echo Failed to verify attestation with a valid policy
exit 1
else
echo Successfully validated vuln attestation with a valid policy
fi
echo '::endgroup::'

- name: Verify vuln attestation with cosign, fails
run: |
echo '::group:: test vuln verify-attestation success'
if ./cosign verify-attestation --type vuln --policy ./test/testdata/policies/cue-vuln-fails.cue --rekor-url ${{ env.REKOR_URL }} --allow-insecure-registry ${{ env.demoimage }} ; then
echo verify-attestation succeeded with cue policy that should not work
exit 1
else
echo Successfully failed a policy that should not work
fi
echo '::endgroup::'
79 changes: 6 additions & 73 deletions cmd/cosign/cli/verify/verify_attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,12 @@ package verify
import (
"context"
"crypto"
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"os"
"path/filepath"

"github.com/google/go-containerregistry/pkg/name"
"github.com/in-toto/in-toto-golang/in_toto"
"github.com/pkg/errors"
"github.com/sigstore/cosign/pkg/cosign/pkcs11key"
"github.com/sigstore/cosign/pkg/cosign/rego"
Expand All @@ -39,6 +36,7 @@ import (
"github.com/sigstore/cosign/pkg/cosign"
"github.com/sigstore/cosign/pkg/cosign/cue"
"github.com/sigstore/cosign/pkg/cosign/pivkey"
"github.com/sigstore/cosign/pkg/policy"
sigs "github.com/sigstore/cosign/pkg/signature"
)

Expand All @@ -47,11 +45,11 @@ import (
type VerifyAttestationCommand struct {
options.RegistryOptions
CheckClaims bool
KeyRef string
CertRef string
CertEmail string
CertOidcIssuer string
CertChain string
KeyRef string
Sk bool
Slot string
Output string
Expand Down Expand Up @@ -182,80 +180,15 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e

var validationErrors []error
for _, vp := range verified {
var payloadData map[string]interface{}

p, err := vp.Payload()
if err != nil {
return errors.Wrap(err, "could not get payload")
}

err = json.Unmarshal(p, &payloadData)
payload, err := policy.AttestationToPayloadJSON(ctx, c.PredicateType, vp)
if err != nil {
return errors.Wrap(err, "unmarshal payload data")
return errors.Wrap(err, "converting to consumable policy validation")
}

var decodedPayload []byte
if val, ok := payloadData["payload"]; ok {
decodedPayload, err = base64.StdEncoding.DecodeString(val.(string))
if err != nil {
return fmt.Errorf("could not decode 'payload': %w", err)
}
} else {
return fmt.Errorf("could not find 'payload' in payload data")
}

predicateURI, ok := options.PredicateTypeMap[c.PredicateType]
if !ok {
return fmt.Errorf("invalid predicate type: %s", c.PredicateType)
}

// Only apply the policy against the requested predicate type
var statement in_toto.Statement
if err := json.Unmarshal(decodedPayload, &statement); err != nil {
return fmt.Errorf("unmarshal in-toto statement: %w", err)
}
if statement.PredicateType != predicateURI {
if len(payload) == 0 {
// This is not the predicate type we're looking for.
continue
}

var payload []byte
switch c.PredicateType {
case options.PredicateCustom:
payload, err = json.Marshal(statement)
if err != nil {
return fmt.Errorf("error when generating CosignStatement: %w", err)
}
case options.PredicateLink:
var linkStatement in_toto.LinkStatement
if err := json.Unmarshal(decodedPayload, &linkStatement); err != nil {
return fmt.Errorf("unmarshal LinkStatement: %w", err)
}
payload, err = json.Marshal(linkStatement)
if err != nil {
return fmt.Errorf("error when generating LinkStatement: %w", err)
}
case options.PredicateSLSA:
var slsaProvenanceStatement in_toto.ProvenanceStatement
if err := json.Unmarshal(decodedPayload, &slsaProvenanceStatement); err != nil {
return fmt.Errorf("unmarshal ProvenanceStatement: %w", err)
}
payload, err = json.Marshal(slsaProvenanceStatement)
if err != nil {
return fmt.Errorf("error when generating ProvenanceStatement: %w", err)
}
case options.PredicateSPDX:
var spdxStatement in_toto.SPDXStatement
if err := json.Unmarshal(decodedPayload, &spdxStatement); err != nil {
return fmt.Errorf("unmarshal SPDXStatement: %w", err)
}
payload, err = json.Marshal(spdxStatement)
if err != nil {
return fmt.Errorf("error when generating SPDXStatement: %w", err)
}
default:
return fmt.Errorf("unsupported predicate type: %s", c.PredicateType)
}

if len(cuePolicies) > 0 {
fmt.Fprintf(os.Stderr, "will be validating against CUE policies: %v\n", cuePolicies)
cueValidationErr := cue.ValidateJSON(payload, cuePolicies)
Expand Down
9 changes: 9 additions & 0 deletions pkg/cosign/attestation/attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ type CosignVulnPredicate struct {
Metadata Metadata `json:"metadata"`
}

// I think this will be moving to upstream in-toto in the fullness of time
// but creating it here for now so that we have a way to deserialize it
// as a InToto Statement
// https://github.com/in-toto/attestation/issues/58
type CosignVulnStatement struct {
in_toto.StatementHeader
Predicate CosignVulnPredicate `json:"predicate"`
}

type Invocation struct {
Parameters interface{} `json:"parameters"`
URI string `json:"uri"`
Expand Down
130 changes: 130 additions & 0 deletions pkg/policy/attestation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
//
// 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.

package policy

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"

"github.com/in-toto/in-toto-golang/in_toto"
"github.com/pkg/errors"
"github.com/sigstore/cosign/pkg/oci"

"github.com/sigstore/cosign/cmd/cosign/cli/options"
"github.com/sigstore/cosign/pkg/cosign/attestation"
)

// AttestationToPayloadJSON takes in a verified Attestation (oci.Signature) and
// marshals it into a JSON depending on the payload that's then consumable
// by policy engine like cue, rego, etc.
//
// Anything fed here must have been validated with either
// `VerifyLocalImageAttestations` or `VerifyImageAttestations`
//
// If there's no error, and payload is empty means the predicateType did not
// match the attestation.
func AttestationToPayloadJSON(ctx context.Context, predicateType string, verifiedAttestation oci.Signature) ([]byte, error) {
// Check the predicate up front, no point in wasting time if it's invalid.
predicateURI, ok := options.PredicateTypeMap[predicateType]
if !ok {
return nil, fmt.Errorf("invalid predicate type: %s", predicateType)
}

var payloadData map[string]interface{}

p, err := verifiedAttestation.Payload()
if err != nil {
return nil, errors.Wrap(err, "getting payload")
}

err = json.Unmarshal(p, &payloadData)
if err != nil {
return nil, errors.Wrap(err, "unmarshaling payload data")
}

var decodedPayload []byte
if val, ok := payloadData["payload"]; ok {
decodedPayload, err = base64.StdEncoding.DecodeString(val.(string))
if err != nil {
return nil, errors.Wrap(err, "decoding payload")
}
} else {
return nil, fmt.Errorf("could not find payload in payload data")
}

// Only apply the policy against the requested predicate type
var statement in_toto.Statement
if err := json.Unmarshal(decodedPayload, &statement); err != nil {
return nil, fmt.Errorf("unmarshal in-toto statement: %w", err)
}
if statement.PredicateType != predicateURI {
// This is not the predicate we're looking for, so skip it.
return nil, nil
}

// NB: In many (all?) of these cases, we could just return the
// 'json.Marshal', but we check for errors here to decorate them
// with more meaningful error message.
var payload []byte
switch predicateType {
case options.PredicateCustom:
payload, err = json.Marshal(statement)
if err != nil {
return nil, errors.Wrap(err, "generating CosignStatement")
}
case options.PredicateLink:
var linkStatement in_toto.LinkStatement
if err := json.Unmarshal(decodedPayload, &linkStatement); err != nil {
return nil, errors.Wrap(err, "unmarshaling LinkStatement")
}
payload, err = json.Marshal(linkStatement)
if err != nil {
return nil, errors.Wrap(err, "marshaling LinkStatement")
}
case options.PredicateSLSA:
var slsaProvenanceStatement in_toto.ProvenanceStatement
if err := json.Unmarshal(decodedPayload, &slsaProvenanceStatement); err != nil {
return nil, errors.Wrap(err, "unmarshaling ProvenanceStatement")
}
payload, err = json.Marshal(slsaProvenanceStatement)
if err != nil {
return nil, errors.Wrap(err, "marshaling ProvenanceStatement")
}
case options.PredicateSPDX:
var spdxStatement in_toto.SPDXStatement
if err := json.Unmarshal(decodedPayload, &spdxStatement); err != nil {
return nil, errors.Wrap(err, "unmarshaling SPDXStatement")
}
payload, err = json.Marshal(spdxStatement)
if err != nil {
return nil, errors.Wrap(err, "marshaling SPDXStatement")
}
case options.PredicateVuln:
var vulnStatement attestation.CosignVulnStatement
if err := json.Unmarshal(decodedPayload, &vulnStatement); err != nil {
return nil, errors.Wrap(err, "unmarshaling CosignVulnStatement")
}
payload, err = json.Marshal(vulnStatement)
if err != nil {
return nil, errors.Wrap(err, "marshaling CosignVulnStatement")
}
default:
return nil, fmt.Errorf("unsupported predicate type: %s", predicateType)
}
return payload, nil
}
Loading