Skip to content

Commit

Permalink
This adds code to generate a VSA (#1141)
Browse files Browse the repository at this point in the history
* This adds code to generate a VSA. The VSA is added as an output option to the validate image command.

https://issues.redhat.com/browse/EC-189
  • Loading branch information
joejstuart authored Nov 15, 2023
1 parent 5eee667 commit 4b52ce6
Show file tree
Hide file tree
Showing 13 changed files with 371 additions and 0 deletions.
18 changes: 18 additions & 0 deletions internal/applicationsnapshot/attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ package applicationsnapshot

import (
"bytes"
"encoding/json"

"github.com/in-toto/in-toto-golang/in_toto"
)

func (r *Report) renderAttestations() ([]byte, error) {
Expand All @@ -31,3 +34,18 @@ func (r *Report) renderAttestations() ([]byte, error) {

return bytes.Join(byts, []byte{'\n'}), nil
}

func (r *Report) attestations() ([]in_toto.Statement, error) {
var statements []in_toto.Statement
for _, c := range r.Components {
for _, a := range c.Attestations {
var statement in_toto.Statement
err := json.Unmarshal(a.Statement(), &statement)
if err != nil {
return []in_toto.Statement{}, nil
}
statements = append(statements, statement)
}
}
return statements, nil
}
40 changes: 40 additions & 0 deletions internal/applicationsnapshot/attestation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@
package applicationsnapshot

import (
"encoding/json"
"testing"

"github.com/gkampitakis/go-snaps/snaps"
"github.com/in-toto/in-toto-golang/in_toto"
app "github.com/redhat-appstudio/application-api/api/v1alpha1"
"github.com/stretchr/testify/assert"

"github.com/enterprise-contract/ec-cli/internal/attestation"
"github.com/enterprise-contract/ec-cli/internal/evaluator"
"github.com/enterprise-contract/ec-cli/internal/signature"
)

Expand Down Expand Up @@ -123,6 +126,39 @@ func TestAttestationReport(t *testing.T) {
}
}

func TestAttestations(t *testing.T) {
statement := in_toto.Statement{
StatementHeader: in_toto.StatementHeader{
Type: "my-type",
PredicateType: "my-predicate-type",
Subject: []in_toto.Subject{},
},
}
data, err := json.Marshal(statement)
assert.NoError(t, err)

components := []Component{
{
SnapshotComponent: app.SnapshotComponent{Name: "component1"},
Violations: []evaluator.Result{
{
Message: "violation1",
},
},
Attestations: []attestation.Attestation{
provenance{
data: data,
},
},
},
}

report := Report{Components: components}
att, err := report.attestations()
assert.NoError(t, err)
assert.Equal(t, []in_toto.Statement{statement}, att)
}

type mockAttestation struct {
data string
}
Expand All @@ -143,6 +179,10 @@ func (a mockAttestation) Signatures() []signature.EntitySignature {
return nil
}

func (a mockAttestation) Subject() []in_toto.Subject {
return []in_toto.Subject{}
}

func att(data string) attestation.Attestation {
return &mockAttestation{
data: data,
Expand Down
11 changes: 11 additions & 0 deletions internal/applicationsnapshot/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ const (
DATA = "data"
ATTESTATION = "attestation"
PolicyInput = "policy-input"
VSA = "vsa"
)

// WriteReport returns a new instance of Report representing the state of
Expand Down Expand Up @@ -191,12 +192,22 @@ func (r *Report) toFormat(format string) (data []byte, err error) {
data, err = r.renderAttestations()
case PolicyInput:
data = bytes.Join(r.PolicyInput, []byte("\n"))
case VSA:
data, err = r.toVSA()
default:
return nil, fmt.Errorf("%q is not a valid report format", format)
}
return
}

func (r *Report) toVSA() ([]byte, error) {
vsa, err := NewVSA(*r)
if err != nil {
return []byte{}, err
}
return json.Marshal(vsa)
}

// toSummary returns a condensed version of the report.
func (r *Report) toSummary() summary {
pr := summary{
Expand Down
61 changes: 61 additions & 0 deletions internal/applicationsnapshot/vsa.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright The Enterprise Contract Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

package applicationsnapshot

import (
"github.com/in-toto/in-toto-golang/in_toto"
)

const (
// Make it visible elsewhere
PredicateVSAProvenance = "https://enterprisecontract.dev/verification_summary/v1"
StatmentVSA = "https://in-toto.io/Statement/v1"
)

type ProvenanceStatementVSA struct {
in_toto.StatementHeader
Predicate Report `json:"predicate"`
}

func NewVSA(report Report) (ProvenanceStatementVSA, error) {
subjects, err := getSubjects(report)
if err != nil {
return ProvenanceStatementVSA{}, err
}

return ProvenanceStatementVSA{
StatementHeader: in_toto.StatementHeader{
Type: StatmentVSA,
PredicateType: PredicateVSAProvenance,
Subject: subjects,
},
Predicate: report,
}, nil
}

func getSubjects(report Report) ([]in_toto.Subject, error) {
statements, err := report.attestations()
if err != nil {
return []in_toto.Subject{}, err
}

var subjects []in_toto.Subject
for _, stmt := range statements {
subjects = append(subjects, stmt.Subject...)
}
return subjects, nil
}
148 changes: 148 additions & 0 deletions internal/applicationsnapshot/vsa_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright The Enterprise Contract Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

package applicationsnapshot

import (
"context"
"encoding/json"
"fmt"
"testing"

ecc "github.com/enterprise-contract/enterprise-contract-controller/api/v1alpha1"
"github.com/in-toto/in-toto-golang/in_toto"
app "github.com/redhat-appstudio/application-api/api/v1alpha1"
"github.com/stretchr/testify/assert"

"github.com/enterprise-contract/ec-cli/internal/attestation"
"github.com/enterprise-contract/ec-cli/internal/evaluator"
"github.com/enterprise-contract/ec-cli/internal/policy"
"github.com/enterprise-contract/ec-cli/internal/signature"
"github.com/enterprise-contract/ec-cli/internal/utils"
)

type provenance struct {
statement in_toto.Statement
data []byte
}

func (p provenance) Type() string {
return in_toto.StatementInTotoV01
}

func (p provenance) PredicateType() string {
return p.statement.StatementHeader.PredicateType
}

func (p provenance) Statement() []byte {
return p.data
}

func (p provenance) Signatures() []signature.EntitySignature {
return []signature.EntitySignature{}
}

func (p provenance) Subject() []in_toto.Subject {
return p.statement.Subject
}

func TestNewVSA(t *testing.T) {
components := []Component{
{
SnapshotComponent: app.SnapshotComponent{Name: "component1"},
Violations: []evaluator.Result{
{
Message: "violation1",
},
},
Attestations: []attestation.Attestation{
provenance{
statement: in_toto.Statement{},
},
},
},
}

utils.SetTestRekorPublicKey(t)

Check failure on line 79 in internal/applicationsnapshot/vsa_test.go

View workflow job for this annotation

GitHub Actions / Test

undefined: utils.SetTestRekorPublicKey

Check failure on line 79 in internal/applicationsnapshot/vsa_test.go

View workflow job for this annotation

GitHub Actions / Test

undefined: utils.SetTestRekorPublicKey
pkey := utils.TestPublicKey

Check failure on line 80 in internal/applicationsnapshot/vsa_test.go

View workflow job for this annotation

GitHub Actions / Test

undefined: utils.TestPublicKey

Check failure on line 80 in internal/applicationsnapshot/vsa_test.go

View workflow job for this annotation

GitHub Actions / Test

undefined: utils.TestPublicKey
testPolicy, err := policy.NewPolicy(context.Background(), policy.Options{
PublicKey: pkey,
EffectiveTime: policy.Now,
PolicyRef: toJson(&ecc.EnterpriseContractPolicySpec{PublicKey: pkey}),
})
assert.NoError(t, err)

report, err := NewReport("snappy", components, testPolicy, "data here", nil)
assert.NoError(t, err)

expected := ProvenanceStatementVSA{
StatementHeader: in_toto.StatementHeader{
Type: "https://in-toto.io/Statement/v1",
PredicateType: "https://enterprisecontract.dev/verification_summary/v1",
Subject: nil,
},
Predicate: report,
}
vsa, err := NewVSA(report)
assert.NoError(t, err)
assert.Equal(t, expected, vsa)
}

func TestSubjects(t *testing.T) {
expected := []in_toto.Subject{
{
Name: "my-subject",
Digest: nil,
},
}
statement := in_toto.Statement{
StatementHeader: in_toto.StatementHeader{
Subject: expected,
},
}
data, err := json.Marshal(statement)
assert.NoError(t, err)

components := []Component{
{
SnapshotComponent: app.SnapshotComponent{Name: "component1"},
Violations: []evaluator.Result{
{
Message: "violation1",
},
},
Attestations: []attestation.Attestation{
provenance{
statement: statement,
data: data,
},
},
},
}

report := Report{Components: components}
subjects, err := getSubjects(report)
assert.NoError(t, err)
assert.Equal(t, expected, subjects)
}

func toJson(policy any) string {
newInline, err := json.Marshal(policy)
if err != nil {
panic(fmt.Errorf("invalid JSON: %w", err))
}
return string(newInline)
}
5 changes: 5 additions & 0 deletions internal/attestation/attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type Attestation interface {
PredicateType() string
Statement() []byte
Signatures() []signature.EntitySignature
Subject() []in_toto.Subject
}

// Extract the payload from a DSSE signature OCI layer
Expand Down Expand Up @@ -162,6 +163,10 @@ func (p provenance) Signatures() []signature.EntitySignature {
return p.signatures
}

func (p provenance) Subject() []in_toto.Subject {
return p.statement.Subject
}

// Todo: It seems odd that this does not contain the statement.
// (See also the equivalent method in slsa_provenance_02.go)
func (p provenance) MarshalJSON() ([]byte, error) {
Expand Down
4 changes: 4 additions & 0 deletions internal/attestation/slsa_provenance_02.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ func (a slsaProvenance) Signatures() []signature.EntitySignature {
return a.signatures
}

func (a slsaProvenance) Subject() []in_toto.Subject {
return a.statement.Subject
}

// Todo: It seems odd that this does not contain the statement.
// (See also the equivalent method in attestation.go)
func (a slsaProvenance) MarshalJSON() ([]byte, error) {
Expand Down
Loading

0 comments on commit 4b52ce6

Please sign in to comment.