Skip to content

Commit ae07713

Browse files
committed
refactor(policy): move to public package with cross-platform support
Move policy code from internal/configuration to registry/remote/policy as a public package so SDK users can create and manage policies. Add platform-specific default path handling so GetDefaultPolicyPath only falls back to /etc/containers/policy.json on Linux. Use interfaces for signature verification instead of direct implementation. Signed-off-by: Terry Howe <thowe@nvidia.com>
1 parent ba2e361 commit ae07713

File tree

12 files changed

+703
-266
lines changed

12 files changed

+703
-266
lines changed

registry/remote/internal/configuration/evaluator.go renamed to registry/remote/policy/evaluator.go

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ See the License for the specific language governing permissions and
1313
limitations under the License.
1414
*/
1515

16-
package configuration
16+
package policy
1717

1818
import (
1919
"context"
2020
"fmt"
21+
22+
"github.com/oras-project/oras-go/v3/errdef"
2123
)
2224

2325
// ImageReference represents a reference to an image
@@ -30,13 +32,48 @@ type ImageReference struct {
3032
Reference string
3133
}
3234

35+
// SignedByVerifier verifies GPG/simple signing signatures.
36+
// Implementations should verify that the image is signed with a valid key
37+
// as specified in the PRSignedBy requirement.
38+
type SignedByVerifier interface {
39+
Verify(ctx context.Context, req *PRSignedBy, image ImageReference) (bool, error)
40+
}
41+
42+
// SigstoreVerifier verifies sigstore signatures.
43+
// Implementations should verify that the image is signed with valid sigstore
44+
// signatures as specified in the PRSigstoreSigned requirement.
45+
type SigstoreVerifier interface {
46+
Verify(ctx context.Context, req *PRSigstoreSigned, image ImageReference) (bool, error)
47+
}
48+
3349
// Evaluator evaluates policy requirements against image references
3450
type Evaluator struct {
35-
policy *Policy
51+
policy *Policy
52+
signedByVerifier SignedByVerifier
53+
sigstoreVerifier SigstoreVerifier
54+
}
55+
56+
// EvaluatorOption configures an Evaluator
57+
type EvaluatorOption func(*Evaluator)
58+
59+
// WithSignedByVerifier sets the verifier for PRSignedBy requirements.
60+
// If not set, evaluating PRSignedBy requirements will return ErrUnsupported.
61+
func WithSignedByVerifier(v SignedByVerifier) EvaluatorOption {
62+
return func(e *Evaluator) {
63+
e.signedByVerifier = v
64+
}
65+
}
66+
67+
// WithSigstoreVerifier sets the verifier for PRSigstoreSigned requirements.
68+
// If not set, evaluating PRSigstoreSigned requirements will return ErrUnsupported.
69+
func WithSigstoreVerifier(v SigstoreVerifier) EvaluatorOption {
70+
return func(e *Evaluator) {
71+
e.sigstoreVerifier = v
72+
}
3673
}
3774

