Skip to content

Commit

Permalink
Add support for new bundle specification in cosign verify-attestation
Browse files Browse the repository at this point in the history
Signed-off-by: Cody Soyland <[email protected]>
  • Loading branch information
codysoyland committed Oct 30, 2024
1 parent 3818a1d commit e46acdb
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 11 deletions.
4 changes: 4 additions & 0 deletions cmd/cosign/cli/options/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ type CertVerifyOptions struct {
CertChain string
SCT string
IgnoreSCT bool
ExpectSigstoreBundle bool
TrustedRootPath string
}

var _ Interface = (*RekorOptions)(nil)
Expand Down Expand Up @@ -103,6 +105,8 @@ func (o *CertVerifyOptions) AddFlags(cmd *cobra.Command) {
cmd.Flags().BoolVar(&o.IgnoreSCT, "insecure-ignore-sct", false,
"when set, verification will not check that a certificate contains an embedded SCT, a proof of "+
"inclusion in a certificate transparency log")
cmd.Flags().StringVar(&o.TrustedRootPath, "trusted-root", "", "Path to a Sigstore TrustedRoot JSON file.")
cmd.Flags().BoolVar(&o.ExpectSigstoreBundle, "expect-sigstore-bundle", false, "expect the signature/attestation to be packaged in a Sigstore bundle")
}

