Skip to content

Commit 4af8cc0

Browse files
committed
Add support for new bundle specification in cosign verify-attestation
Signed-off-by: Cody Soyland <[email protected]>
1 parent 677a262 commit 4af8cc0

File tree

8 files changed

+204
-11
lines changed

8 files changed

+204
-11
lines changed

cmd/cosign/cli/options/certificate.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ type CertVerifyOptions struct {
3838
CertChain string
3939
SCT string
4040
IgnoreSCT bool
41+
ExpectSigstoreBundle bool
42+
TrustedRootPath string
4143
}
4244

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

108112
func (o *CertVerifyOptions) Identities() ([]cosign.Identity, error) {

cmd/cosign/cli/options/verify.go

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,6 @@ type VerifyBlobOptions struct {
162162
Signature string
163163
BundlePath string
164164
NewBundleFormat bool
165-
TrustedRootPath string
166165

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

197-
cmd.Flags().StringVar(&o.TrustedRootPath, "trusted-root", "",
198-
"path to trusted root FILE")
199-
200196
cmd.Flags().StringVar(&o.RFC3161TimestampPath, "rfc3161-timestamp", "",
201197
"path to RFC3161 timestamp FILE")
202198
}
@@ -259,9 +255,6 @@ func (o *VerifyBlobAttestationOptions) AddFlags(cmd *cobra.Command) {
259255
cmd.Flags().BoolVar(&o.NewBundleFormat, "new-bundle-format", false,
260256
"output bundle in new format that contains all verification material")
261257

262-
cmd.Flags().StringVar(&o.TrustedRootPath, "trusted-root", "",
263-
"path to trusted root FILE")
264-
265258
cmd.Flags().BoolVar(&o.CheckClaims, "check-claims", true,
266259
"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.")
267260

cmd/cosign/cli/verify.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ The blob may be specified as a path to a file or - for stdin.`,
345345
CARoots: o.CertVerify.CARoots,
346346
CAIntermediates: o.CertVerify.CAIntermediates,
347347
SigRef: o.Signature,
348-
TrustedRootPath: o.TrustedRootPath,
348+
TrustedRootPath: o.CertVerify.TrustedRootPath,
349349
CertGithubWorkflowTrigger: o.CertVerify.CertGithubWorkflowTrigger,
350350
CertGithubWorkflowSHA: o.CertVerify.CertGithubWorkflowSha,
351351
CertGithubWorkflowName: o.CertVerify.CertGithubWorkflowName,

cmd/cosign/cli/verify/verify.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import (
4141
"github.com/sigstore/cosign/v2/pkg/cosign/pkcs11key"
4242
"github.com/sigstore/cosign/v2/pkg/oci"
4343
sigs "github.com/sigstore/cosign/v2/pkg/signature"
44+
"github.com/sigstore/sigstore-go/pkg/root"
4445
"github.com/sigstore/sigstore/pkg/cryptoutils"
4546
"github.com/sigstore/sigstore/pkg/signature"
4647
"github.com/sigstore/sigstore/pkg/signature/payload"
@@ -144,7 +145,16 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) {
144145
IgnoreTlog: c.IgnoreTlog,
145146
MaxWorkers: c.MaxWorkers,
146147
ExperimentalOCI11: c.ExperimentalOCI11,
148+
ExpectSigstoreBundle: c.ExpectSigstoreBundle,
147149
}
150+
151+
if c.TrustedRootPath != "" {
152+
co.TrustedMaterial, err = root.NewTrustedRootFromPath(c.TrustedRootPath)
153+
if err != nil {
154+
return fmt.Errorf("loading trusted root: %w", err)
155+
}
156+
}
157+
148158
if c.CheckClaims {
149159
co.ClaimVerifier = cosign.SimpleClaimVerifier
150160
}

cmd/cosign/cli/verify/verify_attestation.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"github.com/sigstore/cosign/v2/pkg/oci"
3838
"github.com/sigstore/cosign/v2/pkg/policy"
3939
sigs "github.com/sigstore/cosign/v2/pkg/signature"
40+
"github.com/sigstore/sigstore-go/pkg/root"
4041
)
4142

4243
// VerifyAttestationCommand verifies a signature on a supplied container image
@@ -119,6 +120,7 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e
119120
Offline: c.Offline,
120121
IgnoreTlog: c.IgnoreTlog,
121122
MaxWorkers: c.MaxWorkers,
123+
ExpectSigstoreBundle: c.ExpectSigstoreBundle,
122124
}
123125
if c.CheckClaims {
124126
co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier
@@ -223,6 +225,14 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e
223225
}
224226
co.SCT = sct
225227
}
228+
case c.TrustedRootPath != "":
229+
if !c.ExpectSigstoreBundle {
230+
return fmt.Errorf("unsupported: trusted root path currently only supported with --expect-sigstore-bundle")
231+
}
232+
co.TrustedMaterial, err = root.NewTrustedRootFromPath(c.TrustedRootPath)
233+
if err != nil {
234+
return fmt.Errorf("loading trusted root: %w", err)
235+
}
226236
case c.CARoots != "":
227237
// CA roots + possible intermediates are already loaded into co.RootCerts with the call to
228238
// loadCertsKeylessVerification above.

pkg/cosign/verify.go

Lines changed: 152 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,14 @@ import (
4343
"github.com/sigstore/cosign/v2/internal/pkg/cosign"
4444
"github.com/sigstore/cosign/v2/pkg/blob"
4545
cbundle "github.com/sigstore/cosign/v2/pkg/cosign/bundle"
46+
47+
ocibundle "github.com/sigstore/cosign/v2/pkg/oci/bundle"
4648
"github.com/sigstore/cosign/v2/pkg/oci/static"
4749
"github.com/sigstore/cosign/v2/pkg/types"
50+
protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1"
51+
sgbundle "github.com/sigstore/sigstore-go/pkg/bundle"
52+
"github.com/sigstore/sigstore-go/pkg/root"
53+
sgverify "github.com/sigstore/sigstore-go/pkg/verify"
4854

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

92+
// type CertPool struct {
93+
// certs []*x509.Certificate
94+
// }
95+
96+
// func (c *CertPool) AddCert(cert *x509.Certificate) {
97+
// c.certs = append(c.certs, cert)
98+
// }
99+
86100
// CheckOpts are the options for checking signatures.
87101
type CheckOpts struct {
88102
// RegistryClientOpts are the options for interacting with the container registry.
@@ -163,6 +177,16 @@ type CheckOpts struct {
163177
// Should the experimental OCI 1.1 behaviour be enabled or not.
164178
// Defaults to false.
165179
ExperimentalOCI11 bool
180+
181+
ExpectSigstoreBundle bool
182+
183+
// TrustedMaterial is the trusted material to use for verification.
184+
// Currently, this is only applicable when ExpectSigstoreBundle is true.
185+
TrustedMaterial root.TrustedMaterial
186+
187+
// TODO: Add the following, deprecate overlapping fields
188+
//CertificateIdentities verify.CertificateIdentities
189+
//VerifierOptions []verify.VerifierOption
166190
}
167191

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

522+
if co.ExpectSigstoreBundle {
523+
return nil, false, errors.New("bundle support for image signatures is not yet implemented")
524+
}
525+
498526
// Enforce this up front.
499527
if co.RootCerts == nil && co.SigVerifier == nil {
500528
return nil, false, errors.New("one of verifier or root certs is required")
@@ -842,6 +870,24 @@ func loadSignatureFromFile(ctx context.Context, sigRef string, signedImgRef name
842870
targetSig = []byte(sigRef)
843871
}
844872

873+
if co.ExpectSigstoreBundle {
874+
var bundle *sgbundle.Bundle
875+
bundle.Bundle = new(protobundle.Bundle)
876+
877+
err = bundle.UnmarshalJSON(targetSig)
878+
if err != nil {
879+
return nil, err
880+
}
881+
signature, err := ocibundle.NewSignature(bundle, &ocibundle.Options{})
882+
if err != nil {
883+
return nil, err
884+
}
885+
886+
return &fakeOCISignatures{
887+
signatures: []oci.Signature{signature},
888+
}, nil
889+
}
890+
845891
_, err = base64.StdEncoding.DecodeString(string(targetSig))
846892

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

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

14141463
return verifySignatures(ctx, sigs, h, co)
14151464
}
1465+
1466+
func getBundles(ctx context.Context, signedImgRef name.Reference, co *CheckOpts) ([]*sgbundle.Bundle, *v1.Hash, error) {
1467+
// Enforce this up front.
1468+
if co.TrustedMaterial == nil {
1469+
return nil, nil, errors.New("Sigstore bundle verification requires TrustedMaterial")
1470+
}
1471+
1472+
// This is a carefully optimized sequence for fetching the signatures of the
1473+
// entity that minimizes registry requests when supplied with a digest input
1474+
digest, err := ociremote.ResolveDigest(signedImgRef, co.RegistryClientOpts...)
1475+
if err != nil {
1476+
if terr := (&transport.Error{}); errors.As(err, &terr) && terr.StatusCode == http.StatusNotFound {
1477+
return nil, nil, &ErrImageTagNotFound{
1478+
fmt.Errorf("image tag not found: %w", err),
1479+
}
1480+
}
1481+
return nil, nil, err
1482+
}
1483+
h, err := v1.NewHash(digest.Identifier())
1484+
if err != nil {
1485+
return nil, nil, err
1486+
}
1487+
1488+
index, err := ociremote.Referrers(digest, "", co.RegistryClientOpts...)
1489+
if err != nil {
1490+
return nil, nil, err
1491+
}
1492+
var bundles = make([]*sgbundle.Bundle, 0, len(index.Manifests))
1493+
for _, result := range index.Manifests {
1494+
st, err := name.ParseReference(fmt.Sprintf("%s@%s", digest.Repository, result.Digest.String()))
1495+
if err != nil {
1496+
return nil, nil, err
1497+
}
1498+
bundle, err := ociremote.Bundle(st, co.RegistryClientOpts...)
1499+
if err != nil {
1500+
return nil, nil, err
1501+
}
1502+
bundles = append(bundles, bundle)
1503+
}
1504+
1505+
return bundles, &h, nil
1506+
}
1507+
1508+
// verifyImageAttestationsSigstoreBundle verifies attestations from attached sigstore bundles
1509+
func verifyImageAttestationsSigstoreBundle(ctx context.Context, signedImgRef name.Reference, co *CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) {
1510+
bundles, hash, err := getBundles(ctx, signedImgRef, co)
1511+
if err != nil {
1512+
return nil, false, err
1513+
}
1514+
1515+
digestBytes, err := hex.DecodeString(hash.Hex)
1516+
if err != nil {
1517+
return nil, false, err
1518+
}
1519+
1520+
// TODO: build verifierConfig from CheckOpts
1521+
verifierConfig := []sgverify.VerifierOption{
1522+
sgverify.WithSignedCertificateTimestamps(1),
1523+
sgverify.WithTransparencyLog(1),
1524+
sgverify.WithIntegratedTimestamps(1),
1525+
}
1526+
1527+
sev, err := sgverify.NewSignedEntityVerifier(co.TrustedMaterial, verifierConfig...)
1528+
if err != nil {
1529+
return nil, false, err
1530+
}
1531+
1532+
policyOptions := make([]sgverify.PolicyOption, 0, len(co.Identities))
1533+
for _, i := range co.Identities {
1534+
id, err := sgverify.NewShortCertificateIdentity(i.Issuer, i.IssuerRegExp, i.Subject, i.SubjectRegExp)
1535+
if err != nil {
1536+
return nil, false, err
1537+
}
1538+
policyOptions = append(policyOptions, sgverify.WithCertificateIdentity(id))
1539+
}
1540+
policy := sgverify.NewPolicy(sgverify.WithArtifactDigest(hash.Algorithm, digestBytes), policyOptions...)
1541+
1542+
checkedSignatures = make([]oci.Signature, 0, len(bundles))
1543+
for _, bundle := range bundles {
1544+
_, err := sev.Verify(bundle, policy)
1545+
if err != nil {
1546+
continue
1547+
}
1548+
dsse, ok := bundle.Content.(*protobundle.Bundle_DsseEnvelope)
1549+
if !ok {
1550+
continue
1551+
}
1552+
payload, err := json.Marshal(dsse.DsseEnvelope)
1553+
if err != nil {
1554+
continue
1555+
}
1556+
// TODO: Add additional data to oci.Signature (Cert, Rekor Bundle, Timestamp, etc)
1557+
sig, err := static.NewAttestation(payload)
1558+
if err != nil {
1559+
continue
1560+
}
1561+
checkedSignatures = append(checkedSignatures, sig)
1562+
bundleVerified = true
1563+
}
1564+
return checkedSignatures, bundleVerified, nil
1565+
}

pkg/oci/remote/referrers.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ import (
2525
func Referrers(d name.Digest, artifactType string, opts ...Option) (*v1.IndexManifest, error) {
2626
o := makeOptions(name.Repository{}, opts...)
2727
rOpt := o.ROpt
28-
rOpt = append(rOpt, remote.WithFilter("artifactType", artifactType))
28+
if artifactType != "" {
29+
rOpt = append(rOpt, remote.WithFilter("artifactType", artifactType))
30+
}
2931
idx, err := remote.Referrers(d, rOpt...)
3032
if err != nil {
3133
return nil, err

pkg/oci/remote/signatures.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package remote
1717

1818
import (
1919
"errors"
20+
"io"
2021
"net/http"
2122

2223
"github.com/google/go-containerregistry/pkg/name"
@@ -26,6 +27,7 @@ import (
2627
"github.com/sigstore/cosign/v2/pkg/oci"
2728
"github.com/sigstore/cosign/v2/pkg/oci/empty"
2829
"github.com/sigstore/cosign/v2/pkg/oci/internal/signature"
30+
sgbundle "github.com/sigstore/sigstore-go/pkg/bundle"
2931
)
3032

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

54+
func Bundle(ref name.Reference, opts ...Option) (*sgbundle.Bundle, error) {
55+
signatures, err := Signatures(ref, opts...)
56+
if err != nil {
57+
return nil, err
58+
}
59+
layers, err := signatures.(*sigs).Image.Layers()
60+
if err != nil {
61+
return nil, err
62+
}
63+
layer0, err := layers[0].Uncompressed()
64+
if err != nil {
65+
return nil, err
66+
}
67+
bundleBytes, err := io.ReadAll(layer0)
68+
if err != nil {
69+
return nil, err
70+
}
71+
b := &sgbundle.Bundle{}
72+
err = b.UnmarshalJSON(bundleBytes)
73+
return b, err
74+
}
75+
5276
type sigs struct {
5377
v1.Image
5478
}

0 commit comments

Comments
 (0)