Skip to content

Commit d5a5d0c

Browse files
authored
feat(policies): load and evaluate remote policies on attestation operations (#1220)
Signed-off-by: Jose I. Paris <[email protected]>
1 parent 50668b9 commit d5a5d0c

File tree

9 files changed

+214
-144
lines changed

9 files changed

+214
-144
lines changed

app/cli/internal/action/action.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,7 @@ func newCrafter(enableRemoteState bool, conn *grpc.ClientConn, opts ...crafter.N
6060
return nil, fmt.Errorf("failed to create state manager: %w", err)
6161
}
6262

63-
return crafter.NewCrafter(stateManager, opts...)
63+
attClient := pb.NewAttestationServiceClient(conn)
64+
65+
return crafter.NewCrafter(stateManager, attClient, opts...)
6466
}

app/cli/internal/action/attestation_push.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,8 @@ func (action *AttestationPush) Run(ctx context.Context, attestationID string, ru
147147
return nil, fmt.Errorf("creating signer: %w", err)
148148
}
149149

150-
renderer, err := renderer.NewAttestationRenderer(crafter.CraftingState, action.cliVersion, action.cliDigest, sig,
150+
attClient := pb.NewAttestationServiceClient(action.CPConnection)
151+
renderer, err := renderer.NewAttestationRenderer(crafter.CraftingState, attClient, action.cliVersion, action.cliDigest, sig,
151152
renderer.WithLogger(action.Logger), renderer.WithBundleOutputPath(action.bundlePath))
152153
if err != nil {
153154
return nil, err

pkg/attestation/crafter/crafter.go

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ type Crafter struct {
6666
// Authn is used to authenticate with the OCI registry
6767
ociRegistryAuth authn.Keychain
6868
validator *protovalidate.Validator
69+
70+
// attestation client is used to load chainloop policies
71+
attClient v1.AttestationServiceClient
6972
}
7073

7174
type VersionedCraftingState struct {
@@ -105,7 +108,7 @@ func WithOCIAuth(server, username, password string) NewOpt {
105108
}
106109

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

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

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

194-
// Load, validate policies, and embed them in the schema
195-
if err := validatePolicyAttachments(schema.GetPolicies().GetMaterials()); err != nil {
196-
return nil, fmt.Errorf("validating policies: %w", err)
197-
}
198-
if err := validatePolicyAttachments(schema.GetPolicies().GetAttestation()); err != nil {
199-
return nil, fmt.Errorf("validating policies: %w", err)
200-
}
201-
202198
return schema, nil
203199
}
204200

205-
func validatePolicyAttachments(pols []*schemaapi.PolicyAttachment) error {
206-
for _, p := range pols {
207-
spec, err := policies.LoadPolicySpec(p)
208-
if err != nil {
209-
return fmt.Errorf("validating policy: %w", err)
210-
}
211-
if _, err := policies.LoadPolicyScriptFromSpec(spec); err != nil {
212-
return fmt.Errorf("loading policy script: %w", err)
213-
}
214-
}
215-
216-
return nil
217-
}
218-
219201
// Initialize the temporary file with the content of the schema
220202
func (c *Crafter) initCraftingStateFile(
221203
ctx context.Context,
@@ -608,7 +590,7 @@ func (c *Crafter) addMaterial(ctx context.Context, m *schemaapi.CraftingSchema_M
608590
}
609591

610592
// Validate policies
611-
pv := policies.NewPolicyVerifier(c.CraftingState.InputSchema, c.Logger)
593+
pv := policies.NewPolicyVerifier(c.CraftingState.InputSchema, c.attClient, c.Logger)
612594
policyResults, err := pv.VerifyMaterial(ctx, mt, value)
613595
if err != nil {
614596
return fmt.Errorf("error applying policies to material: %w", err)

pkg/attestation/crafter/crafter_test.go

Lines changed: 3 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ func newInitializedCrafter(t *testing.T, contractPath string, wfMeta *v1.Workflo
164164
}
165165

166166
statePath := fmt.Sprintf("%s/attestation.json", t.TempDir())
167-
c, err := crafter.NewCrafter(testingStateManager(t, statePath), opts...)
167+
c, err := crafter.NewCrafter(testingStateManager(t, statePath), nil, opts...)
168168
require.NoError(t, err)
169169
contract, err := crafter.LoadSchema(contractPath)
170170
if err != nil {
@@ -220,48 +220,6 @@ func (s *crafterSuite) TestLoadSchema() {
220220
contractPath: "testdata/contracts/invalid.yaml",
221221
wantErr: true,
222222
},
223-
{
224-
name: "policies",
225-
contractPath: "testdata/contracts/with_policy_embedded.yaml",
226-
want: &schemaapi.CraftingSchema{
227-
SchemaVersion: "v1",
228-
Policies: &schemaapi.Policies{
229-
Attestation: []*schemaapi.PolicyAttachment{
230-
{
231-
Policy: &schemaapi.PolicyAttachment_Ref{
232-
Ref: "testdata/policies/policy_embedded.yaml",
233-
},
234-
},
235-
},
236-
},
237-
},
238-
},
239-
{
240-
name: "missing policy",
241-
contractPath: "testdata/contracts/with_missing_policy.yaml",
242-
wantErr: true,
243-
},
244-
{
245-
name: "missing script",
246-
contractPath: "testdata/contracts/with_policy_missing_rego.yaml",
247-
wantErr: true,
248-
},
249-
{
250-
name: "rego policy",
251-
contractPath: "testdata/contracts/with_rego.yaml",
252-
want: &schemaapi.CraftingSchema{
253-
SchemaVersion: "v1",
254-
Policies: &schemaapi.Policies{
255-
Attestation: []*schemaapi.PolicyAttachment{
256-
{
257-
Policy: &schemaapi.PolicyAttachment_Ref{
258-
Ref: "testdata/policies/policy_rego.yaml",
259-
},
260-
},
261-
},
262-
},
263-
},
264-
},
265223
}
266224

267225
for _, tc := range testCases {
@@ -410,14 +368,14 @@ func (s *crafterSuite) TestAlreadyInitialized() {
410368
_, err := os.Create(statePath)
411369
require.NoError(s.T(), err)
412370
// TODO: replace by a mock
413-
c, err := crafter.NewCrafter(testingStateManager(t, statePath))
371+
c, err := crafter.NewCrafter(testingStateManager(t, statePath), nil)
414372
require.NoError(s.T(), err)
415373
s.True(c.AlreadyInitialized(context.Background(), ""))
416374
})
417375

418376
s.T().Run("non existing", func(t *testing.T) {
419377
statePath := fmt.Sprintf("%s/attestation.json", t.TempDir())
420-
c, err := crafter.NewCrafter(testingStateManager(t, statePath))
378+
c, err := crafter.NewCrafter(testingStateManager(t, statePath), nil)
421379
require.NoError(s.T(), err)
422380
s.False(c.AlreadyInitialized(context.Background(), ""))
423381
})

pkg/attestation/renderer/renderer.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"fmt"
2626
"os"
2727

28+
pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
2829
schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
2930
"github.com/chainloop-dev/chainloop/pkg/attestation/crafter"
3031
v1 "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1"
@@ -51,6 +52,7 @@ type AttestationRenderer struct {
5152
signer sigstoresigner.Signer
5253
dsseSigner sigstoresigner.Signer
5354
bundlePath string
55+
attClient pb.AttestationServiceClient
5456
}
5557

5658
type r interface {
@@ -73,7 +75,7 @@ func WithBundleOutputPath(bundlePath string) Opt {
7375
}
7476
}
7577

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

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

111114
// validate attestation-level policies
112-
pv := policies.NewPolicyVerifier(ab.schema, &ab.logger)
115+
pv := policies.NewPolicyVerifier(ab.schema, ab.attClient, &ab.logger)
113116
policyResults, err := pv.VerifyStatement(ctx, statement)
114117
if err != nil {
115118
return nil, fmt.Errorf("applying policies to statement: %w", err)

pkg/attestation/renderer/renderer_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func (s *rendererSuite) SetupTest() {
6969

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

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

85-
renderer, err := NewAttestationRenderer(s.cs, "", "", doubleWrapper)
85+
renderer, err := NewAttestationRenderer(s.cs, nil, "", "", doubleWrapper)
8686
s.Require().NoError(err)
8787

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

101101
signer := cosign.NewSigner("", zerolog.Nop())
102-
renderer, err := NewAttestationRenderer(s.cs, "", "", signer)
102+
renderer, err := NewAttestationRenderer(s.cs, nil, "", "", signer)
103103
s.Require().NoError(err)
104104

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

123123
// 2 certs
124124
signer.Chain = []string{cert, "ROOT"}
125-
renderer, err := NewAttestationRenderer(s.cs, "", "", signer)
125+
renderer, err := NewAttestationRenderer(s.cs, nil, "", "", signer)
126126
s.Require().NoError(err)
127127

128128
bundle, err := renderer.envelopeToBundle(*envelope)

pkg/policies/loader.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
//
2+
// Copyright 2024 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package policies
17+
18+
import (
19+
"context"
20+
"fmt"
21+
"path/filepath"
22+
"strings"
23+
"sync"
24+
25+
pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
26+
v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
27+
"github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials"
28+
"github.com/sigstore/cosign/v2/pkg/blob"
29+
"google.golang.org/protobuf/encoding/protojson"
30+
)
31+
32+
// Loader defines the interface for policy loaders from contract attachments
33+
type Loader interface {
34+
Load(context.Context, *v1.PolicyAttachment) (*v1.Policy, error)
35+
}
36+
37+
// EmbeddedLoader returns embedded policies
38+
type EmbeddedLoader struct{}
39+
40+
func (e *EmbeddedLoader) Load(_ context.Context, attachment *v1.PolicyAttachment) (*v1.Policy, error) {
41+
return attachment.GetEmbedded(), nil
42+
}
43+
44+
// URLLoader loader loads policies from filesystem and HTTPS references
45+
type URLLoader struct{}
46+
47+
func (l *URLLoader) Load(_ context.Context, attachment *v1.PolicyAttachment) (*v1.Policy, error) {
48+
reference := attachment.GetRef()
49+
50+
// look for the referenced policy spec (note: loading by `name` is not supported yet)
51+
// this method understands env, http and https schemes, and defaults to file system.
52+
rawData, err := blob.LoadFileOrURL(reference)
53+
if err != nil {
54+
return nil, fmt.Errorf("loading policy spec: %w", err)
55+
}
56+
57+
jsonContent, err := materials.LoadJSONBytes(rawData, filepath.Ext(reference))
58+
if err != nil {
59+
return nil, fmt.Errorf("loading policy spec: %w", err)
60+
}
61+
62+
var spec v1.Policy
63+
if err := protojson.Unmarshal(jsonContent, &spec); err != nil {
64+
return nil, fmt.Errorf("unmarshalling policy spec: %w", err)
65+
}
66+
return &spec, nil
67+
}
68+
69+
const chainloopScheme = "chainloop"
70+
71+
// ChainloopLoader loads policies referenced with chainloop://provider/name URLs
72+
type ChainloopLoader struct {
73+
Client pb.AttestationServiceClient
74+
75+
cacheMutex sync.Mutex
76+
}
77+
78+
var remotePolicyCache = make(map[string]*v1.Policy)
79+
80+
func NewChainloopLoader(client pb.AttestationServiceClient) *ChainloopLoader {
81+
return &ChainloopLoader{Client: client}
82+
}
83+
84+
func (c *ChainloopLoader) Load(ctx context.Context, attachment *v1.PolicyAttachment) (*v1.Policy, error) {
85+
ref := attachment.GetRef()
86+
87+
c.cacheMutex.Lock()
88+
defer c.cacheMutex.Unlock()
89+
90+
if remotePolicyCache[ref] != nil {
91+
return remotePolicyCache[ref], nil
92+
}
93+
94+
parts := strings.SplitN(ref, "://", 2)
95+
if len(parts) != 2 || parts[0] != chainloopScheme {
96+
return nil, fmt.Errorf("invalid policy reference %q", ref)
97+
}
98+
99+
pn := strings.SplitN(parts[1], "/", 2)
100+
var (
101+
name = pn[0]
102+
provider string
103+
)
104+
if len(pn) == 2 {
105+
provider = pn[0]
106+
name = pn[1]
107+
}
108+
109+
resp, err := c.Client.GetPolicy(ctx, &pb.AttestationServiceGetPolicyRequest{
110+
Provider: provider,
111+
PolicyName: name,
112+
})
113+
if err != nil {
114+
return nil, fmt.Errorf("loading policy: %w", err)
115+
}
116+
117+
// cache result
118+
remotePolicyCache[ref] = resp.GetPolicy()
119+
120+
return resp.GetPolicy(), nil
121+
}

0 commit comments

Comments
 (0)