func (o *CertVerifyOptions) Identities() ([]cosign.Identity, error) {
Expand Down
7 changes: 0 additions & 7 deletions cmd/cosign/cli/options/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,6 @@ type VerifyBlobOptions struct {
Signature string
BundlePath string
NewBundleFormat bool
TrustedRootPath string

SecurityKey SecurityKeyOptions
CertVerify CertVerifyOptions
Expand Down Expand Up @@ -194,9 +193,6 @@ func (o *VerifyBlobOptions) AddFlags(cmd *cobra.Command) {
cmd.Flags().BoolVar(&o.NewBundleFormat, "new-bundle-format", false,
"output bundle in new format that contains all verification material")

cmd.Flags().StringVar(&o.TrustedRootPath, "trusted-root", "",
"path to trusted root FILE")

cmd.Flags().StringVar(&o.RFC3161TimestampPath, "rfc3161-timestamp", "",
"path to RFC3161 timestamp FILE")
}
Expand Down Expand Up @@ -259,9 +255,6 @@ func (o *VerifyBlobAttestationOptions) AddFlags(cmd *cobra.Command) {
cmd.Flags().BoolVar(&o.NewBundleFormat, "new-bundle-format", false,
"output bundle in new format that contains all verification material")

cmd.Flags().StringVar(&o.TrustedRootPath, "trusted-root", "",
"path to trusted root FILE")

cmd.Flags().BoolVar(&o.CheckClaims, "check-claims", true,
"if true, verifies the provided blob's sha256 digest exists as an in-toto subject within the attestation. If false, only the DSSE envelope is verified.")

Expand Down
2 changes: 1 addition & 1 deletion cmd/cosign/cli/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ The blob may be specified as a path to a file or - for stdin.`,
CARoots: o.CertVerify.CARoots,
CAIntermediates: o.CertVerify.CAIntermediates,
SigRef: o.Signature,
TrustedRootPath: o.TrustedRootPath,
TrustedRootPath: o.CertVerify.TrustedRootPath,
CertGithubWorkflowTrigger: o.CertVerify.CertGithubWorkflowTrigger,
CertGithubWorkflowSHA: o.CertVerify.CertGithubWorkflowSha,
CertGithubWorkflowName: o.CertVerify.CertGithubWorkflowName,
Expand Down
10 changes: 10 additions & 0 deletions cmd/cosign/cli/verify/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import (
"github.com/sigstore/cosign/v2/pkg/cosign/pkcs11key"
"github.com/sigstore/cosign/v2/pkg/oci"
sigs "github.com/sigstore/cosign/v2/pkg/signature"
"github.com/sigstore/sigstore-go/pkg/root"
"github.com/sigstore/sigstore/pkg/cryptoutils"
"github.com/sigstore/sigstore/pkg/signature"
"github.com/sigstore/sigstore/pkg/signature/payload"
Expand Down Expand Up @@ -144,7 +145,16 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) {
IgnoreTlog: c.IgnoreTlog,
MaxWorkers: c.MaxWorkers,
ExperimentalOCI11: c.ExperimentalOCI11,
ExpectSigstoreBundle: c.ExpectSigstoreBundle,
}

if c.TrustedRootPath != "" {
co.TrustedMaterial, err = root.NewTrustedRootFromPath(c.TrustedRootPath)
if err != nil {
return fmt.Errorf("loading trusted root: %w", err)
}
}

if c.CheckClaims {
co.ClaimVerifier = cosign.SimpleClaimVerifier
}
Expand Down
10 changes: 10 additions & 0 deletions cmd/cosign/cli/verify/verify_attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
"github.com/sigstore/cosign/v2/pkg/oci"
"github.com/sigstore/cosign/v2/pkg/policy"
sigs "github.com/sigstore/cosign/v2/pkg/signature"
"github.com/sigstore/sigstore-go/pkg/root"
)

// VerifyAttestationCommand verifies a signature on a supplied container image
Expand Down Expand Up @@ -119,6 +120,7 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e
Offline: c.Offline,
IgnoreTlog: c.IgnoreTlog,
MaxWorkers: c.MaxWorkers,
ExpectSigstoreBundle: c.ExpectSigstoreBundle,
}
if c.CheckClaims {
co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier
Expand Down Expand Up @@ -223,6 +225,14 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e
}
co.SCT = sct
}
case c.TrustedRootPath != "":
if !c.ExpectSigstoreBundle {
return fmt.Errorf("unsupported: trusted root path currently only supported with --expect-sigstore-bundle")
}
co.TrustedMaterial, err = root.NewTrustedRootFromPath(c.TrustedRootPath)
if err != nil {
return fmt.Errorf("loading trusted root: %w", err)
}
case c.CARoots != "":
// CA roots + possible intermediates are already loaded into co.RootCerts with the call to
// loadCertsKeylessVerification above.
Expand Down
154 changes: 152 additions & 2 deletions pkg/cosign/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,14 @@ import (
"github.com/sigstore/cosign/v2/internal/pkg/cosign"
"github.com/sigstore/cosign/v2/pkg/blob"
cbundle "github.com/sigstore/cosign/v2/pkg/cosign/bundle"

ocibundle "github.com/sigstore/cosign/v2/pkg/oci/bundle"
"github.com/sigstore/cosign/v2/pkg/oci/static"
"github.com/sigstore/cosign/v2/pkg/types"
protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1"
sgbundle "github.com/sigstore/sigstore-go/pkg/bundle"
"github.com/sigstore/sigstore-go/pkg/root"
sgverify "github.com/sigstore/sigstore-go/pkg/verify"

"github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer"
"github.com/google/go-containerregistry/pkg/name"
Expand Down Expand Up @@ -83,6 +89,14 @@ type Identity struct {
SubjectRegExp string
}

// type CertPool struct {
// certs []*x509.Certificate
// }

// func (c *CertPool) AddCert(cert *x509.Certificate) {
// c.certs = append(c.certs, cert)
// }

// CheckOpts are the options for checking signatures.
type CheckOpts struct {
// RegistryClientOpts are the options for interacting with the container registry.
Expand Down Expand Up @@ -163,6 +177,16 @@ type CheckOpts struct {
// Should the experimental OCI 1.1 behaviour be enabled or not.
// Defaults to false.
ExperimentalOCI11 bool

ExpectSigstoreBundle bool

// TrustedMaterial is the trusted material to use for verification.
// Currently, this is only applicable when ExpectSigstoreBundle is true.
TrustedMaterial root.TrustedMaterial

// TODO: Add the following, deprecate overlapping fields
//CertificateIdentities verify.CertificateIdentities
//VerifierOptions []verify.VerifierOption
}

// This is a substitutable signature verification function that can be used for verifying
Expand Down Expand Up @@ -495,6 +519,10 @@ func VerifyImageSignatures(ctx context.Context, signedImgRef name.Reference, co
}
}

if co.ExpectSigstoreBundle {
return nil, false, errors.New("bundle support for image signatures is not yet implemented")
}

// Enforce this up front.
if co.RootCerts == nil && co.SigVerifier == nil {
return nil, false, errors.New("one of verifier or root certs is required")
Expand Down Expand Up @@ -842,6 +870,24 @@ func loadSignatureFromFile(ctx context.Context, sigRef string, signedImgRef name
targetSig = []byte(sigRef)
}

if co.ExpectSigstoreBundle {
var bundle *sgbundle.Bundle
bundle.Bundle = new(protobundle.Bundle)

err = bundle.UnmarshalJSON(targetSig)
if err != nil {
return nil, err
}
signature, err := ocibundle.NewSignature(bundle, &ocibundle.Options{})
if err != nil {
return nil, err
}

return &fakeOCISignatures{
signatures: []oci.Signature{signature},
}, nil
}

_, err = base64.StdEncoding.DecodeString(string(targetSig))

if err == nil {
Expand Down Expand Up @@ -880,8 +926,11 @@ func loadSignatureFromFile(ctx context.Context, sigRef string, signedImgRef name
// If there were no valid attestations, we return an error.
func VerifyImageAttestations(ctx context.Context, signedImgRef name.Reference, co *CheckOpts) (checkedAttestations []oci.Signature, bundleVerified bool, err error) {
// Enforce this up front.
if co.RootCerts == nil && co.SigVerifier == nil {
return nil, false, errors.New("one of verifier or root certs is required")
if co.RootCerts == nil && co.SigVerifier == nil && co.TrustedMaterial == nil {
return nil, false, errors.New("one of verifier, root certs, or TrustedMaterial is required")
}
if co.ExpectSigstoreBundle {
return verifyImageAttestationsSigstoreBundle(ctx, signedImgRef, co)
}

// This is a carefully optimized sequence for fetching the attestations of
Expand Down Expand Up @@ -1413,3 +1462,104 @@ func verifyImageSignaturesExperimentalOCI(ctx context.Context, signedImgRef name

return verifySignatures(ctx, sigs, h, co)
}

func getBundles(ctx context.Context, signedImgRef name.Reference, co *CheckOpts) ([]*sgbundle.Bundle, *v1.Hash, error) {
// Enforce this up front.
if co.TrustedMaterial == nil {
return nil, nil, errors.New("Sigstore bundle verification requires TrustedMaterial")
}

// This is a carefully optimized sequence for fetching the signatures of the
// entity that minimizes registry requests when supplied with a digest input
digest, err := ociremote.ResolveDigest(signedImgRef, co.RegistryClientOpts...)
if err != nil {
if terr := (&transport.Error{}); errors.As(err, &terr) && terr.StatusCode == http.StatusNotFound {
return nil, nil, &ErrImageTagNotFound{
fmt.Errorf("image tag not found: %w", err),
}
}
return nil, nil, err
}
h, err := v1.NewHash(digest.Identifier())
if err != nil {
return nil, nil, err
}

index, err := ociremote.Referrers(digest, "", co.RegistryClientOpts...)
if err != nil {
return nil, nil, err
}
var bundles = make([]*sgbundle.Bundle, 0, len(index.Manifests))
for _, result := range index.Manifests {
st, err := name.ParseReference(fmt.Sprintf("%s@%s", digest.Repository, result.Digest.String()))
if err != nil {
return nil, nil, err
}
bundle, err := ociremote.Bundle(st, co.RegistryClientOpts...)
if err != nil {
return nil, nil, err
}
bundles = append(bundles, bundle)
}

return bundles, &h, nil
}

// verifyImageAttestationsSigstoreBundle verifies attestations from attached sigstore bundles
func verifyImageAttestationsSigstoreBundle(ctx context.Context, signedImgRef name.Reference, co *CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) {
bundles, hash, err := getBundles(ctx, signedImgRef, co)
if err != nil {
return nil, false, err
}

digestBytes, err := hex.DecodeString(hash.Hex)
if err != nil {
return nil, false, err
}

// TODO: build verifierConfig from CheckOpts
verifierConfig := []sgverify.VerifierOption{
sgverify.WithSignedCertificateTimestamps(1),
sgverify.WithTransparencyLog(1),
sgverify.WithIntegratedTimestamps(1),
}

sev, err := sgverify.NewSignedEntityVerifier(co.TrustedMaterial, verifierConfig...)
if err != nil {
return nil, false, err
}

policyOptions := make([]sgverify.PolicyOption, 0, len(co.Identities))
for _, i := range co.Identities {
id, err := sgverify.NewShortCertificateIdentity(i.Issuer, i.IssuerRegExp, i.Subject, i.SubjectRegExp)
if err != nil {
return nil, false, err
}
policyOptions = append(policyOptions, sgverify.WithCertificateIdentity(id))
}
policy := sgverify.NewPolicy(sgverify.WithArtifactDigest(hash.Algorithm, digestBytes), policyOptions...)

checkedSignatures = make([]oci.Signature, 0, len(bundles))
for _, bundle := range bundles {
_, err := sev.Verify(bundle, policy)
if err != nil {
continue
}
dsse, ok := bundle.Content.(*protobundle.Bundle_DsseEnvelope)
if !ok {
continue
}
payload, err := json.Marshal(dsse.DsseEnvelope)
if err != nil {
continue
}
// TODO: Add additional data to oci.Signature (Cert, Rekor Bundle, Timestamp, etc)
sig, err := static.NewAttestation(payload)
if err != nil {
continue
}
checkedSignatures = append(checkedSignatures, sig)
bundleVerified = true
}
return checkedSignatures, bundleVerified, nil
}
4 changes: 3 additions & 1 deletion pkg/oci/remote/referrers.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import (
func Referrers(d name.Digest, artifactType string, opts ...Option) (*v1.IndexManifest, error) {
o := makeOptions(name.Repository{}, opts...)
rOpt := o.ROpt
rOpt = append(rOpt, remote.WithFilter("artifactType", artifactType))
if artifactType != "" {
rOpt = append(rOpt, remote.WithFilter("artifactType", artifactType))
}
idx, err := remote.Referrers(d, rOpt...)
if err != nil {
return nil, err
Expand Down
24 changes: 24 additions & 0 deletions pkg/oci/remote/signatures.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package remote

import (
"errors"
"io"
"net/http"

"github.com/google/go-containerregistry/pkg/name"
Expand All @@ -26,6 +27,7 @@ import (
"github.com/sigstore/cosign/v2/pkg/oci"
"github.com/sigstore/cosign/v2/pkg/oci/empty"
"github.com/sigstore/cosign/v2/pkg/oci/internal/signature"
sgbundle "github.com/sigstore/sigstore-go/pkg/bundle"
)

const maxLayers = 1000
Expand All @@ -49,6 +51,28 @@ func Signatures(ref name.Reference, opts ...Option) (oci.Signatures, error) {
}, nil
}

func Bundle(ref name.Reference, opts ...Option) (*sgbundle.Bundle, error) {
signatures, err := Signatures(ref, opts...)
if err != nil {
return nil, err
}
layers, err := signatures.(*sigs).Image.Layers()
if err != nil {
return nil, err
}
layer0, err := layers[0].Uncompressed()
if err != nil {
return nil, err
}
bundleBytes, err := io.ReadAll(layer0)
if err != nil {
return nil, err
}
b := &sgbundle.Bundle{}
err = b.UnmarshalJSON(bundleBytes)
return b, err
}

type sigs struct {
v1.Image
}
Expand Down

0 comments on commit e46acdb

Please sign in to comment.