Skip to content

Commit 58ea404

Browse files
committed
Add schema validation for slsa attestations
Add schema validation to SLSA v0.2 and v1.0 attestation parsers to ensure incoming attestations conform to their respective schemas before being accepted. Co-authored-by: Claude Code <[email protected]> Ref: https://issues.redhat.com/browse/EC-1581
1 parent fafc487 commit 58ea404

File tree

4 files changed

+112
-8
lines changed

4 files changed

+112
-8
lines changed

internal/attestation/slsa_provenance_02.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/sigstore/cosign/v2/pkg/oci"
2626

2727
"github.com/conforma/cli/internal/signature"
28+
"github.com/conforma/cli/pkg/schema"
2829
)
2930

3031
const (
@@ -64,6 +65,14 @@ func SLSAProvenanceFromSignature(sig oci.Signature) (Attestation, error) {
6465
return nil, fmt.Errorf("cannot create signed entity: %w", err)
6566
}
6667

68+
// Validate against SLSA v0.2 schema
69+
var schemaValidation any
70+
if err := json.Unmarshal(embedded, &schemaValidation); err == nil {
71+
if err := schema.SLSA_Provenance_v0_2.Validate(schemaValidation); err != nil {
72+
return nil, fmt.Errorf("attestation does not conform to SLSA v0.2 schema: %w", err)
73+
}
74+
}
75+
6776
return slsaProvenance{statement: statement, data: embedded, signatures: signatures}, nil
6877
}
6978

