Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 12 additions & 48 deletions app/cli/internal/action/workflow_run_describe.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ package action

import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
Expand All @@ -43,13 +42,12 @@ type WorkflowRunItemFull struct {
}

type WorkflowRunAttestationItem struct {
ID string `json:"id"`
CreatedAt *time.Time `json:"createdAt"`
Envelope *dsse.Envelope `json:"envelope"`
statement *in_toto.Statement
predicateV1 *chainloop.ProvenancePredicateV01
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the predicate is not needed in fact since we are already providing the materials and env variables denormalized

Materials []*Material `json:"materials,omitempty"`
EnvVars []*EnvVar `json:"envvars,omitempty"`
ID string `json:"id"`
CreatedAt *time.Time `json:"createdAt"`
Envelope *dsse.Envelope `json:"envelope"`
statement *in_toto.Statement
Materials []*Material `json:"materials,omitempty"`
EnvVars []*EnvVar `json:"envvars,omitempty"`
}

type Material struct {
Expand All @@ -63,10 +61,6 @@ type EnvVar struct {
Value string `json:"value"`
}

func (i *WorkflowRunAttestationItem) Predicate() *chainloop.ProvenancePredicateV01 {
return i.predicateV1
}

func (i *WorkflowRunAttestationItem) Statement() *in_toto.Statement {
return i.statement
}
Expand Down Expand Up @@ -114,24 +108,9 @@ func (action *WorkflowRunDescribe) Run(runID string, verify bool, publicKey stri
item.Verified = true
}

// Decode in-toto statement
statement := &in_toto.Statement{}
decodedPayload, err := envelope.DecodeB64Payload()
statement, err := chainloop.ExtractStatement(envelope)
if err != nil {
return nil, fmt.Errorf("decoding payload: %w", err)
}

if err := json.Unmarshal(decodedPayload, statement); err != nil {
return nil, fmt.Errorf("un-marshaling predicate: %w", err)
}

var predicate *chainloop.ProvenancePredicateV01
if statement.PredicateType == chainloop.PredicateTypeV01 {
if predicate, err = extractPredicateV1(statement); err != nil {
return nil, fmt.Errorf("extracting predicate: %w", err)
}
} else {
return nil, errors.New("predicate type not supported")
return nil, fmt.Errorf("extracting statement: %w", err)
}

envVars := make([]*EnvVar, 0, len(attestation.GetEnvVars()))
Expand All @@ -146,30 +125,15 @@ func (action *WorkflowRunDescribe) Run(runID string, verify bool, publicKey stri

item.Attestation = &WorkflowRunAttestationItem{
ID: attestation.Id, CreatedAt: toTimePtr(attestation.CreatedAt.AsTime()),
Envelope: envelope,
statement: statement,
predicateV1: predicate,
EnvVars: envVars,
Materials: materials,
Envelope: envelope,
statement: statement,
EnvVars: envVars,
Materials: materials,
}

return item, nil
}

func extractPredicateV1(statement *in_toto.Statement) (*chainloop.ProvenancePredicateV01, error) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved to renderer.chainloop

jsonPredicate, err := json.Marshal(statement.Predicate)
if err != nil {
return nil, fmt.Errorf("un-marshaling predicate: %w", err)
}

predicate := &chainloop.ProvenancePredicateV01{}
if err := json.Unmarshal(jsonPredicate, predicate); err != nil {
return nil, fmt.Errorf("un-marshaling predicate: %w", err)
}

return predicate, nil
}

