Skip to content

Commit 2c3dc9a

Browse files
committed
feature: add policy.json support
Signed-off-by: Terry Howe <terrylhowe@gmail.com>
1 parent a5c8b77 commit 2c3dc9a

File tree

8 files changed

+1670
-0
lines changed

8 files changed

+1670
-0
lines changed

registry/remote/policy/README.md

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# Policy Package
2+
3+
The `policy` package provides support for the `containers-policy.json` format used for OCI image signature verification policies.
4+
5+
## Overview
6+
7+
This package implements parsing, validation, and evaluation of container image policies as defined in the [containers-policy.json specification](https://man.archlinux.org/man/containers-policy.json.5.en).
8+
9+
## Features
10+
11+
- **Policy Management**: Load, save, and validate policy configurations
12+
- **Multiple Transports**: Support for docker, oci, and other transport types
13+
- **Policy Requirements**:
14+
- `insecureAcceptAnything` - Accept any image without verification
15+
- `reject` - Reject all images
16+
- `signedBy` - Require GPG signature verification (placeholder)
17+
- `sigstoreSigned` - Require sigstore signature verification (placeholder)
18+
- **Scope-based Policies**: Define different policies for different image scopes
19+
- **Identity Matching**: Support for various identity matching strategies
20+
21+
## Usage
22+
23+
### Creating a Policy
24+
25+
```go
26+
import "oras.land/oras-go/v2/registry/remote/policy"
27+
28+
// Create a basic policy
29+
p := &policy.Policy{
30+
Default: policy.PolicyRequirements{&policy.Reject{}},
31+
Transports: map[policy.TransportName]policy.TransportScopes{
32+
policy.TransportDocker: {
33+
"": policy.PolicyRequirements{&policy.InsecureAcceptAnything{}},
34+
},
35+
},
36+
}
37+
```
38+
39+
### Loading and Saving Policies
40+
41+
```go
42+
// Load from default location
43+
policy, err := policy.LoadDefaultPolicy()
44+
if err != nil {
45+
log.Fatal(err)
46+
}
47+
48+
// Load from specific path
49+
policy, err := policy.LoadPolicy("/path/to/policy.json")
50+
if err != nil {
51+
log.Fatal(err)
52+
}
53+
54+
// Save policy
55+
err = policy.SavePolicy(p, "/path/to/policy.json")
56+
if err != nil {
57+
log.Fatal(err)
58+
}
59+
```
60+
61+
### Evaluating Policies
62+
63+
```go
64+
import "context"
65+
66+
// Create an evaluator
67+
evaluator, err := policy.NewEvaluator(p)
68+
if err != nil {
69+
log.Fatal(err)
70+
}
71+
72+
// Check if an image is allowed
73+
image := policy.ImageReference{
74+
Transport: policy.TransportDocker,
75+
Scope: "docker.io/library/nginx",
76+
Reference: "docker.io/library/nginx:latest",
77+
}
78+
79+
allowed, err := evaluator.IsImageAllowed(context.Background(), image)
80+
if err != nil {
81+
log.Fatal(err)
82+
}
83+
84+
fmt.Printf("Image allowed: %v\n", allowed)
85+
```
86+
87+
### Signature Verification Policies
88+
89+
```go
90+
// GPG signature verification
91+
signedByReq := &policy.PRSignedBy{
92+
KeyType: "GPGKeys",
93+
KeyPath: "/path/to/trusted-key.gpg",
94+
SignedIdentity: &policy.SignedIdentity{
95+
Type: policy.MatchRepository,
96+
},
97+
}
98+
99+
// Sigstore signature verification
100+
sigstoreReq := &policy.PRSigstoreSigned{
101+
KeyPath: "/path/to/cosign.pub",
102+
Fulcio: &policy.FulcioConfig{
103+
CAPath: "/path/to/fulcio-ca.pem",
104+
OIDCIssuer: "https://oauth2.sigstore.dev/auth",
105+
SubjectEmail: "user@example.com",
106+
},
107+
RekorPublicKeyPath: "/path/to/rekor.pub",
108+
SignedIdentity: &policy.SignedIdentity{
109+
Type: policy.MatchRepository,
110+
},
111+
}
112+
```
113+
114+
## Policy File Format
115+
116+
The policy.json file follows this structure:
117+
118+
```json
119+
{
120+
"default": [
121+
{"type": "reject"}
122+
],
123+
"transports": {
124+
"docker": {
125+
"": [
126+
{"type": "insecureAcceptAnything"}
127+
],
128+
"docker.io/library/nginx": [
129+
{"type": "reject"}
130+
]
131+
}
132+
}
133+
}
134+
```
135+
136+
## Default Policy Locations
137+
138+
The package checks for policy files in the following order:
139+
140+
1. `$HOME/.config/containers/policy.json` (user-specific)
141+
2. `/etc/containers/policy.json` (system-wide)
142+
143+
## Supported Transports
144+
145+
- `docker` - Docker registries
146+
- `atomic` - Atomic registries
147+
- `containers-storage` - Local containers storage
148+
- `dir` - Directory transport
149+
- `docker-archive` - Docker archive files
150+
- `docker-daemon` - Docker daemon
151+
- `oci` - OCI layout
152+
- `oci-archive` - OCI archive files
153+
- `sif` - Singularity Image Format
154+
- `tarball` - Tarball transport
155+
156+
## Identity Matching Types
157+
158+
- `matchExact` - Exact identity match
159+
- `matchRepoDigestOrExact` - Repository digest or exact match
160+
- `matchRepository` - Repository match
161+
- `exactReference` - Exact reference match
162+
- `exactRepository` - Exact repository match
163+
- `remapIdentity` - Remap identity with prefix
164+
165+
## Limitations
166+
167+
- The `signedBy` and `sigstoreSigned` requirement types are currently placeholders
168+
- Full signature verification requires integration with GPG and sigstore libraries
169+
- Advanced identity matching strategies are defined but not fully implemented
170+
171+
## Testing
172+
173+
Run the tests with:
174+
175+
```bash
176+
go test ./registry/remote/policy/...
177+
```
178+
179+
## References
180+
181+
- [containers-policy.json specification](https://man.archlinux.org/man/containers-policy.json.5.en)
182+
- [OCI Image Spec](https://github.com/opencontainers/image-spec)
183+
- [OCI Distribution Spec](https://github.com/opencontainers/distribution-spec)
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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+
"fmt"
21+
)
22+
23+
// ImageReference represents a reference to an image
24+
type ImageReference struct {
25+
// Transport is the transport type (e.g., "docker")
26+
Transport TransportName
27+
// Scope is the scope within the transport (e.g., "docker.io/library/nginx")
28+
Scope string
29+
// Reference is the full reference (e.g., "docker.io/library/nginx:latest")
30+
Reference string
31+
}
32+
33+
// Evaluator evaluates policy requirements against image references
34+
type Evaluator struct {
35+
policy *Policy
36+
}
37+
38+
// NewEvaluator creates a new policy evaluator
39+
func NewEvaluator(policy *Policy) (*Evaluator, error) {
40+
if policy == nil {
41+
return nil, fmt.Errorf("policy cannot be nil")
42+
}
43+
44+
if err := policy.Validate(); err != nil {
45+
return nil, fmt.Errorf("invalid policy: %w", err)
46+
}
47+
48+
return &Evaluator{
49+
policy: policy,
50+
}, nil
51+
}
52+
53+
// IsImageAllowed determines if an image is allowed by the policy
54+
func (e *Evaluator) IsImageAllowed(ctx context.Context, image ImageReference) (bool, error) {
55+
reqs := e.policy.GetRequirementsForImage(image.Transport, image.Scope)
56+
57+
if len(reqs) == 0 {
58+
// No requirements means reject by default for safety
59+
return false, fmt.Errorf("no policy requirements found for %s:%s", image.Transport, image.Scope)
60+
}
61+
62+
// All requirements must be satisfied
63+
for _, req := range reqs {
64+
allowed, err := e.evaluateRequirement(ctx, req, image)
65+
if err != nil {
66+
return false, fmt.Errorf("failed to evaluate requirement %s: %w", req.Type(), err)
67+
}
68+
if !allowed {
69+
return false, nil
70+
}
71+
}
72+
73+
return true, nil
74+
}
75+
76+
// evaluateRequirement evaluates a single policy requirement
77+
func (e *Evaluator) evaluateRequirement(ctx context.Context, req PolicyRequirement, image ImageReference) (bool, error) {
78+
switch r := req.(type) {
79+
case *InsecureAcceptAnything:
80+
return e.evaluateInsecureAcceptAnything(ctx, r, image)
81+
case *Reject:
82+
return e.evaluateReject(ctx, r, image)
83+
case *PRSignedBy:
84+
return e.evaluateSignedBy(ctx, r, image)
85+
case *PRSigstoreSigned:
86+
return e.evaluateSigstoreSigned(ctx, r, image)
87+
default:
88+
return false, fmt.Errorf("unknown requirement type: %T", req)
89+
}
90+
}
91+
92+
// evaluateInsecureAcceptAnything always accepts the image
93+
func (e *Evaluator) evaluateInsecureAcceptAnything(ctx context.Context, req *InsecureAcceptAnything, image ImageReference) (bool, error) {
94+
return true, nil
95+
}
96+
97+
// evaluateReject always rejects the image
98+
func (e *Evaluator) evaluateReject(ctx context.Context, req *Reject, image ImageReference) (bool, error) {
99+
return false, nil
100+
}
101+
102+
// evaluateSignedBy evaluates a signedBy requirement
103+
// Note: This is a placeholder implementation. Full signature verification
104+
// would require integration with GPG/signing libraries.
105+
func (e *Evaluator) evaluateSignedBy(ctx context.Context, req *PRSignedBy, image ImageReference) (bool, error) {
106+
// TODO: Implement actual signature verification
107+
// This would involve:
108+
// 1. Fetching the image manifest and signatures
109+
// 2. Verifying signatures using the provided GPG keys
110+
// 3. Checking identity matching rules
111+
return false, fmt.Errorf("signedBy verification not yet implemented")
112+
}
113+
114+
// evaluateSigstoreSigned evaluates a sigstoreSigned requirement
115+
// Note: This is a placeholder implementation. Full signature verification
116+
// would require integration with sigstore libraries.
117+
func (e *Evaluator) evaluateSigstoreSigned(ctx context.Context, req *PRSigstoreSigned, image ImageReference) (bool, error) {
118+
// TODO: Implement actual sigstore verification
119+
// This would involve:
120+
// 1. Fetching the image manifest and sigstore signatures
121+
// 2. Verifying signatures using sigstore
122+
// 3. Optionally verifying Fulcio certificates and Rekor transparency log
123+
// 4. Checking identity matching rules
124+
return false, fmt.Errorf("sigstoreSigned verification not yet implemented")
125+
}
126+
127+
// ShouldAcceptImage is a convenience function that returns true if the image is allowed
128+
func ShouldAcceptImage(ctx context.Context, policy *Policy, image ImageReference) (bool, error) {
129+
evaluator, err := NewEvaluator(policy)
130+
if err != nil {
131+
return false, err
132+
}
133+
134+
return evaluator.IsImageAllowed(ctx, image)
135+
}

0 commit comments

Comments
 (0)