Skip to content

Commit

Permalink
Refactor policy related code, add support for vuln verify (#1747)
Browse files Browse the repository at this point in the history
* Refactor policy related code, add support for vuln verify

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

* Thanks @hectorj2f for catching a bad upstream rebase.

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

* Fix typo.

Signed-off-by: Ville Aikas <vaikas@chainguard.dev>
  • Loading branch information
vaikas authored Apr 12, 2022
1 parent 427d83a commit cf03ef2
Show file tree
Hide file tree
Showing 10 changed files with 443 additions and 84 deletions.
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

0 comments on commit cf03ef2

Please sign in to comment.