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
4 changes: 3 additions & 1 deletion app/cli/internal/action/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,7 @@ func newCrafter(enableRemoteState bool, conn *grpc.ClientConn, opts ...crafter.N
return nil, fmt.Errorf("failed to create state manager: %w", err)
}

return crafter.NewCrafter(stateManager, opts...)
attClient := pb.NewAttestationServiceClient(conn)

return crafter.NewCrafter(stateManager, attClient, opts...)
}
3 changes: 2 additions & 1 deletion app/cli/internal/action/attestation_push.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ func (action *AttestationPush) Run(ctx context.Context, attestationID string, ru
return nil, fmt.Errorf("creating signer: %w", err)
}

renderer, err := renderer.NewAttestationRenderer(crafter.CraftingState, action.cliVersion, action.cliDigest, sig,
attClient := pb.NewAttestationServiceClient(action.CPConnection)
renderer, err := renderer.NewAttestationRenderer(crafter.CraftingState, attClient, action.cliVersion, action.cliDigest, sig,
renderer.WithLogger(action.Logger), renderer.WithBundleOutputPath(action.bundlePath))
if err != nil {
return nil, err
Expand Down
30 changes: 6 additions & 24 deletions pkg/attestation/crafter/crafter.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ type Crafter struct {
// Authn is used to authenticate with the OCI registry
ociRegistryAuth authn.Keychain
validator *protovalidate.Validator

// attestation client is used to load chainloop policies
attClient v1.AttestationServiceClient
}

type VersionedCraftingState struct {
Expand Down Expand Up @@ -105,7 +108,7 @@ func WithOCIAuth(server, username, password string) NewOpt {
}

// Create a completely new crafter
func NewCrafter(stateManager StateManager, opts ...NewOpt) (*Crafter, error) {
func NewCrafter(stateManager StateManager, attClient v1.AttestationServiceClient, opts ...NewOpt) (*Crafter, error) {
noopLogger := zerolog.Nop()

validator, err := protovalidate.New()
Expand All @@ -121,6 +124,7 @@ func NewCrafter(stateManager StateManager, opts ...NewOpt) (*Crafter, error) {
// By default we authenticate with the current user's keychain (i.e ~/.docker/config.json)
ociRegistryAuth: authn.DefaultKeychain,
validator: validator,
attClient: attClient,
}

for _, opt := range opts {
Expand Down Expand Up @@ -191,31 +195,9 @@ func LoadSchema(pathOrURI string) (*schemaapi.CraftingSchema, error) {
return nil, err
}

// Load, validate policies, and embed them in the schema
if err := validatePolicyAttachments(schema.GetPolicies().GetMaterials()); err != nil {
return nil, fmt.Errorf("validating policies: %w", err)
}
if err := validatePolicyAttachments(schema.GetPolicies().GetAttestation()); err != nil {
return nil, fmt.Errorf("validating policies: %w", err)
}

return schema, nil
}

func validatePolicyAttachments(pols []*schemaapi.PolicyAttachment) error {
for _, p := range pols {
spec, err := policies.LoadPolicySpec(p)
if err != nil {
return fmt.Errorf("validating policy: %w", err)
}
if _, err := policies.LoadPolicyScriptFromSpec(spec); err != nil {
return fmt.Errorf("loading policy script: %w", err)
}
}

return nil
}

// Initialize the temporary file with the content of the schema
func (c *Crafter) initCraftingStateFile(
ctx context.Context,
Expand Down Expand Up @@ -608,7 +590,7 @@ func (c *Crafter) addMaterial(ctx context.Context, m *schemaapi.CraftingSchema_M
}

// Validate policies
pv := policies.NewPolicyVerifier(c.CraftingState.InputSchema, c.Logger)
pv := policies.NewPolicyVerifier(c.CraftingState.InputSchema, c.attClient, c.Logger)
policyResults, err := pv.VerifyMaterial(ctx, mt, value)
if err != nil {
return fmt.Errorf("error applying policies to material: %w", err)
Expand Down
48 changes: 3 additions & 45 deletions pkg/attestation/crafter/crafter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ func newInitializedCrafter(t *testing.T, contractPath string, wfMeta *v1.Workflo
}

statePath := fmt.Sprintf("%s/attestation.json", t.TempDir())
c, err := crafter.NewCrafter(testingStateManager(t, statePath), opts...)
c, err := crafter.NewCrafter(testingStateManager(t, statePath), nil, opts...)
require.NoError(t, err)
contract, err := crafter.LoadSchema(contractPath)
if err != nil {
Expand Down Expand Up @@ -220,48 +220,6 @@ func (s *crafterSuite) TestLoadSchema() {
contractPath: "testdata/contracts/invalid.yaml",
wantErr: true,
},
{
name: "policies",
contractPath: "testdata/contracts/with_policy_embedded.yaml",
want: &schemaapi.CraftingSchema{
SchemaVersion: "v1",
Policies: &schemaapi.Policies{
Attestation: []*schemaapi.PolicyAttachment{
{
Policy: &schemaapi.PolicyAttachment_Ref{
Ref: "testdata/policies/policy_embedded.yaml",
},
},
},
},
},
},
{
name: "missing policy",
contractPath: "testdata/contracts/with_missing_policy.yaml",
wantErr: true,
},
{
name: "missing script",
contractPath: "testdata/contracts/with_policy_missing_rego.yaml",
wantErr: true,
},
{
name: "rego policy",
contractPath: "testdata/contracts/with_rego.yaml",
want: &schemaapi.CraftingSchema{
SchemaVersion: "v1",
Policies: &schemaapi.Policies{
Attestation: []*schemaapi.PolicyAttachment{
{
Policy: &schemaapi.PolicyAttachment_Ref{
Ref: "testdata/policies/policy_rego.yaml",
},
},
},
},
},
},
}

for _, tc := range testCases {
Expand Down Expand Up @@ -410,14 +368,14 @@ func (s *crafterSuite) TestAlreadyInitialized() {
_, err := os.Create(statePath)
require.NoError(s.T(), err)
// TODO: replace by a mock
c, err := crafter.NewCrafter(testingStateManager(t, statePath))
c, err := crafter.NewCrafter(testingStateManager(t, statePath), nil)
require.NoError(s.T(), err)
s.True(c.AlreadyInitialized(context.Background(), ""))
})

s.T().Run("non existing", func(t *testing.T) {
statePath := fmt.Sprintf("%s/attestation.json", t.TempDir())
c, err := crafter.NewCrafter(testingStateManager(t, statePath))
c, err := crafter.NewCrafter(testingStateManager(t, statePath), nil)
require.NoError(s.T(), err)
s.False(c.AlreadyInitialized(context.Background(), ""))
})
Expand Down
7 changes: 5 additions & 2 deletions pkg/attestation/renderer/renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"fmt"
"os"

pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
"github.com/chainloop-dev/chainloop/pkg/attestation/crafter"
v1 "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1"
Expand All @@ -51,6 +52,7 @@ type AttestationRenderer struct {
signer sigstoresigner.Signer
dsseSigner sigstoresigner.Signer
bundlePath string
attClient pb.AttestationServiceClient
}

type r interface {
Expand All @@ -73,7 +75,7 @@ func WithBundleOutputPath(bundlePath string) Opt {
}
}

func NewAttestationRenderer(state *crafter.VersionedCraftingState, builderVersion, builderDigest string, signer sigstoresigner.Signer, opts ...Opt) (*AttestationRenderer, error) {
func NewAttestationRenderer(state *crafter.VersionedCraftingState, attClient pb.AttestationServiceClient, builderVersion, builderDigest string, signer sigstoresigner.Signer, opts ...Opt) (*AttestationRenderer, error) {
if state.GetAttestation() == nil {
return nil, errors.New("attestation not initialized")
}
Expand All @@ -85,6 +87,7 @@ func NewAttestationRenderer(state *crafter.VersionedCraftingState, builderVersio
dsseSigner: sigdsee.WrapSigner(signer, "application/vnd.in-toto+json"),
signer: signer,
renderer: chainloop.NewChainloopRendererV02(state.GetAttestation(), builderVersion, builderDigest),
attClient: attClient,
}

for _, opt := range opts {
Expand All @@ -109,7 +112,7 @@ func (ab *AttestationRenderer) Render(ctx context.Context) (*dsse.Envelope, erro
}

// validate attestation-level policies
pv := policies.NewPolicyVerifier(ab.schema, &ab.logger)
pv := policies.NewPolicyVerifier(ab.schema, ab.attClient, &ab.logger)
policyResults, err := pv.VerifyStatement(ctx, statement)
if err != nil {
return nil, fmt.Errorf("applying policies to statement: %w", err)
Expand Down
8 changes: 4 additions & 4 deletions pkg/attestation/renderer/renderer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func (s *rendererSuite) SetupTest() {

func (s *rendererSuite) TestRender() {
s.Run("generated envelope is always well-formed", func() {
renderer, err := NewAttestationRenderer(s.cs, "", "", s.sv)
renderer, err := NewAttestationRenderer(s.cs, nil, "", "", s.sv)
s.Require().NoError(err)

envelope, err := renderer.Render(context.TODO())
Expand All @@ -82,7 +82,7 @@ func (s *rendererSuite) TestRender() {
s.Run("simulates double wrapping bug", func() {
doubleWrapper := sigdsee.WrapSigner(s.sv, "application/vnd.in-toto+json")

renderer, err := NewAttestationRenderer(s.cs, "", "", doubleWrapper)
renderer, err := NewAttestationRenderer(s.cs, nil, "", "", doubleWrapper)
s.Require().NoError(err)

envelope, err := renderer.Render(context.TODO())
Expand All @@ -99,7 +99,7 @@ func (s *rendererSuite) TestEnvelopeToBundle() {
s.Require().NoError(err)

signer := cosign.NewSigner("", zerolog.Nop())
renderer, err := NewAttestationRenderer(s.cs, "", "", signer)
renderer, err := NewAttestationRenderer(s.cs, nil, "", "", signer)
s.Require().NoError(err)

bundle, err := renderer.envelopeToBundle(*envelope)
Expand All @@ -122,7 +122,7 @@ func (s *rendererSuite) TestEnvelopeToBundle() {

// 2 certs
signer.Chain = []string{cert, "ROOT"}
renderer, err := NewAttestationRenderer(s.cs, "", "", signer)
renderer, err := NewAttestationRenderer(s.cs, nil, "", "", signer)
s.Require().NoError(err)

bundle, err := renderer.envelopeToBundle(*envelope)
Expand Down
121 changes: 121 additions & 0 deletions pkg/policies/loader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
//
// Copyright 2024 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 policies

import (
"context"
"fmt"
"path/filepath"
"strings"
"sync"

pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
"github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials"
"github.com/sigstore/cosign/v2/pkg/blob"
"google.golang.org/protobuf/encoding/protojson"
)

// Loader defines the interface for policy loaders from contract attachments
type Loader interface {
Load(context.Context, *v1.PolicyAttachment) (*v1.Policy, error)
}

// EmbeddedLoader returns embedded policies
type EmbeddedLoader struct{}

func (e *EmbeddedLoader) Load(_ context.Context, attachment *v1.PolicyAttachment) (*v1.Policy, error) {
return attachment.GetEmbedded(), nil
}

// URLLoader loader loads policies from filesystem and HTTPS references
type URLLoader struct{}

func (l *URLLoader) Load(_ context.Context, attachment *v1.PolicyAttachment) (*v1.Policy, error) {
reference := attachment.GetRef()

// look for the referenced policy spec (note: loading by `name` is not supported yet)
// this method understands env, http and https schemes, and defaults to file system.
rawData, err := blob.LoadFileOrURL(reference)
if err != nil {
return nil, fmt.Errorf("loading policy spec: %w", err)
}

jsonContent, err := materials.LoadJSONBytes(rawData, filepath.Ext(reference))
if err != nil {
return nil, fmt.Errorf("loading policy spec: %w", err)
}

var spec v1.Policy
if err := protojson.Unmarshal(jsonContent, &spec); err != nil {
return nil, fmt.Errorf("unmarshalling policy spec: %w", err)
}
return &spec, nil
}

const chainloopScheme = "chainloop"

// ChainloopLoader loads policies referenced with chainloop://provider/name URLs
type ChainloopLoader struct {
Client pb.AttestationServiceClient

cacheMutex sync.Mutex
}

var remotePolicyCache = make(map[string]*v1.Policy)

func NewChainloopLoader(client pb.AttestationServiceClient) *ChainloopLoader {
return &ChainloopLoader{Client: client}
}

func (c *ChainloopLoader) Load(ctx context.Context, attachment *v1.PolicyAttachment) (*v1.Policy, error) {
ref := attachment.GetRef()

c.cacheMutex.Lock()
defer c.cacheMutex.Unlock()

if remotePolicyCache[ref] != nil {
return remotePolicyCache[ref], nil
}

parts := strings.SplitN(ref, "://", 2)
if len(parts) != 2 || parts[0] != chainloopScheme {
return nil, fmt.Errorf("invalid policy reference %q", ref)
}

pn := strings.SplitN(parts[1], "/", 2)
var (
name = pn[0]
provider string
)
if len(pn) == 2 {
provider = pn[0]
name = pn[1]
}

resp, err := c.Client.GetPolicy(ctx, &pb.AttestationServiceGetPolicyRequest{
Provider: provider,
PolicyName: name,
})
if err != nil {
return nil, fmt.Errorf("loading policy: %w", err)
}

// cache result
remotePolicyCache[ref] = resp.GetPolicy()

return resp.GetPolicy(), nil
}
Loading