Skip to content

Commit

Permalink
This adds code to generate a VSA
Browse files Browse the repository at this point in the history
  • Loading branch information
joejstuart committed Nov 2, 2023
1 parent be05ab1 commit 59973fd
Show file tree
Hide file tree
Showing 14 changed files with 259 additions and 15 deletions.
9 changes: 9 additions & 0 deletions internal/applicationsnapshot/attestation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"testing"

"github.com/gkampitakis/go-snaps/snaps"
"github.com/in-toto/in-toto-golang/in_toto"
"github.com/stretchr/testify/assert"

app "github.com/enterprise-contract/ec-cli/application/v1alpha1"
Expand Down Expand Up @@ -143,6 +144,14 @@ func (a mockAttestation) Signatures() []signature.EntitySignature {
return nil
}

func (a mockAttestation) Digest() map[string]string {
return map[string]string{}
}

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

func att(data string) attestation.Attestation {
return &mockAttestation{
data: data,
Expand Down
21 changes: 20 additions & 1 deletion internal/attestation/attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ type Attestation interface {
PredicateType() string
Statement() []byte
Signatures() []signature.EntitySignature
Subject() []in_toto.Subject
Digest() map[string]string
}

// Extract the payload from a DSSE signature OCI layer
Expand Down Expand Up @@ -137,13 +139,20 @@ func ProvenanceFromSignature(sig oci.Signature) (Attestation, error) {
return nil, fmt.Errorf("cannot create signed entity: %w", err)
}

return provenance{statement: statement, data: embedded, signatures: signatures}, nil
digest, err := sig.Digest()
if err != nil {
return nil, err
}

return provenance{statement: statement, data: embedded, signatures: signatures, digest: map[string]string{digest.Algorithm: digest.String()}}, nil
}

type provenance struct {
statement in_toto.Statement
data []byte
signatures []signature.EntitySignature
uri string

Check failure on line 154 in internal/attestation/attestation.go

View workflow job for this annotation

GitHub Actions / Lint

field `uri` is unused (unused)
digest map[string]string
}

func (p provenance) Type() string {
Expand All @@ -162,17 +171,27 @@ func (p provenance) Signatures() []signature.EntitySignature {
return p.signatures
}

func (p provenance) Digest() map[string]string {
return p.digest
}

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) {
val := struct {
Type string `json:"type"`
PredicateType string `json:"predicateType"`
Signatures []signature.EntitySignature `json:"signatures"`
Digest map[string]string `json:"digest"`
}{
Type: p.Type(),
PredicateType: p.PredicateType(),
Signatures: p.Signatures(),
Digest: p.Digest(),
}

return json.Marshal(val)
Expand Down
18 changes: 17 additions & 1 deletion internal/attestation/slsa_provenance_02.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,19 @@ func SLSAProvenanceFromSignature(sig oci.Signature) (Attestation, error) {
return nil, fmt.Errorf("cannot create signed entity: %w", err)
}

return slsaProvenance{statement: statement, data: embedded, signatures: signatures}, nil
digest, err := sig.Digest()
if err != nil {
return nil, err
}

return slsaProvenance{statement: statement, data: embedded, signatures: signatures, digest: map[string]string{digest.Algorithm: digest.String()}}, nil
}

type slsaProvenance struct {
statement in_toto.ProvenanceStatementSLSA02
data []byte
signatures []signature.EntitySignature
digest map[string]string
}

func (a slsaProvenance) Type() string {
Expand All @@ -90,6 +96,14 @@ func (a slsaProvenance) Signatures() []signature.EntitySignature {
return a.signatures
}

func (a slsaProvenance) Digest() map[string]string {
return a.digest
}

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 All @@ -98,11 +112,13 @@ func (a slsaProvenance) MarshalJSON() ([]byte, error) {
PredicateType string `json:"predicateType"`
PredicateBuildType string `json:"predicateBuildType"`
Signatures []signature.EntitySignature `json:"signatures"`
Digest map[string]string `json:"digest"`
}{
Type: a.statement.Type,
PredicateType: a.statement.PredicateType,
PredicateBuildType: a.statement.Predicate.BuildType,
Signatures: a.signatures,
Digest: a.digest,
}

return json.Marshal(val)
Expand Down
125 changes: 125 additions & 0 deletions internal/attestation/vsa.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// 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 attestation

import (
"strings"
"time"

Check failure on line 22 in internal/attestation/vsa.go

View workflow job for this annotation

GitHub Actions / Lint

File is not `gci`-ed with --skip-generated -s standard -s default -s prefix(github.com/enterprise-contract/ec-cli) (gci)
"github.com/enterprise-contract/ec-cli/internal/evaluator"
"github.com/enterprise-contract/ec-cli/internal/policy"
"github.com/enterprise-contract/ec-cli/internal/policy/source"
"github.com/in-toto/in-toto-golang/in_toto"

Check failure on line 26 in internal/attestation/vsa.go

View workflow job for this annotation

GitHub Actions / Lint

File is not `gci`-ed with --skip-generated -s standard -s default -s prefix(github.com/enterprise-contract/ec-cli) (gci)
)

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

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

type policySource struct {
uri string
digest map[string]string

Check failure on line 42 in internal/attestation/vsa.go

View workflow job for this annotation

GitHub Actions / Lint

field `digest` is unused (unused)
}

type attestationSource struct {
uri string

Check failure on line 46 in internal/attestation/vsa.go

View workflow job for this annotation

GitHub Actions / Lint

field `uri` is unused (unused)
digest map[string]string
}

type predicate struct {
Verifier map[string]string `json:"verifier"`
TimeVerified string `json:"timeVerified"`
ResourceUri string `json:"resourceUri"`
Policies []policySource `json:"policies"`
InputAttestations []attestationSource `json:"intputAttestations"`
VerificationResult string `json:"verificationResult"`
VerifiedRules []string `json:"verifiedRules"`
VerifiedCollections []string `json:"verfiedCollection"`
SlsaVersion string `json:"slsaVersion"`
}

func VsaFromImageValidation(results []evaluator.Outcome, policies []source.PolicySource, policy policy.Policy, attestations []Attestation) (ProvenanceStatementVSA, error) {
var verifiedPolicies []policySource
for _, p := range policies {
verifiedPolicies = append(verifiedPolicies, policySource{uri: p.PolicyUrl()})
}

var verfiedResults int
var verifiedLevels []string
for _, res := range results {
for _, success := range res.Successes {
verifiedLevels = append(verifiedLevels, success.Metadata["code"].(string))
}
verfiedResults = verfiedResults + len(res.Failures)
}
verificationResult := "Success"
if verfiedResults > 0 {
verificationResult = "Failure"
}

var verifiedCollections []string
for _, source := range policy.Spec().Sources {
for _, include := range source.Config.Include {
splitInclude := strings.Split(include, "@")
if len(splitInclude) > 1 {
verifiedCollections = append(verifiedCollections, splitInclude[1])
}
}
}

var slsaVersion string
var digest map[string]string
var subject []in_toto.Subject
for _, sp := range attestations {
slsaVersion = sp.PredicateType()
digest = sp.Digest()
subject = sp.Subject()
}

return ProvenanceStatementVSA{
StatementHeader: in_toto.StatementHeader{
Type: StatmentVSA,
PredicateType: PredicateVSAProvenance,
Subject: subject,
},
Predicate: predicate{
Verifier: map[string]string{
"id": "ec",
},
TimeVerified: time.Now().String(),
// need to check on this. Sounds like it should be the same as the subject, but not compatible types
ResourceUri: subject[0].Name,
Policies: verifiedPolicies,
InputAttestations: []attestationSource{
{
digest: digest,
},
},
VerificationResult: verificationResult,
VerifiedRules: verifiedLevels,
VerifiedCollections: verifiedCollections,
SlsaVersion: slsaVersion,
},
}, nil
}
8 changes: 8 additions & 0 deletions internal/definition/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ func (e mockEvaluator) CapabilitiesPath() string {
return ""
}

func (e mockEvaluator) GetPolicySources() []source.PolicySource {
return []source.PolicySource{}
}

func (b badMockEvaluator) Evaluate(ctx context.Context, inputs []string) ([]evaluator.Outcome, evaluator.Data, error) {
return nil, nil, errors.New("Evaluator error")
}
Expand All @@ -60,6 +64,10 @@ func (e badMockEvaluator) CapabilitiesPath() string {
return ""
}

func (e badMockEvaluator) GetPolicySources() []source.PolicySource {
return []source.PolicySource{}
}

func mockNewPipelineDefinitionFile(ctx context.Context, fpath []string, sources []source.PolicySource, namespace []string) (*definition.Definition, error) {
return &definition.Definition{
Evaluator: mockEvaluator{},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ type ApplicationSnapshotImage struct {
component app.SnapshotComponent
}

func (a ApplicationSnapshotImage) GetReference() name.Reference {
return a.reference
}

// NewApplicationSnapshotImage returns an ApplicationSnapshotImage struct with reference, checkOpts, and evaluator ready to use.
func NewApplicationSnapshotImage(ctx context.Context, component app.SnapshotComponent, p policy.Policy) (*ApplicationSnapshotImage, error) {
opts, err := p.CheckOpts()
Expand Down Expand Up @@ -233,6 +237,7 @@ func (a *ApplicationSnapshotImage) ValidateAttestationSignature(ctx context.Cont
// Set the ClaimVerifier on a shallow *copy* of CheckOpts to avoid unexpected side-effects
opts := a.checkOpts
opts.ClaimVerifier = cosign.IntotoSubjectClaimVerifier

layers, _, err := NewClient(ctx).VerifyImageAttestations(ctx, a.reference, &opts)
if err != nil {
return err
Expand Down Expand Up @@ -333,6 +338,14 @@ func (a *ApplicationSnapshotImage) Signatures() []signature.EntitySignature {
return a.signatures
}

func (a *ApplicationSnapshotImage) ResolveDigest(ctx context.Context) (string, error) {
digest, err := NewClient(ctx).ResolveDigest(a.reference, &a.checkOpts)
if err != nil {
return "", err
}
return digest, nil
}

type attestationData struct {
json.RawMessage // Deprecated
Extra attestationExtraData `json:"extra"` // Deprecated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@ func (f fakeAtt) Signatures() []signature.EntitySignature {
return f.signatures
}

func (f fakeAtt) Digest() map[string]string {
return map[string]string{}
}

func (f fakeAtt) Subject() []in_toto.Subject {
return []in_toto.Subject{}
}

type opts func(*fakeAtt)

func createSimpleAttestation(statement *in_toto.ProvenanceStatementSLSA02, o ...opts) attestation.Attestation {
Expand Down Expand Up @@ -443,6 +451,10 @@ func (c *MockClient) Head(name name.Reference, options ...remote.Option) (*v1.De
return args.Get(0).(*v1.Descriptor), args.Error(1)
}

func (c *MockClient) ResolveDigest(ref name.Reference, opts *cosign.CheckOpts) (string, error) {
return "", nil
}

func TestValidateImageSignatureClaims(t *testing.T) {
ref := name.MustParseReference("registry.io/repository/image:tag")
a := ApplicationSnapshotImage{
Expand Down
15 changes: 15 additions & 0 deletions internal/evaluation_target/application_snapshot_image/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ import (

"github.com/google/go-containerregistry/pkg/name"
gcr "github.com/google/go-containerregistry/pkg/v1"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/sigstore/cosign/v2/pkg/cosign"
"github.com/sigstore/cosign/v2/pkg/oci"
ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote"
)

type contextKey string
Expand All @@ -36,6 +38,7 @@ type Client interface {
VerifyImageSignatures(context.Context, name.Reference, *cosign.CheckOpts) ([]oci.Signature, bool, error)
VerifyImageAttestations(context.Context, name.Reference, *cosign.CheckOpts) ([]oci.Signature, bool, error)
Head(name.Reference, ...remote.Option) (*gcr.Descriptor, error)
ResolveDigest(name.Reference, *cosign.CheckOpts) (string, error)
}

func WithClient(ctx context.Context, client Client) context.Context {
Expand Down Expand Up @@ -66,3 +69,15 @@ func (c *defaultClient) VerifyImageAttestations(ctx context.Context, ref name.Re
func (c *defaultClient) Head(ref name.Reference, opts ...remote.Option) (*gcr.Descriptor, error) {
return remote.Head(ref, opts...)
}

func (c *defaultClient) ResolveDigest(ref name.Reference, opts *cosign.CheckOpts) (string, error) {
digest, err := ociremote.ResolveDigest(ref, opts.RegistryClientOpts...)
if err != nil {
return "", err
}
h, err := v1.NewHash(digest.Identifier())
if err != nil {
return "", err
}
return h.String(), nil
}
4 changes: 4 additions & 0 deletions internal/evaluator/conftest_evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,10 @@ func NewConftestEvaluatorWithNamespace(ctx context.Context, policySources []sour
return c, nil
}

func (c conftestEvaluator) GetPolicySources() []source.PolicySource {
return c.policySources
}

// Destroy removes the working directory
func (c conftestEvaluator) Destroy() {
if os.Getenv("EC_DEBUG") == "" {
Expand Down
Loading

0 comments on commit 59973fd

Please sign in to comment.