Skip to content

Commit a168a42

Browse files
committed
add SignVSA function and unit tests for VSA signing
Introduce the SignVSA function, which signs a Verification Summary Attestation (VSA) JSON file using a cosign-compatible private key. The function writes a detached, base64-encoded signature alongside the VSA and returns the absolute path to the signature file. It handles key loading, passphrase support, payload reading, signing, and signature persistence with robust error handling. Add unit tests for SignVSA, covering: - Successful signing with a valid cosign private key - Error handling for missing key files - Error handling for missing VSA files https://issues.redhat.com/browse/EC-1308
1 parent d8c2e8b commit a168a42

File tree

6 files changed

+636
-116
lines changed

6 files changed

+636
-116
lines changed

cmd/validate/image.go

Lines changed: 48 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -458,11 +458,31 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command {
458458
}
459459

460460
if data.vsaEnabled {
461+
generator := vsa.NewGenerator(report)
462+
writer := vsa.NewWriter()
461463
for _, comp := range components {
462-
if err := processVSA(cmd.Context(), report, comp); err != nil {
463-
log.Errorf("[VSA] Error processing VSA for image %s: %v", comp.ContainerImage, err)
464+
writtenPath, err := generateAndWrite(cmd.Context(), generator, writer, comp)
465+
if err != nil {
466+
log.Error(err)
464467
continue
465468
}
469+
470+
signer, err := vsa.NewSigner(data.vsaSigningKey, utils.FS(cmd.Context()))
471+
if err != nil {
472+
log.Error(err)
473+
continue
474+
}
475+
attestor, err := vsa.NewAttestOptions(writtenPath, comp.Source.GitSource.URL, comp.ContainerImage, signer)
476+
if err != nil {
477+
log.Error(err)
478+
continue
479+
}
480+
envelope, err := attestVSA(cmd.Context(), attestor, comp)
481+
if err != nil {
482+
log.Error(err)
483+
continue
484+
}
485+
log.Infof("[VSA] VSA attested and envelope written to %s", envelope)
466486
}
467487
}
468488
if data.strict && !report.Success {
@@ -581,50 +601,42 @@ func containsOutput(data []string, value string) bool {
581601
return false
582602
}
583603

584-
// PredicateGenerator defines the interface for generating VSA predicates
585-
type PredicateGenerator interface {
586-
GeneratePredicate(ctx context.Context, report applicationsnapshot.Report, comp applicationsnapshot.Component) (*vsa.Predicate, error)
604+
// Create interfaces for the VSA components for easier testing
605+
type Generator interface {
606+
GeneratePredicate(ctx context.Context, comp applicationsnapshot.Component) (*vsa.Predicate, error)
607+
}
608+
609+
type Writer interface {
610+
WriteVSA(pred *vsa.Predicate) (string, error)
587611
}
588612

589-
// VSAWriter defines the interface for writing VSA files
590-
type VSAWriter interface {
591-
WriteVSA(predicate *vsa.Predicate) (string, error)
613+
type Attestor interface {
614+
AttestPredicate(ctx context.Context) ([]byte, error)
615+
WriteEnvelope(data []byte) (string, error)
592616
}
593617

594-
// generateAndWriteVSA generates a VSA predicate and writes it to a file
595-
func generateAndWriteVSA(
596-
ctx context.Context,
597-
report applicationsnapshot.Report,
598-
comp applicationsnapshot.Component,
599-
generator PredicateGenerator,
600-
writer VSAWriter,
601-
) (string, error) {
602-
log.Debugf("[VSA] Generating predicate for image: %s", comp.ContainerImage)
603-
pred, err := generator.GeneratePredicate(ctx, report, comp)
618+
// attestVSA handles VSA attestation and envelope writing for a single component.
619+
func attestVSA(ctx context.Context, attestor Attestor, comp applicationsnapshot.Component) (string, error) {
620+
env, err := attestor.AttestPredicate(ctx)
604621
if err != nil {
605-
return "", fmt.Errorf("failed to generate predicate for image %s: %w", comp.ContainerImage, err)
622+
return "", fmt.Errorf("[VSA] Error attesting VSA for image %s: %w", comp.ContainerImage, err)
606623
}
607-
log.Debugf("[VSA] Predicate generated for image: %s", comp.ContainerImage)
608-
609-
log.Debugf("[VSA] Writing VSA for image: %s", comp.ContainerImage)
610-
writtenPath, err := writer.WriteVSA(pred)
624+
envelopePath, err := attestor.WriteEnvelope(env)
611625
if err != nil {
612-
return "", fmt.Errorf("failed to write VSA for image %s: %w", comp.ContainerImage, err)
626+
return "", fmt.Errorf("[VSA] Error writing envelope for image %s: %w", comp.ContainerImage, err)
613627
}
614-
log.Debugf("[VSA] VSA written to %s", writtenPath)
615-
616-
return writtenPath, nil
628+
return envelopePath, nil
617629
}
618630

619-
// processVSA handles the complete VSA generation, signing and upload process for a component
620-
func processVSA(ctx context.Context, report applicationsnapshot.Report, comp applicationsnapshot.Component) error {
621-
generator := vsa.NewGenerator()
622-
writer := vsa.NewWriter()
623-
624-
vsaPath, err := generateAndWriteVSA(ctx, report, comp, generator, writer)
625-
log.Infof("[VSA] VSA written to %s", vsaPath)
631+
// generateAndWrite generates a VSA predicate and writes it to a file, returning the written path.
632+
func generateAndWrite(ctx context.Context, generator Generator, writer Writer, comp applicationsnapshot.Component) (string, error) {
633+
pred, err := generator.GeneratePredicate(ctx, comp)
626634
if err != nil {
627-
return err
635+
return "", err
628636
}
629-
return nil
637+
writtenPath, err := writer.WriteVSA(pred)
638+
if err != nil {
639+
return "", err
640+
}
641+
return writtenPath, nil
630642
}

cmd/validate/image_test.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import (
4444
"github.com/conforma/cli/internal/utils"
4545
"github.com/conforma/cli/internal/utils/oci"
4646
"github.com/conforma/cli/internal/utils/oci/fake"
47+
"github.com/conforma/cli/internal/validate/vsa"
4748
)
4849

4950
type data struct {
@@ -1362,3 +1363,165 @@ func TestContainsAttestation(t *testing.T) {
13621363
assert.Equal(t, test.expected, result, test.name)
13631364
}
13641365
}
1366+
1367+
// --- Mocks and tests for processVSAForComponent ---
1368+
1369+
type mockGenerator struct {
1370+
GeneratePredicateFunc func(ctx context.Context, comp applicationsnapshot.Component) (*vsa.Predicate, error)
1371+
}
1372+
1373+
func (m *mockGenerator) GeneratePredicate(ctx context.Context, comp applicationsnapshot.Component) (*vsa.Predicate, error) {
1374+
return m.GeneratePredicateFunc(ctx, comp)
1375+
}
1376+
1377+
type mockWriter struct {
1378+
WriteVSAFunc func(pred *vsa.Predicate) (string, error)
1379+
}
1380+
1381+
func (m *mockWriter) WriteVSA(pred *vsa.Predicate) (string, error) {
1382+
return m.WriteVSAFunc(pred)
1383+
}
1384+
1385+
type mockAttestor struct {
1386+
AttestPredicateFunc func(ctx context.Context) ([]byte, error)
1387+
WriteEnvelopeFunc func(data []byte) (string, error)
1388+
}
1389+
1390+
func (m *mockAttestor) AttestPredicate(ctx context.Context) ([]byte, error) {
1391+
return m.AttestPredicateFunc(ctx)
1392+
}
1393+
1394+
func (m *mockAttestor) WriteEnvelope(data []byte) (string, error) {
1395+
return m.WriteEnvelopeFunc(data)
1396+
}
1397+
1398+
func Test_attestVSA_success(t *testing.T) {
1399+
ctx := context.Background()
1400+
comp := applicationsnapshot.Component{
1401+
SnapshotComponent: app.SnapshotComponent{
1402+
ContainerImage: "test-image",
1403+
},
1404+
}
1405+
1406+
attestor := &mockAttestor{
1407+
AttestPredicateFunc: func(ctx context.Context) ([]byte, error) {
1408+
return []byte("envelope"), nil
1409+
},
1410+
WriteEnvelopeFunc: func(data []byte) (string, error) {
1411+
if string(data) != "envelope" {
1412+
t.Errorf("unexpected data passed to WriteEnvelope")
1413+
}
1414+
return "/tmp/envelope.json", nil
1415+
},
1416+
}
1417+
1418+
path, err := attestVSA(ctx, attestor, comp)
1419+
assert.NoError(t, err)
1420+
assert.Equal(t, "/tmp/envelope.json", path)
1421+
}
1422+
1423+
func Test_attestVSA_errors(t *testing.T) {
1424+
ctx := context.Background()
1425+
comp := applicationsnapshot.Component{
1426+
SnapshotComponent: app.SnapshotComponent{
1427+
ContainerImage: "test-image",
1428+
},
1429+
}
1430+
1431+
t.Run("attest predicate fails", func(t *testing.T) {
1432+
attestor := &mockAttestor{
1433+
AttestPredicateFunc: func(ctx context.Context) ([]byte, error) {
1434+
return nil, errors.New("attest error")
1435+
},
1436+
}
1437+
path, err := attestVSA(ctx, attestor, comp)
1438+
assert.Error(t, err)
1439+
assert.Contains(t, err.Error(), "Error attesting VSA")
1440+
assert.Empty(t, path)
1441+
})
1442+
1443+
t.Run("write envelope fails", func(t *testing.T) {
1444+
attestor := &mockAttestor{
1445+
AttestPredicateFunc: func(ctx context.Context) ([]byte, error) {
1446+
return []byte("envelope"), nil
1447+
},
1448+
WriteEnvelopeFunc: func(data []byte) (string, error) {
1449+
return "", errors.New("envelope error")
1450+
},
1451+
}
1452+
path, err := attestVSA(ctx, attestor, comp)
1453+
assert.Error(t, err)
1454+
assert.Contains(t, err.Error(), "Error writing envelope")
1455+
assert.Empty(t, path)
1456+
})
1457+
}
1458+
1459+
func Test_generateAndWrite_success(t *testing.T) {
1460+
ctx := context.Background()
1461+
comp := applicationsnapshot.Component{
1462+
SnapshotComponent: app.SnapshotComponent{
1463+
ContainerImage: "test-image",
1464+
},
1465+
}
1466+
pred := &vsa.Predicate{ImageRef: "test-image"}
1467+
1468+
gen := &mockGenerator{
1469+
GeneratePredicateFunc: func(ctx context.Context, c applicationsnapshot.Component) (*vsa.Predicate, error) {
1470+
return pred, nil
1471+
},
1472+
}
1473+
writer := &mockWriter{
1474+
WriteVSAFunc: func(p *vsa.Predicate) (string, error) {
1475+
if p != pred {
1476+
t.Errorf("unexpected predicate passed to WriteVSA")
1477+
}
1478+
return "/tmp/vsa.json", nil
1479+
},
1480+
}
1481+
1482+
path, err := generateAndWrite(ctx, gen, writer, comp)
1483+
assert.NoError(t, err)
1484+
assert.Equal(t, "/tmp/vsa.json", path)
1485+
}
1486+
1487+
func Test_generateAndWrite_errors(t *testing.T) {
1488+
ctx := context.Background()
1489+
comp := applicationsnapshot.Component{
1490+
SnapshotComponent: app.SnapshotComponent{
1491+
ContainerImage: "test-image",
1492+
},
1493+
}
1494+
pred := &vsa.Predicate{ImageRef: "test-image"}
1495+
1496+
t.Run("predicate generation fails", func(t *testing.T) {
1497+
gen := &mockGenerator{
1498+
GeneratePredicateFunc: func(ctx context.Context, c applicationsnapshot.Component) (*vsa.Predicate, error) {
1499+
return nil, errors.New("predicate generation error")
1500+
},
1501+
}
1502+
writer := &mockWriter{}
1503+
1504+
path, err := generateAndWrite(ctx, gen, writer, comp)
1505+
assert.Error(t, err)
1506+
assert.Contains(t, err.Error(), "predicate generation error")
1507+
assert.Empty(t, path)
1508+
})
1509+
1510+
t.Run("write VSA fails", func(t *testing.T) {
1511+
gen := &mockGenerator{
1512+
GeneratePredicateFunc: func(ctx context.Context, c applicationsnapshot.Component) (*vsa.Predicate, error) {
1513+
return pred, nil
1514+
},
1515+
}
1516+
writer := &mockWriter{
1517+
WriteVSAFunc: func(p *vsa.Predicate) (string, error) {
1518+
return "", errors.New("write VSA error")
1519+
},
1520+
}
1521+
1522+
path, err := generateAndWrite(ctx, gen, writer, comp)
1523+
assert.Error(t, err)
1524+
assert.Contains(t, err.Error(), "write VSA error")
1525+
assert.Empty(t, path)
1526+
})
1527+
}

0 commit comments

Comments
 (0)