3875
// NewEvaluator creates a new policy evaluator
39-
func NewEvaluator(policy *Policy) (*Evaluator, error) {
76+
func NewEvaluator(policy *Policy, opts ...EvaluatorOption) (*Evaluator, error) {
4077
if policy == nil {
4178
return nil, fmt.Errorf("policy cannot be nil")
4279
}
@@ -45,9 +82,15 @@ func NewEvaluator(policy *Policy) (*Evaluator, error) {
4582
return nil, fmt.Errorf("invalid policy: %w", err)
4683
}
4784

48-
return &Evaluator{
85+
e := &Evaluator{
4986
policy: policy,
50-
}, nil
87+
}
88+
89+
for _, opt := range opts {
90+
opt(e)
91+
}
92+
93+
return e, nil
5194
}
5295

5396
// IsImageAllowed determines if an image is allowed by the policy
@@ -100,19 +143,19 @@ func (e *Evaluator) evaluateReject(ctx context.Context, req *Reject, image Image
100143
}
101144

102145
// evaluateSignedBy evaluates a signedBy requirement
103-
// Note: This is a placeholder implementation. Full signature verification
104-
// would require integration with GPG/signing libraries.
105146
func (e *Evaluator) evaluateSignedBy(ctx context.Context, req *PRSignedBy, image ImageReference) (bool, error) {
106-
// TODO: Implement actual signature verification https://github.com/oras-project/oras-go/issues/1029
107-
return false, fmt.Errorf("signedBy verification not yet implemented")
147+
if e.signedByVerifier == nil {
148+
return false, fmt.Errorf("signedBy verification requires a SignedByVerifier: %w", errdef.ErrUnsupported)
149+
}
150+
return e.signedByVerifier.Verify(ctx, req, image)
108151
}
109152

110153
// evaluateSigstoreSigned evaluates a sigstoreSigned requirement
111-
// Note: This is a placeholder implementation. Full signature verification
112-
// would require integration with sigstore libraries.
113154
func (e *Evaluator) evaluateSigstoreSigned(ctx context.Context, req *PRSigstoreSigned, image ImageReference) (bool, error) {
114-
// TODO: Implement actual sigstore verification https://github.com/oras-project/oras-go/issues/1029
115-
return false, fmt.Errorf("sigstoreSigned verification not yet implemented")
155+
if e.sigstoreVerifier == nil {
156+
return false, fmt.Errorf("sigstoreSigned verification requires a SigstoreVerifier: %w", errdef.ErrUnsupported)
157+
}
158+
return e.sigstoreVerifier.Verify(ctx, req, image)
116159
}
117160

118161
// ShouldAcceptImage is a convenience function that returns true if the image is allowed
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
/*
2+
Copyright The ORAS Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
*/
15+
16+
package policy
17+
18+
import (
19+
"context"
20+
"errors"
21+
"testing"
22+
23+
"github.com/oras-project/oras-go/v3/errdef"
24+
)
25+
26+
// mockSignedByVerifier is a mock implementation of SignedByVerifier for testing
27+
type mockSignedByVerifier struct {
28+
result bool
29+
err error
30+
}
31+
32+
func (m *mockSignedByVerifier) Verify(ctx context.Context, req *PRSignedBy, image ImageReference) (bool, error) {
33+
return m.result, m.err
34+
}
35+
36+
// mockSigstoreVerifier is a mock implementation of SigstoreVerifier for testing
37+
type mockSigstoreVerifier struct {
38+
result bool
39+
err error
40+
}
41+
42+
func (m *mockSigstoreVerifier) Verify(ctx context.Context, req *PRSigstoreSigned, image ImageReference) (bool, error) {
43+
return m.result, m.err
44+
}
45+
46+
func TestEvaluator_WithSignedByVerifier(t *testing.T) {
47+
policy := &Policy{
48+
Default: PolicyRequirements{
49+
&PRSignedBy{
50+
KeyType: "GPGKeys",
51+
KeyPath: "/path/to/key.gpg",
52+
},
53+
},
54+
}
55+
56+
image := ImageReference{
57+
Transport: TransportNameDocker,
58+
Scope: "docker.io/library/nginx",
59+
Reference: "docker.io/library/nginx:latest",
60+
}
61+
62+
tests := []struct {
63+
name string
64+
verifier *mockSignedByVerifier
65+
wantResult bool
66+
wantErr bool
67+
}{
68+
{
69+
name: "verifier returns true",
70+
verifier: &mockSignedByVerifier{result: true, err: nil},
71+
wantResult: true,
72+
wantErr: false,
73+
},
74+
{
75+
name: "verifier returns false",
76+
verifier: &mockSignedByVerifier{result: false, err: nil},
77+
wantResult: false,
78+
wantErr: false,
79+
},
80+
{
81+
name: "verifier returns error",
82+
verifier: &mockSignedByVerifier{result: false, err: errors.New("verification failed")},
83+
wantResult: false,
84+
wantErr: true,
85+
},
86+
}
87+
88+
for _, tt := range tests {
89+
t.Run(tt.name, func(t *testing.T) {
90+
evaluator, err := NewEvaluator(policy, WithSignedByVerifier(tt.verifier))
91+
if err != nil {
92+
t.Fatalf("NewEvaluator() error = %v", err)
93+
}
94+
95+
result, err := evaluator.IsImageAllowed(context.Background(), image)
96+
if (err != nil) != tt.wantErr {
97+
t.Errorf("IsImageAllowed() error = %v, wantErr %v", err, tt.wantErr)
98+
return
99+
}
100+
if result != tt.wantResult {
101+
t.Errorf("IsImageAllowed() = %v, want %v", result, tt.wantResult)
102+
}
103+
})
104+
}
105+
}
106+
107+
func TestEvaluator_WithSigstoreVerifier(t *testing.T) {
108+
policy := &Policy{
109+
Default: PolicyRequirements{
110+
&PRSigstoreSigned{
111+
KeyPath: "/path/to/key.pub",
112+
},
113+
},
114+
}
115+
116+
image := ImageReference{
117+
Transport: TransportNameDocker,
118+
Scope: "docker.io/library/nginx",
119+
Reference: "docker.io/library/nginx:latest",
120+
}
121+
122+
tests := []struct {
123+
name string
124+
verifier *mockSigstoreVerifier
125+
wantResult bool
126+
wantErr bool
127+
}{
128+
{
129+
name: "verifier returns true",
130+
verifier: &mockSigstoreVerifier{result: true, err: nil},
131+
wantResult: true,
132+
wantErr: false,
133+
},
134+
{
135+
name: "verifier returns false",
136+
verifier: &mockSigstoreVerifier{result: false, err: nil},
137+
wantResult: false,
138+
wantErr: false,
139+
},
140+
{
141+
name: "verifier returns error",
142+
verifier: &mockSigstoreVerifier{result: false, err: errors.New("verification failed")},
143+
wantResult: false,
144+
wantErr: true,
145+
},
146+
}
147+
148+
for _, tt := range tests {
149+
t.Run(tt.name, func(t *testing.T) {
150+
evaluator, err := NewEvaluator(policy, WithSigstoreVerifier(tt.verifier))
151+
if err != nil {
152+
t.Fatalf("NewEvaluator() error = %v", err)
153+
}
154+
155+
result, err := evaluator.IsImageAllowed(context.Background(), image)
156+
if (err != nil) != tt.wantErr {
157+
t.Errorf("IsImageAllowed() error = %v, wantErr %v", err, tt.wantErr)
158+
return
159+
}
160+
if result != tt.wantResult {
161+
t.Errorf("IsImageAllowed() = %v, want %v", result, tt.wantResult)
162+
}
163+
})
164+
}
165+
}
166+
167+
func TestEvaluator_NoVerifier_ReturnsUnsupported(t *testing.T) {
168+
tests := []struct {
169+
name string
170+
policy *Policy
171+
}{
172+
{
173+
name: "signedBy without verifier",
174+
policy: &Policy{
175+
Default: PolicyRequirements{
176+
&PRSignedBy{
177+
KeyType: "GPGKeys",
178+
KeyPath: "/path/to/key.gpg",
179+
},
180+
},
181+
},
182+
},
183+
{
184+
name: "sigstoreSigned without verifier",
185+
policy: &Policy{
186+
Default: PolicyRequirements{
187+
&PRSigstoreSigned{
188+
KeyPath: "/path/to/key.pub",
189+
},
190+
},
191+
},
192+
},
193+
}
194+
195+
image := ImageReference{
196+
Transport: TransportNameDocker,
197+
Scope: "docker.io/library/nginx",
198+
Reference: "docker.io/library/nginx:latest",
199+
}
200+
201+
for _, tt := range tests {
202+
t.Run(tt.name, func(t *testing.T) {
203+
evaluator, err := NewEvaluator(tt.policy)
204+
if err != nil {
205+
t.Fatalf("NewEvaluator() error = %v", err)
206+
}
207+
208+
_, err = evaluator.IsImageAllowed(context.Background(), image)
209+
if err == nil {
210+
t.Error("IsImageAllowed() should return error when no verifier is set")
211+
}
212+
if !errors.Is(err, errdef.ErrUnsupported) {
213+
t.Errorf("IsImageAllowed() error should wrap ErrUnsupported, got: %v", err)
214+
}
215+
})
216+
}
217+
}
218+
219+
func TestEvaluator_WithBothVerifiers(t *testing.T) {
220+
policy := &Policy{
221+
Default: PolicyRequirements{
222+
&PRSignedBy{
223+
KeyType: "GPGKeys",
224+
KeyPath: "/path/to/key.gpg",
225+
},
226+
&PRSigstoreSigned{
227+
KeyPath: "/path/to/key.pub",
228+
},
229+
},
230+
}
231+
232+
image := ImageReference{
233+
Transport: TransportNameDocker,
234+
Scope: "docker.io/library/nginx",
235+
Reference: "docker.io/library/nginx:latest",
236+
}
237+
238+
signedByVerifier := &mockSignedByVerifier{result: true, err: nil}
239+
sigstoreVerifier := &mockSigstoreVerifier{result: true, err: nil}
240+
241+
evaluator, err := NewEvaluator(policy,
242+
WithSignedByVerifier(signedByVerifier),
243+
WithSigstoreVerifier(sigstoreVerifier),
244+
)
245+
if err != nil {
246+
t.Fatalf("NewEvaluator() error = %v", err)
247+
}
248+
249+
result, err := evaluator.IsImageAllowed(context.Background(), image)
250+
if err != nil {
251+
t.Errorf("IsImageAllowed() error = %v", err)
252+
}
253+
if !result {
254+
t.Error("IsImageAllowed() should return true when all verifiers pass")
255+
}
256+
}

0 commit comments

Comments
 (0)