internal/attestation/slsa_provenance_02_test.go

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import (
2222
"bytes"
2323
"crypto/x509"
2424
"encoding/base64"
25-
"encoding/json"
2625
"errors"
2726
"fmt"
2827
"io"
@@ -233,18 +232,53 @@ func TestSLSAProvenanceFromSignature(t *testing.T) {
233232
},
234233
err: errors.New("unsupported attestation predicate type: kaboom"),
235234
},
235+
{
236+
name: "schema validation fails - missing subject",
237+
setup: func(l *mockSignature) {
238+
payload := encode(`{
239+
"_type": "https://in-toto.io/Statement/v0.1",
240+
"predicateType": "https://slsa.dev/provenance/v0.2",
241+
"predicate": {"builder": {"id": "https://my.builder"}, "buildType": "https://my.build.type"}
242+
}`)
243+
l.On("MediaType").Return(types.MediaType(ct.DssePayloadType), nil)
244+
l.On("Uncompressed").Return(buffy(fmt.Sprintf(`{"payload":"%s"}`, payload)), nil)
245+
l.On("Base64Signature").Return("", nil)
246+
l.On("Cert").Return(&x509.Certificate{}, nil)
247+
l.On("Chain").Return([]*x509.Certificate{}, nil)
248+
},
249+
err: errors.New("attestation does not conform to SLSA v0.2 schema: jsonschema: '' does not validate with https://slsa.dev/provenance/v0.2#/required: missing properties: 'subject'"),
250+
},
251+
{
252+
name: "schema validation fails - missing builder",
253+
setup: func(l *mockSignature) {
254+
payload := encode(`{
255+
"_type": "https://in-toto.io/Statement/v0.1",
256+
"subject": [{"name": "example.com/test", "digest": {"sha256": "abc123"}}],
257+
"predicateType": "https://slsa.dev/provenance/v0.2",
258+
"predicate": {"buildType": "https://my.build.type"}
259+
}`)
260+
l.On("MediaType").Return(types.MediaType(ct.DssePayloadType), nil)
261+
l.On("Uncompressed").Return(buffy(fmt.Sprintf(`{"payload":"%s"}`, payload)), nil)
262+
l.On("Base64Signature").Return("", nil)
263+
l.On("Cert").Return(&x509.Certificate{}, nil)
264+
l.On("Chain").Return([]*x509.Certificate{}, nil)
265+
},
266+
err: errors.New("attestation does not conform to SLSA v0.2 schema: jsonschema: '/predicate' does not validate with https://slsa.dev/provenance/v0.2#/properties/predicate/required: missing properties: 'builder'"),
267+
},
236268
{
237269
name: "cannot create entity signature",
238270
data: `{
239271
"_type": "https://in-toto.io/Statement/v0.1",
272+
"subject": [{"name": "example.com/test", "digest": {"sha256": "abc123"}}],
240273
"predicateType": "https://slsa.dev/provenance/v0.2",
241-
"predicate": {"buildType": "https://my.build.type"}
274+
"predicate": {"builder": {"id": "https://my.builder"}, "buildType": "https://my.build.type"}
242275
}`,
243276
setup: func(l *mockSignature) {
244277
payload := encode(`{
245278
"_type": "https://in-toto.io/Statement/v0.1",
279+
"subject": [{"name": "example.com/test", "digest": {"sha256": "abc123"}}],
246280
"predicateType": "https://slsa.dev/provenance/v0.2",
247-
"predicate": {"buildType":"https://my.build.type"}
281+
"predicate": {"builder": {"id": "https://my.builder"}, "buildType":"https://my.build.type"}
248282
}`)
249283
l.On("MediaType").Return(types.MediaType(ct.DssePayloadType), nil)
250284
l.On("Uncompressed").Return(buffy(fmt.Sprintf(`{"payload":"%s"}`, payload)), nil)
@@ -256,16 +290,18 @@ func TestSLSAProvenanceFromSignature(t *testing.T) {
256290
name: "valid with signature from payload",
257291
data: `{
258292
"_type": "https://in-toto.io/Statement/v0.1",
293+
"subject": [{"name": "example.com/test", "digest": {"sha256": "abc123"}}],
259294
"predicateType": "https://slsa.dev/provenance/v0.2",
260-
"predicate": {"buildType": "https://my.build.type"}
295+
"predicate": {"builder": {"id": "https://my.builder"}, "buildType": "https://my.build.type"}
261296
}`,
262297
setup: func(l *mockSignature) {
263298
sig1 := `{"keyid": "key-id-1", "sig": "sig-1"}`
264299
sig2 := `{"keyid": "key-id-2", "sig": "sig-2"}`
265300
payload := encode(`{
266301
"_type": "https://in-toto.io/Statement/v0.1",
302+
"subject": [{"name": "example.com/test", "digest": {"sha256": "abc123"}}],
267303
"predicateType": "https://slsa.dev/provenance/v0.2",
268-
"predicate": {"buildType": "https://my.build.type"}
304+
"predicate": {"builder": {"id": "https://my.builder"}, "buildType": "https://my.build.type"}
269305
}`)
270306
l.On("MediaType").Return(types.MediaType(ct.DssePayloadType), nil)
271307
l.On("Uncompressed").Return(buffy(
@@ -280,16 +316,18 @@ func TestSLSAProvenanceFromSignature(t *testing.T) {
280316
name: "valid with signature from certificate",
281317
data: `{
282318
"_type": "https://in-toto.io/Statement/v0.1",
319+
"subject": [{"name": "example.com/test", "digest": {"sha256": "abc123"}}],
283320
"predicateType": "https://slsa.dev/provenance/v0.2",
284-
"predicate": {"buildType": "https://my.build.type"}
321+
"predicate": {"builder": {"id": "https://my.builder"}, "buildType": "https://my.build.type"}
285322
}`,
286323
setup: func(l *mockSignature) {
287324
sig1 := `{"keyid": "ignored-1", "sig": "ignored-1"}`
288325
sig2 := `{"keyid": "ignored-2", "sig": "ignored-2"}`
289326
payload := encode(`{
290327
"_type": "https://in-toto.io/Statement/v0.1",
328+
"subject": [{"name": "example.com/test", "digest": {"sha256": "abc123"}}],
291329
"predicateType": "https://slsa.dev/provenance/v0.2",
292-
"predicate": {"buildType": "https://my.build.type"}
330+
"predicate": {"builder": {"id": "https://my.builder"}, "buildType": "https://my.build.type"}
293331
}`)
294332
l.On("MediaType").Return(types.MediaType(ct.DssePayloadType), nil)
295333
l.On("Uncompressed").Return(buffy(

internal/attestation/slsa_provenance_v1.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/sigstore/cosign/v2/pkg/oci"
2626

2727
"github.com/conforma/cli/internal/signature"
28+
"github.com/conforma/cli/pkg/schema"
2829
)
2930

3031
const (
@@ -64,6 +65,14 @@ func SLSAProvenanceFromSignatureV1(sig oci.Signature) (Attestation, error) {
6465
return nil, fmt.Errorf("cannot create signed entity: %w", err)
6566
}
6667

68+
// Validate against SLSA v1 schema
69+
var schemaValidation any
70+
if err := json.Unmarshal(embedded, &schemaValidation); err == nil {
71+
if err := schema.SLSA_Provenance_v1.Validate(schemaValidation); err != nil {
72+
return nil, fmt.Errorf("attestation does not conform to SLSA v1.0 schema: %w", err)
73+
}
74+
}
75+
6776
return slsaProvenanceV1{statement: statement, data: embedded, signatures: signatures}, nil
6877
}
6978

internal/attestation/slsa_provenance_v1_test.go

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
package attestation
2020

2121
import (
22-
"encoding/json"
2322
"errors"
2423
"fmt"
2524
"testing"
@@ -140,10 +139,56 @@ func TestSLSAProvenanceFromSignatureV1(t *testing.T) {
140139
},
141140
err: errors.New("unsupported attestation predicate type: kaboom"),
142141
},
142+
{
143+
name: "schema validation fails - missing subject",
144+
setup: func(l *mockSignature) {
145+
payload := encode(`{
146+
"_type": "https://in-toto.io/Statement/v0.1",
147+
"predicateType": "https://slsa.dev/provenance/v1",
148+
"predicate": {
149+
"buildDefinition": {
150+
"buildType": "https://my.build.type",
151+
"externalParameters": {}
152+
},
153+
"runDetails": {
154+
"builder": {"id": "https://my.builder"}
155+
}
156+
}
157+
}`)
158+
l.On("MediaType").Return(types.MediaType(ct.DssePayloadType), nil)
159+
l.On("Uncompressed").Return(buffy(fmt.Sprintf(`{"payload":"%s"}`, payload)), nil)
160+
l.On("Base64Signature").Return("", nil)
161+
l.On("Cert").Return(signature.ParseChainguardReleaseCert(), nil)
162+
l.On("Chain").Return(signature.ParseSigstoreChainCert(), nil)
163+
},
164+
err: errors.New("attestation does not conform to SLSA v1.0 schema: jsonschema: '' does not validate with https://slsa.dev/provenance/v1#/required: missing properties: 'subject'"),
165+
},
166+
{
167+
name: "schema validation fails - missing buildDefinition",
168+
setup: func(l *mockSignature) {
169+
payload := encode(`{
170+
"_type": "https://in-toto.io/Statement/v0.1",
171+
"subject": [{"name": "example.com/test", "digest": {"sha256": "abc123"}}],
172+
"predicateType": "https://slsa.dev/provenance/v1",
173+
"predicate": {
174+
"runDetails": {
175+
"builder": {"id": "https://my.builder"}
176+
}
177+
}
178+
}`)
179+
l.On("MediaType").Return(types.MediaType(ct.DssePayloadType), nil)
180+
l.On("Uncompressed").Return(buffy(fmt.Sprintf(`{"payload":"%s"}`, payload)), nil)
181+
l.On("Base64Signature").Return("", nil)
182+
l.On("Cert").Return(signature.ParseChainguardReleaseCert(), nil)
183+
l.On("Chain").Return(signature.ParseSigstoreChainCert(), nil)
184+
},
185+
err: errors.New("attestation does not conform to SLSA v1.0 schema: jsonschema: '/predicate' does not validate with https://slsa.dev/provenance/v1#/properties/predicate/required: missing properties: 'buildDefinition'"),
186+
},
143187
{
144188
name: "cannot create entity signature",
145189
data: `{
146190
"_type": "https://in-toto.io/Statement/v0.1",
191+
"subject": [{"name": "example.com/test", "digest": {"sha256": "abc123"}}],
147192
"predicateType": "https://slsa.dev/provenance/v1",
148193
"predicate": {
149194
"buildDefinition": {
@@ -158,6 +203,7 @@ func TestSLSAProvenanceFromSignatureV1(t *testing.T) {
158203
setup: func(l *mockSignature) {
159204
payload := encode(`{
160205
"_type": "https://in-toto.io/Statement/v0.1",
206+
"subject": [{"name": "example.com/test", "digest": {"sha256": "abc123"}}],
161207
"predicateType": "https://slsa.dev/provenance/v1",
162208
"predicate": {
163209
"buildDefinition": {
@@ -179,6 +225,7 @@ func TestSLSAProvenanceFromSignatureV1(t *testing.T) {
179225
name: "valid with signature from payload",
180226
data: `{
181227
"_type": "https://in-toto.io/Statement/v0.1",
228+
"subject": [{"name": "example.com/test", "digest": {"sha256": "abc123"}}],
182229
"predicateType": "https://slsa.dev/provenance/v1",
183230
"predicate": {
184231
"buildDefinition": {
@@ -195,6 +242,7 @@ func TestSLSAProvenanceFromSignatureV1(t *testing.T) {
195242
sig2 := `{"keyid": "key-id-2", "sig": "sig-2"}`
196243
payload := encode(`{
197244
"_type": "https://in-toto.io/Statement/v0.1",
245+
"subject": [{"name": "example.com/test", "digest": {"sha256": "abc123"}}],
198246
"predicateType": "https://slsa.dev/provenance/v1",
199247
"predicate": {
200248
"buildDefinition": {

0 commit comments

Comments
 (0)