func verifyEnvelope(ctx context.Context, e *dsse.Envelope, publicKey string) error {
// Currently we only support basic cosign public key check
// TODO: Add more verification methods
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,43 +106,38 @@ func (uc *Integration) UploadSBOMs(envelope *dsse.Envelope, orgID, workflowID st
}

// There is at least one enabled integration, extract the SBOMs
predicates, err := chainloop.ExtractPredicate(envelope)
predicate, err := chainloop.ExtractPredicate(envelope)
if err != nil {
return err
}

predicate := predicates.V01
if predicate == nil {
return errors.Forbidden("not implemented", "only v0.1 predicate is supported for now")
}

repo, err := uc.ociUC.FindMainRepo(ctx, orgID)
if err != nil {
return err
} else if repo == nil {
return errors.NotFound("not found", "main repository not found")
}

for _, m := range predicate.Materials {
if m.Type != contractAPI.CraftingSchema_Material_SBOM_CYCLONEDX_JSON.String() {
for _, material := range predicate.GetMaterials() {
if material.Type != contractAPI.CraftingSchema_Material_SBOM_CYCLONEDX_JSON.String() {
continue
}

buf := bytes.NewBuffer(nil)
digest, ok := m.Material.SLSA.Digest["sha256"]
if !ok {
if material.Hash == nil {
uc.log.Warnw("msg", "CYCLONE_DX material but download digest missing, skipping", "workflowID", workflowID, "integration", Kind, "name", material.Name)
continue
}

digest = "sha256:" + digest
digest := material.Hash.String()

uc.log.Infow("msg", "SBOM present, downloading", "workflowID", workflowID, "integration", Kind, "name", m.Name)
uc.log.Infow("msg", "SBOM present, downloading", "workflowID", workflowID, "integration", Kind, "name", material.Name)
// Download SBOM
buf := bytes.NewBuffer(nil)
if err := uc.casClient.Download(ctx, repo.SecretName, buf, digest); err != nil {
return fmt.Errorf("downloading from CAS: %w", err)
}

uc.log.Infow("msg", "SBOM downloaded", "digest", digest, "workflowID", workflowID, "integration", Kind, "name", m.Name)
uc.log.Infow("msg", "SBOM downloaded", "digest", digest, "workflowID", workflowID, "integration", Kind, "name", material.Name)

// Run integrations with that sbom
var wg sync.WaitGroup
Expand Down
25 changes: 14 additions & 11 deletions app/controlplane/internal/service/attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,20 +282,15 @@ func bizAttestationToPb(att *biz.Attestation) (*cpAPI.AttestationItem, error) {
return nil, err
}

predicates, err := chainloop.ExtractPredicate(att.Envelope)
predicate, err := chainloop.ExtractPredicate(att.Envelope)
if err != nil {
return nil, err
}

predicate := predicates.V01
if predicate == nil {
return nil, errors.InternalServer("invalid attestation type", "attestation does not contain a V01 predicate")
return nil, fmt.Errorf("error extracting predicate from attestation: %w", err)
}

return &cpAPI.AttestationItem{
Envelope: encodedAttestation,
EnvVars: extractEnvVariables(predicate.Env),
Materials: extractMaterials(predicate.Materials),
EnvVars: extractEnvVariables(predicate.GetEnvVars()),
Materials: extractMaterials(predicate.GetMaterials()),
}, nil
}

Expand All @@ -313,10 +308,18 @@ func extractEnvVariables(in map[string]string) []*cpAPI.AttestationItem_EnvVaria
return res
}

func extractMaterials(in []*chainloop.ProvenanceMaterial) []*cpAPI.AttestationItem_Material {
func extractMaterials(in []*chainloop.NormalizedMaterial) []*cpAPI.AttestationItem_Material {
res := make([]*cpAPI.AttestationItem_Material, 0, len(in))
for _, m := range in {
res = append(res, &cpAPI.AttestationItem_Material{Name: m.Name, Value: m.Material.String(), Type: m.Type})
// Initialize simply with the value
displayValue := m.Value
// Override if there is a hash attached
if m.Hash != nil {
displayValue = fmt.Sprintf("%s@%s", m.Value, m.Hash)
}

res = append(res, &cpAPI.AttestationItem_Material{Name: m.Name, Value: displayValue, Type: m.Type})
}

return res
}
69 changes: 69 additions & 0 deletions app/controlplane/internal/service/attestation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//
// Copyright 2023 The Chainloop Authors.
//
// 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.

package service

import (
"testing"

cpAPI "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
"github.com/chainloop-dev/chainloop/internal/attestation/renderer/chainloop"
crv1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/stretchr/testify/assert"
)

func TestExtractMaterials(t *testing.T) {
testCases := []struct {
name string
input []*chainloop.NormalizedMaterial
want []*cpAPI.AttestationItem_Material
}{
{
name: "different material types",
input: []*chainloop.NormalizedMaterial{
{
Name: "foo",
Type: "STRING",
Value: "bar",
},
{
Name: "foo",
Type: "ARTIFACT",
Value: "bar",
Hash: &crv1.Hash{Algorithm: "sha256", Hex: "deadbeef"},
},
},
want: []*cpAPI.AttestationItem_Material{
{
Name: "foo",
Type: "STRING",
Value: "bar",
},
{
Name: "foo",
Type: "ARTIFACT",
Value: "bar@sha256:deadbeef",
},
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := extractMaterials(tc.input)
assert.Equal(t, got, tc.want)
})
}
}
76 changes: 71 additions & 5 deletions internal/attestation/renderer/chainloop/chainloop.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,33 @@ import (
"time"

v1 "github.com/chainloop-dev/chainloop/app/cli/api/attestation/v1"
"github.com/secure-systems-lab/go-securesystemslib/dsse"

crv1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/in-toto/in-toto-golang/in_toto"
slsacommon "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
)

// // Replace custom material type with https://github.com/in-toto/attestation/blob/main/spec/v1.0/resource_descriptor.md
// const ChainloopPredicateTypeV02 = "chainloop.dev/attestation/v0.2"

// TODO: Figure out a more appropriate meaning
const chainloopBuildType = "chainloop.dev/workflowrun/v0.1"

const builderIDFmt = "chainloop.dev/cli/%s@%s"

type ProvenancePredicateVersions struct {
V01 *ProvenancePredicateV01
// NormalizablePredicate represents a common interface of how to extract materials and env vars
type NormalizablePredicate interface {
GetEnvVars() map[string]string
GetMaterials() []*NormalizedMaterial
}

type NormalizedMaterial struct {
// Name of the Material
Name string
// Type of the Material
Type string
// Either the fileName or the actual string content
Value string
// Hash of the Material
Hash *crv1.Hash
}

type ProvenancePredicateCommon struct {
Expand Down Expand Up @@ -101,6 +113,55 @@ func getChainloopMeta(att *v1.Attestation) *Metadata {
}
}

func ExtractStatement(envelope *dsse.Envelope) (*in_toto.Statement, error) {
decodedPayload, err := envelope.DecodeB64Payload()
if err != nil {
return nil, err
}

// 1 - Extract the in-toto statement
statement := &in_toto.Statement{}
if err := json.Unmarshal(decodedPayload, statement); err != nil {
return nil, fmt.Errorf("un-marshaling predicate: %w", err)
}

return statement, nil
}

// Extract the Chainloop attestation predicate from an encoded DSSE envelope
// NOTE: We return a NormalizablePredicate interface to allow for future versions
// of the predicate to be extracted without updating the consumer.
// Yes, having the producer define and return an interface is an anti-pattern.
// but it greatly simplifies the code since there are multiple consumers at different layers of the app
// and we expect predicates to evolve quickly
func ExtractPredicate(envelope *dsse.Envelope) (NormalizablePredicate, error) {
// 1 - Extract the in-toto statement
statement, err := ExtractStatement(envelope)
if err != nil {
return nil, fmt.Errorf("extracting statement: %w", err)
}

// 2 - Extract the Chainloop predicate from the in-toto statement
switch statement.PredicateType {
case PredicateTypeV01:
var predicate *ProvenancePredicateV01
if err = extractPredicate(statement, &predicate); err != nil {
return nil, fmt.Errorf("extracting predicate: %w", err)
}

return predicate, nil
case PredicateTypeV02:
var predicate *ProvenancePredicateV02
if err = extractPredicate(statement, &predicate); err != nil {
return nil, fmt.Errorf("extracting predicate: %w", err)
}

return predicate, nil
default:
return nil, fmt.Errorf("unsupported predicate type: %s", statement.PredicateType)
}
}

func extractPredicate(statement *in_toto.Statement, v any) error {
jsonPredicate, err := json.Marshal(statement.Predicate)
if err != nil {
Expand All @@ -113,3 +174,8 @@ func extractPredicate(statement *in_toto.Statement, v any) error {

return nil
}

// Implement NormalizablePredicate interface
func (p *ProvenancePredicateCommon) GetEnvVars() map[string]string {
return p.Env
}
Loading