Skip to content

Commit bb47538

Browse files
authored
Merge pull request #3123 from SequeI/bundleAware
Support Sigstore bundle verification for cosign v3
2 parents d176d7d + 62db32b commit bb47538

9 files changed

Lines changed: 364 additions & 30 deletions

File tree

cmd/sigstore/initialize.go

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,22 +47,18 @@ func sigstoreInitializeCmd(f sigstoreInitializeFunc) *cobra.Command {
4747
Any updated TUF repository will be written to $HOME/.sigstore/root/<mirror_url>.
4848
4949
Trusted keys and certificate used in ec verification (e.g. verifying Fulcio issued certificates
50-
with Fulcio root CA) are pulled form the trusted metadata.
51-
52-
This command is mostly a wrapper around "cosign initialize".
50+
with Fulcio root CA) are pulled from the trusted metadata.
5351
`),
5452

5553
Example: hd.Doc(`
56-
ec initialize -mirror <url> -out <file>
57-
5854
Initialize root with distributed root keys, default mirror, and default out path.
59-
ec initialize
55+
ec sigstore initialize
6056
6157
Initialize with an out-of-band root key file, using the default mirror.
62-
ec initialize -root <url>
58+
ec sigstore initialize --root <url>
6359
6460
Initialize with an out-of-band root key file and custom repository mirror.
65-
ec initialize -mirror <url> -root <url>
61+
ec sigstore initialize --mirror <url> --root <url>
6662
`),
6763

6864
Args: cobra.NoArgs,

cmd/validate/image.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command {
116116
117117
ec validate image --image registry/name:tag
118118
119-
Return a zero status code even if there are validation failures:
119+
Return a zero status code even if there are validation failures:
120120
121121
ec validate image --image registry/name:tag --strict=false
122122
@@ -506,7 +506,7 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command {
506506
"URL of the certificate OIDC issuer for keyless verification")
507507

508508
cmd.Flags().StringVar(&data.certificateOIDCIssuerRegExp, "certificate-oidc-issuer-regexp", data.certificateOIDCIssuerRegExp,
509-
"Regular expresssion for the URL of the certificate OIDC issuer for keyless verification")
509+
"Regular expression for the URL of the certificate OIDC issuer for keyless verification")
510510

511511
// Deprecated: images replaced this
512512
cmd.Flags().StringVarP(&data.filePath, "file-path", "f", data.filePath,

docs/modules/ROOT/pages/ec_sigstore_initialize.adoc

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,22 @@ URL reference. This will enable you to point ec to a separate TUF root.
1616
Any updated TUF repository will be written to $HOME/.sigstore/root/<mirror_url>.
1717

1818
Trusted keys and certificate used in ec verification (e.g. verifying Fulcio issued certificates
19-
with Fulcio root CA) are pulled form the trusted metadata.
20-
21-
This command is mostly a wrapper around "cosign initialize".
19+
with Fulcio root CA) are pulled from the trusted metadata.
2220

2321
[source,shell]
2422
----
2523
ec sigstore initialize [flags]
2624
----
2725

2826
== Examples
29-
ec initialize -mirror <url> -out <file>
30-
3127
Initialize root with distributed root keys, default mirror, and default out path.
32-
ec initialize
28+
ec sigstore initialize
3329

3430
Initialize with an out-of-band root key file, using the default mirror.
35-
ec initialize -root <url>
31+
ec sigstore initialize --root <url>
3632

3733
Initialize with an out-of-band root key file and custom repository mirror.
38-
ec initialize -mirror <url> -root <url>
34+
ec sigstore initialize --mirror <url> --root <url>
3935

4036
== Options
4137

docs/modules/ROOT/pages/ec_validate_image.adoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Return a non-zero status code on validation failure:
5656

5757
ec validate image --image registry/name:tag
5858

59-
Return a zero status code even if there are validation failures:
59+
Return a zero status code even if there are validation failures:
6060

6161
ec validate image --image registry/name:tag --strict=false
6262

@@ -115,7 +115,7 @@ Use a regular expression to match certificate attributes.
115115
--certificate-identity:: URL of the certificate identity for keyless verification
116116
--certificate-identity-regexp:: Regular expression for the URL of the certificate identity for keyless verification
117117
--certificate-oidc-issuer:: URL of the certificate OIDC issuer for keyless verification
118-
--certificate-oidc-issuer-regexp:: Regular expresssion for the URL of the certificate OIDC issuer for keyless verification
118+
--certificate-oidc-issuer-regexp:: Regular expression for the URL of the certificate OIDC issuer for keyless verification
119119
--color:: Enable color when using text output even when the current terminal does not support it (Default: false)
120120
--effective-time:: Run policy checks with the provided time. Useful for testing rules with
121121
effective dates in the future. The value can be "now" (default) - for

internal/attestation/attestation.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,37 @@ func ProvenanceFromSignature(sig oci.Signature) (Attestation, error) {
143143
return provenance{statement: statement, data: embedded, signatures: signatures}, nil
144144
}
145145

146+
// ProvenanceFromBundlePayload parses an attestation from a raw DSSE envelope
147+
// JSON payload as returned by the Sigstore bundle verification path.
148+
func ProvenanceFromBundlePayload(sig oci.Signature, dsseJSON []byte) (Attestation, error) {
149+
var payload cosign.AttestationPayload
150+
if err := json.Unmarshal(dsseJSON, &payload); err != nil {
151+
return nil, fmt.Errorf("malformed bundle attestation: %w", err)
152+
}
153+
154+
if payload.PayLoad == "" {
155+
return nil, errors.New("no `payload` data found in bundle attestation")
156+
}
157+
158+
embedded, err := decodedPayload(payload)
159+
if err != nil {
160+
return nil, err
161+
}
162+
163+
//nolint:staticcheck
164+
var statement in_toto.Statement
165+
if err := json.Unmarshal(embedded, &statement); err != nil {
166+
return nil, fmt.Errorf("malformed bundle attestation: %w", err)
167+
}
168+
169+
signatures, err := createEntitySignatures(sig, payload)
170+
if err != nil {
171+
return nil, fmt.Errorf("cannot create signed entity: %w", err)
172+
}
173+
174+
return provenance{statement: statement, data: embedded, signatures: signatures}, nil
175+
}
176+
146177
type provenance struct {
147178
//nolint:staticcheck
148179
statement in_toto.Statement

internal/attestation/attestation_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ package attestation
2020

2121
import (
2222
"crypto/x509"
23+
"encoding/base64"
2324
"fmt"
2425
"testing"
2526

@@ -220,6 +221,91 @@ func TestProvenance_Signatures(t *testing.T) {
220221
}
221222
}
222223

224+
func TestProvenanceFromBundlePayload(t *testing.T) {
225+
sig1 := `{"keyid": "key-id-1", "sig": "sig-1"}`
226+
227+
payloadJson := `{
228+
"_type": "https://in-toto.io/Statement/v0.1",
229+
"predicateType": "https://cool-type.example.io/Amazing/v2.0",
230+
"predicate": {
231+
"secure": "very",
232+
"hacks": "none"
233+
}
234+
}`
235+
236+
fullAtt := fmt.Sprintf(`{"payloadType":"application/vnd.in-toto+json","signatures": [%s], "payload": "%s"}`, sig1, encode(payloadJson))
237+
238+
cases := []struct {
239+
name string
240+
setup func(l *mockSignature)
241+
dsseJSON string
242+
expectErr string
243+
}{
244+
{
245+
name: "valid bundle attestation with signature from payload",
246+
setup: func(l *mockSignature) {
247+
l.On("Base64Signature").Return("", nil)
248+
l.On("Cert").Return(&x509.Certificate{}, nil)
249+
l.On("Chain").Return([]*x509.Certificate{}, nil)
250+
},
251+
dsseJSON: fullAtt,
252+
},
253+
{
254+
name: "valid bundle attestation with signature from certificate",
255+
setup: func(l *mockSignature) {
256+
l.On("Base64Signature").Return("sig-from-cert", nil)
257+
l.On("Cert").Return(signature.ParseChainguardReleaseCert(), nil)
258+
l.On("Chain").Return(signature.ParseSigstoreChainCert(), nil)
259+
},
260+
dsseJSON: fullAtt,
261+
},
262+
{
263+
name: "malformed JSON",
264+
setup: func(l *mockSignature) {},
265+
dsseJSON: `{not json`,
266+
expectErr: "malformed bundle attestation",
267+
},
268+
{
269+
name: "empty payload field",
270+
setup: func(l *mockSignature) {},
271+
dsseJSON: `{"signatures": [], "payload": ""}`,
272+
expectErr: "no `payload` data found in bundle attestation",
273+
},
274+
{
275+
name: "invalid base64 payload",
276+
setup: func(l *mockSignature) {},
277+
dsseJSON: `{"signatures": [], "payload": "not-valid-base64!@#"}`,
278+
expectErr: "malformed attestation data",
279+
},
280+
{
281+
name: "invalid statement JSON in payload",
282+
setup: func(l *mockSignature) {},
283+
dsseJSON: fmt.Sprintf(`{"signatures": [], "payload": "%s"}`,
284+
base64.StdEncoding.EncodeToString([]byte(`not-json`))),
285+
expectErr: "malformed bundle attestation",
286+
},
287+
}
288+
289+
for _, c := range cases {
290+
t.Run(c.name, func(t *testing.T) {
291+
sig := mockSignature{&mock.Mock{}}
292+
c.setup(&sig)
293+
294+
p, err := ProvenanceFromBundlePayload(sig, []byte(c.dsseJSON))
295+
if c.expectErr != "" {
296+
assert.ErrorContains(t, err, c.expectErr)
297+
assert.Nil(t, p)
298+
return
299+
}
300+
301+
assert.NoError(t, err)
302+
assert.JSONEq(t, payloadJson, string(p.Statement()))
303+
assert.Equal(t, "https://cool-type.example.io/Amazing/v2.0", p.PredicateType())
304+
assert.NotEmpty(t, p.Signatures())
305+
})
306+
}
307+
}
308+
223309
func TestProvenance_Subject(t *testing.T) {
224310
//nolint:staticcheck
225311
mockSubject1 := in_toto.Subject{

internal/evaluation_target/application_snapshot_image/application_snapshot_image.go

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import (
3131
app "github.com/konflux-ci/application-api/api/v1alpha1"
3232
"github.com/santhosh-tekuri/jsonschema/v5"
3333
"github.com/sigstore/cosign/v3/pkg/cosign"
34+
cosignOCI "github.com/sigstore/cosign/v3/pkg/oci"
35+
ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote"
3436
log "github.com/sirupsen/logrus"
3537
"github.com/spf13/afero"
3638

@@ -120,6 +122,12 @@ func (a *ApplicationSnapshotImage) SetImageURL(url string) error {
120122
return nil
121123
}
122124

125+
func (a *ApplicationSnapshotImage) hasBundles(ctx context.Context) bool {
126+
regOpts := []ociremote.Option{ociremote.WithRemoteOptions(oci.CreateRemoteOptions(ctx)...)}
127+
bundles, _, err := cosign.GetBundles(ctx, a.reference, regOpts)
128+
return err == nil && len(bundles) > 0
129+
}
130+
123131
func (a *ApplicationSnapshotImage) FetchImageConfig(ctx context.Context) error {
124132
var err error
125133
a.configJSON, err = config.FetchImageConfig(ctx, a.reference)
@@ -143,38 +151,58 @@ func (a *ApplicationSnapshotImage) FetchImageFiles(ctx context.Context) error {
143151
return err
144152
}
145153

146-
// ValidateImageSignature executes the cosign.VerifyImageSignature method on the ApplicationSnapshotImage image ref.
154+
// ValidateImageSignature verifies the image signature. For images with Sigstore
155+
// bundles (OCI referrers) the new bundle path is used; otherwise the legacy
156+
// tag-based path is used.
147157
func (a *ApplicationSnapshotImage) ValidateImageSignature(ctx context.Context) error {
148-
// Set the ClaimVerifier on a shallow *copy* of CheckOpts to avoid unexpected side-effects
149158
opts := a.checkOpts
150-
opts.ClaimVerifier = cosign.SimpleClaimVerifier
151-
signatures, _, err := oci.NewClient(ctx).VerifyImageSignatures(a.reference, &opts)
159+
client := oci.NewClient(ctx)
160+
161+
var sigs []cosignOCI.Signature
162+
var err error
163+
164+
if a.hasBundles(ctx) {
165+
opts.NewBundleFormat = true
166+
opts.ClaimVerifier = cosign.IntotoSubjectClaimVerifier
167+
sigs, _, err = client.VerifyImageAttestations(a.reference, &opts)
168+
} else {
169+
opts.ClaimVerifier = cosign.SimpleClaimVerifier
170+
sigs, _, err = client.VerifyImageSignatures(a.reference, &opts)
171+
}
152172
if err != nil {
153173
return err
154174
}
155175

156-
for _, s := range signatures {
176+
for _, s := range sigs {
157177
es, err := signature.NewEntitySignature(s)
158178
if err != nil {
159179
return err
160180
}
161181
a.signatures = append(a.signatures, es)
162182
}
163-
164183
return nil
165184
}
166185

167-
// ValidateAttestationSignature executes the cosign.VerifyImageAttestations method
186+
// ValidateAttestationSignature verifies and collects in-toto attestations
187+
// attached to the image.
168188
func (a *ApplicationSnapshotImage) ValidateAttestationSignature(ctx context.Context) error {
169-
// Set the ClaimVerifier on a shallow *copy* of CheckOpts to avoid unexpected side-effects
170189
opts := a.checkOpts
171190
opts.ClaimVerifier = cosign.IntotoSubjectClaimVerifier
172191

192+
useBundles := a.hasBundles(ctx)
193+
if useBundles {
194+
opts.NewBundleFormat = true
195+
}
196+
173197
layers, _, err := oci.NewClient(ctx).VerifyImageAttestations(a.reference, &opts)
174198
if err != nil {
175199
return err
176200
}
177201

202+
if useBundles {
203+
return a.parseAttestationsFromBundles(layers)
204+
}
205+
178206
// Extract the signatures from the attestations here in order to also validate that
179207
// the signatures do exist in the expected format.
180208
for _, sig := range layers {
@@ -220,6 +248,40 @@ func (a *ApplicationSnapshotImage) ValidateAttestationSignature(ctx context.Cont
220248
return nil
221249
}
222250

251+
// parseAttestationsFromBundles extracts attestations from Sigstore bundles.
252+
// Bundle-wrapped layers report an incorrect media type, so we unmarshal the
253+
// DSSE envelope from the raw payload directly.
254+
func (a *ApplicationSnapshotImage) parseAttestationsFromBundles(layers []cosignOCI.Signature) error {
255+
for _, sig := range layers {
256+
payload, err := sig.Payload()
257+
if err != nil {
258+
log.Debugf("Skipping bundle entry: cannot read payload: %v", err)
259+
continue
260+
}
261+
var dsseEnvelope struct {
262+
PayloadType string `json:"payloadType"`
263+
Payload string `json:"payload"`
264+
}
265+
if err := json.Unmarshal(payload, &dsseEnvelope); err != nil {
266+
log.Debugf("Skipping bundle entry: not a valid DSSE envelope: %v", err)
267+
continue
268+
}
269+
if dsseEnvelope.PayloadType != "application/vnd.in-toto+json" {
270+
log.Debugf("Skipping bundle entry with payloadType: %s", dsseEnvelope.PayloadType)
271+
continue
272+
}
273+
274+
att, err := attestation.ProvenanceFromBundlePayload(sig, payload)
275+
if err != nil {
276+
return fmt.Errorf("unable to parse bundle attestation: %w", err)
277+
}
278+
t := att.PredicateType()
279+
log.Debugf("Found bundle attestation with predicateType: %s", t)
280+
a.attestations = append(a.attestations, att)
281+
}
282+
return nil
283+
}
284+
223285
// ValidateAttestationSyntax validates the attestations against known JSON
224286
// schemas, errors out if there are no attestations to check to prevent
225287
// successful syntax check of no inputs, must invoke

0 commit comments

Comments
 (0)