Skip to content

Commit

Permalink
feat: add validation for predicates via cue or rego policy files supp…
Browse files Browse the repository at this point in the history
…ort (#641)

Signed-off-by: Batuhan Apaydın <[email protected]>
Co-authored-by: Erkan Zileli <[email protected]>
Co-authored-by: Scott Nichols <[email protected]>
Co-authored-by: Furkan Türkal <[email protected]>
Co-authored-by: Dan Lorenc <[email protected]>
Signed-off-by: Batuhan Apaydın <[email protected]>

Co-authored-by: Erkan Zileli <[email protected]>
Co-authored-by: Scott Nichols <[email protected]>
Co-authored-by: Furkan Türkal <[email protected]>
Co-authored-by: Dan Lorenc <[email protected]>
  • Loading branch information
5 people authored Oct 10, 2021
1 parent 278ad7d commit b0408bf
Show file tree
Hide file tree
Showing 12 changed files with 391 additions and 44 deletions.
27 changes: 4 additions & 23 deletions cmd/cosign/cli/attest/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,10 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"os"

"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/in-toto/in-toto-golang/in_toto"
"github.com/pkg/errors"

"github.com/sigstore/cosign/cmd/cosign/cli/options"
Expand All @@ -45,20 +43,6 @@ import (
signatureoptions "github.com/sigstore/sigstore/pkg/signature/options"
)

const (
predicateCustom = "custom"
predicateSlsa = "slsaprovenance"
predicateSpdx = "spdx"
predicateLink = "link"
)

var predicateTypeMap = map[string]string{
predicateCustom: attestation.CosignCustomProvenanceV01,
predicateSlsa: in_toto.PredicateSLSAProvenanceV01,
predicateSpdx: in_toto.PredicateSPDX,
predicateLink: in_toto.PredicateLinkV1,
}

//nolint
func AttestCmd(ctx context.Context, ko sign.KeyOpts, regOpts options.RegistryOptions, imageRef string, certPath string,
upload bool, predicatePath string, force bool, predicateType string) error {
Expand All @@ -73,19 +57,16 @@ func AttestCmd(ctx context.Context, ko sign.KeyOpts, regOpts options.RegistryOpt
}
}

predicateURI, ok := predicateTypeMap[predicateType]
if !ok {
if _, err := url.ParseRequestURI(predicateType); err != nil {
return fmt.Errorf("invalid predicate type: %s", predicateType)
} else {
predicateURI = predicateType
}
predicateURI, err := options.ParsePredicateType(predicateType)
if err != nil {
return err
}

ref, err := name.ParseReference(imageRef)
if err != nil {
return errors.Wrap(err, "parsing reference")
}

ociremoteOpts, err := regOpts.ClientOpts(ctx)
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion cmd/cosign/cli/options/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type AttestOptions struct {
Rekor RekorOptions
Fulcio FulcioOptions
SecurityKey SecurityKeyOptions
Predicate PredicateOptions
Predicate PredicateLocalOptions
Registry RegistryOptions
}

Expand Down
63 changes: 60 additions & 3 deletions cmd/cosign/cli/options/predicate.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,79 @@
package options

import (
"fmt"
"net/url"

"github.com/in-toto/in-toto-golang/in_toto"
"github.com/spf13/cobra"

"github.com/sigstore/cosign/pkg/cosign/attestation"
)

const (
PredicateCustom = "custom"
PredicateSLSA = "slsaprovenance"
PredicateSPDX = "spdx"
PredicateLink = "link"
)

// PredicateTypeMap is the mapping between the predicate `type` option to predicate URI.
var PredicateTypeMap = map[string]string{
PredicateCustom: attestation.CosignCustomProvenanceV01,
PredicateSLSA: in_toto.PredicateSLSAProvenanceV01,
PredicateSPDX: in_toto.PredicateSPDX,
PredicateLink: in_toto.PredicateLinkV1,
}

// PredicateOptions is the wrapper for predicate related options.
type PredicateOptions struct {
Path string
Type string
}

var _ Interface = (*PredicateOptions)(nil)

// AddFlags implements Interface
func (o *PredicateOptions) AddFlags(cmd *cobra.Command) {
cmd.Flags().StringVar(&o.Type, "type", "custom",
"specify a predicate type (slsaprovenance|link|spdx|custom) or an URI")
}

// ParsePredicateType parses the predicate `type` flag passed into a predicate URI, or validates `type` is a valid URI.
func ParsePredicateType(t string) (string, error) {
uri, ok := PredicateTypeMap[t]
if !ok {
if _, err := url.ParseRequestURI(t); err != nil {
return "", fmt.Errorf("invalid predicate type: %s", t)
}
uri = t
}
return uri, nil
}

// PredicateLocalOptions is the wrapper for predicate related options.
type PredicateLocalOptions struct {
PredicateOptions
Path string
}

var _ Interface = (*PredicateLocalOptions)(nil)

// AddFlags implements Interface
func (o *PredicateLocalOptions) AddFlags(cmd *cobra.Command) {
o.PredicateOptions.AddFlags(cmd)

cmd.Flags().StringVar(&o.Path, "predicate", "",
"path to the predicate file.")
}

cmd.Flags().StringVar(&o.Type, "type", "custom",
"specify a predicate type (slsaprovenance|link|spdx|custom) or an URI")
// PredicateRemoteOptions is the wrapper for remote predicate related options.
type PredicateRemoteOptions struct {
PredicateOptions
}

var _ Interface = (*PredicateRemoteOptions)(nil)

// AddFlags implements Interface
func (o *PredicateRemoteOptions) AddFlags(cmd *cobra.Command) {
o.PredicateOptions.AddFlags(cmd)
}
6 changes: 6 additions & 0 deletions cmd/cosign/cli/options/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ type VerifyAttestationOptions struct {
Rekor RekorOptions
Fulcio FulcioOptions // TODO: the original command did not use id token, mistake?
Registry RegistryOptions
Predicate PredicateRemoteOptions
Policies []string
}

var _ Interface = (*VerifyAttestationOptions)(nil)
Expand All @@ -79,13 +81,17 @@ func (o *VerifyAttestationOptions) AddFlags(cmd *cobra.Command) {
o.Rekor.AddFlags(cmd)
o.Fulcio.AddFlags(cmd)
o.Registry.AddFlags(cmd)
o.Predicate.AddFlags(cmd)

cmd.Flags().StringVar(&o.Key, "key", "",
"path to the private key file, KMS URI or Kubernetes Secret")

cmd.Flags().BoolVar(&o.CheckClaims, "check-claims", true,
"whether to check the claims found")

cmd.Flags().StringSliceVar(&o.Policies, "policy", nil,
"specify CUE or Rego files will be using for validation")

cmd.Flags().StringVarP(&o.Output, "output", "o", "json",
"output format for the signing image information (json|text)")
}
Expand Down
2 changes: 2 additions & 0 deletions cmd/cosign/cli/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ against the transparency log.`,
Output: o.Output,
RekorURL: o.Rekor.URL,
FulcioURL: o.Fulcio.URL,
PredicateType: o.Predicate.Type,
Policies: o.Policies,
}
return v.Exec(cmd.Context(), args)
},
Expand Down
143 changes: 136 additions & 7 deletions cmd/cosign/cli/verify/verify_attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,23 @@ package verify

import (
"context"
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"io"
"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/rego"

"github.com/sigstore/cosign/cmd/cosign/cli/fulcio"
"github.com/sigstore/cosign/cmd/cosign/cli/options"
"github.com/sigstore/cosign/pkg/cosign"
"github.com/sigstore/cosign/pkg/cosign/cue"
"github.com/sigstore/cosign/pkg/cosign/pivkey"
sigs "github.com/sigstore/cosign/pkg/signature"
"github.com/sigstore/sigstore/pkg/signature"
Expand All @@ -36,13 +44,15 @@ import (
// nolint
type VerifyAttestationCommand struct {
options.RegistryOptions
CheckClaims bool
KeyRef string
Sk bool
Slot string
Output string
FulcioURL string
RekorURL string
CheckClaims bool
KeyRef string
Sk bool
Slot string
Output string
FulcioURL string
RekorURL string
PredicateType string
Policies []string
}

// DSSE messages contain the signature and payload in one object, but our interface expects a signature and payload
Expand Down Expand Up @@ -117,6 +127,125 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e
return err
}

var cuePolicies, regoPolicies []string

fmt.Println(c.Policies)

for _, policy := range c.Policies {
switch filepath.Ext(policy) {
case ".rego":
regoPolicies = append(regoPolicies, policy)
case ".cue":
cuePolicies = append(cuePolicies, policy)
default:
return errors.New("invalid policy format, expected .cue or .rego")
}
}

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)
if err != nil {
return errors.Wrap(err, "unmarshal payload data")
}

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

// sanity checks
if val, ok := payloadData["payloadType"]; ok {
// we need to check only given type from the cli flag
// so we are skipping other types
if predicateURI != val {
continue
}
} else {
return fmt.Errorf("could not find 'payloadType' in payload data")
}

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")
}

var payload []byte
switch c.PredicateType {
case options.PredicateCustom:
var cosignStatement in_toto.Statement
if err := json.Unmarshal(decodedPayload, &cosignStatement); err != nil {
return fmt.Errorf("unmarshal CosignStatement: %w", err)
}
payload, err = json.Marshal(cosignStatement.Predicate)
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.Predicate)
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.Predicate)
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.Predicate)
if err != nil {
return fmt.Errorf("error when generating SPDXStatement: %w", err)
}
}

if len(cuePolicies) > 0 {
fmt.Fprintf(os.Stderr, "will be validating against CUE policies: %v\n", cuePolicies)
if err := cue.ValidateJSON(payload, cuePolicies); err != nil {
validationErrors = append(validationErrors, err)
}
}

if len(regoPolicies) > 0 {
fmt.Fprintf(os.Stderr, "will be validating against Rego policies: %v\n", regoPolicies)
if err := rego.ValidateJSON(payload, regoPolicies); err != nil {
validationErrors = append(validationErrors, err)
}
}
}

if len(validationErrors) > 0 {
fmt.Fprintf(os.Stderr, "There are %d number of errors occurred during the validation:\n", len(validationErrors))
for _, v := range validationErrors {
_, _ = fmt.Fprintf(os.Stderr, "- %v\n", v)
}
return fmt.Errorf("%d validation errors occurred", len(validationErrors))
}

// TODO: add CUE validation report to `PrintVerificationHeader`.
PrintVerificationHeader(imageRef, co, bundleVerified)
// The attestations are always JSON, so use the raw "text" mode for outputting them instead of conversion
PrintVerification(imageRef, verified, "text")
Expand Down
2 changes: 2 additions & 0 deletions doc/cosign_verify-attestation.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,16 @@ require (

require (
cloud.google.com/go/kms v1.0.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
cuelang.org/go v0.4.0
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/envoyproxy/protoc-gen-validate v0.6.1 // indirect
github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/google/go-containerregistry/pkg/authn/k8schain v0.0.0-20211004163346-9ae11fe20941
github.com/imdario/mergo v0.3.12 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/onsi/gomega v1.16.0 // indirect
github.com/open-policy-agent/opa v0.33.1
github.com/prometheus/procfs v0.7.3 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.1.0
github.com/tent/canonical-json-go v0.0.0-20130607151641-96e4ba3a7613
Expand Down
Loading

0 comments on commit b0408bf

Please sign in to comment.