Skip to content
Open
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
1 change: 1 addition & 0 deletions cmd/notation/policy/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func Cmd() *cobra.Command {
}

command.AddCommand(
initCmd(),
showCmd(),
importCmd(),
)
Expand Down
134 changes: 134 additions & 0 deletions cmd/notation/policy/init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright The Notary Project 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 policy

import (
"encoding/json"
"fmt"
"os"

"github.com/notaryproject/notation-go/dir"
"github.com/notaryproject/notation-go/verifier/trustpolicy"
"github.com/notaryproject/notation/v2/cmd/notation/internal/display"
"github.com/notaryproject/notation/v2/cmd/notation/internal/display/output"
"github.com/notaryproject/notation/v2/internal/osutil"
"github.com/spf13/cobra"
)

// wildcardRegistryScope is the registry scope that matches any registry. It is
// the default scope for a starter policy so verification applies everywhere
// until the user narrows it down.
const wildcardRegistryScope = "*"

type initOpts struct {
printer *output.Printer
name string
registryScopes []string
trustStores []string
trustedIdentities []string
force bool
}

func initCmd() *cobra.Command {
opts := initOpts{}
command := &cobra.Command{
Use: `init [flags] --name <policy_name> --trust-store "<store_type>:<store_name>" --trusted-identity "<trusted_identity>"`,
Short: "Initialize OCI trust policy configuration",
Long: `Initialize OCI trust policy configuration.

The generated policy statement applies to all registries by default (registry scope "*"). Use --registry-scope to pin it to specific repositories.

Example - init an OCI trust policy configuration with a trust store and a trusted identity:
notation policy init --name examplePolicy --trust-store ca:exampleStore --trusted-identity "x509.subject: C=US, ST=WA, O=acme-rockets.io"

Example - init an OCI trust policy configuration scoped to specific repositories:
notation policy init --name examplePolicy --registry-scope registry.acme-rockets.io/software/net-monitor --trust-store ca:exampleStore --trusted-identity "x509.subject: C=US, ST=WA, O=acme-rockets.io"

Example - init an OCI trust policy configuration with multiple trust stores and trusted identities:
notation policy init --name examplePolicy --trust-store ca:exampleStore --trust-store ca:exampleStore2 --trusted-identity "x509.subject: C=US, ST=WA, O=acme-rockets.io" --trusted-identity "x509.subject: C=US, ST=WA, L=Seattle, O=wabbit-networks.io"

Example - init an OCI trust policy configuration with any trusted identity:
notation policy init --name examplePolicy --trust-store ca:exampleStore --trusted-identity "*"

Example - init an OCI trust policy configuration without prompt:
notation policy init --name examplePolicy --trust-store ca:exampleStore --trusted-identity "x509.subject: C=US, ST=WA, O=acme-rockets.io" --force
`,
Args: cobra.ExactArgs(0),
PreRun: func(cmd *cobra.Command, args []string) {
opts.printer = output.NewPrinter(cmd.OutOrStdout(), cmd.OutOrStderr())
},
RunE: func(cmd *cobra.Command, args []string) error {
return runInit(&opts)
},
}

command.Flags().StringVarP(&opts.name, "name", "n", "", "name of the OCI trust policy")
command.Flags().StringArrayVar(&opts.registryScopes, "registry-scope", []string{wildcardRegistryScope}, "registry scope the policy applies to, e.g. \"registry.acme-rockets.io/software/net-monitor\"; defaults to \"*\" for all registries")
command.Flags().StringArrayVar(&opts.trustStores, "trust-store", nil, "trust store in the format \"<store_type>:<store_name>\"")
command.Flags().StringArrayVar(&opts.trustedIdentities, "trusted-identity", nil, "trusted identity, use the format \"x509.subject:<subject_of_signing_certificate>\" for x509 CA scheme and \"<signing_authority_identity>\" for x509 signingAuthority scheme")
command.Flags().BoolVar(&opts.force, "force", false, "override the existing OCI trust policy configuration, never prompt (default --force=false)")
command.MarkFlagRequired("name")
command.MarkFlagRequired("trust-store")
command.MarkFlagRequired("trusted-identity")
return command
}

func runInit(opts *initOpts) error {
ociPolicy := trustpolicy.OCIDocument{
Version: "1.0",
TrustPolicies: []trustpolicy.OCITrustPolicy{
{
Name: opts.name,
SignatureVerification: trustpolicy.SignatureVerification{
VerificationLevel: trustpolicy.LevelStrict.Name,
},
RegistryScopes: opts.registryScopes,
TrustStores: opts.trustStores,
TrustedIdentities: opts.trustedIdentities,
},
},
}
if err := ociPolicy.Validate(); err != nil {
return fmt.Errorf("invalid OCI policy: %w", err)
}

// optional confirmation
if _, err := trustpolicy.LoadOCIDocument(); err == nil {
if !opts.force {
confirmed, err := display.AskForConfirmation(os.Stdin, "The OCI trust policy configuration already exists, do you want to overwrite it?", opts.force)
if err != nil {
return err
}
if !confirmed {
return nil
}
} else {
opts.printer.PrintErrorf("Warning: existing OCI trust policy configuration will be overwritten\n")
}
}

policyPath, err := dir.ConfigFS().SysPath(dir.PathOCITrustPolicy)
if err != nil {
return fmt.Errorf("failed to obtain path of OCI trust policy configuration: %w", err)
}
policyJSON, err := json.MarshalIndent(ociPolicy, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal OCI trust policy: %w", err)
}
if err = osutil.WriteFile(policyPath, policyJSON); err != nil {
return fmt.Errorf("failed to write OCI trust policy configuration: %w", err)
}

return opts.printer.Printf("Successfully initialized OCI trust policy file to %s.\n", policyPath)
}
104 changes: 104 additions & 0 deletions cmd/notation/policy/init_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright The Notary Project 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 policy

import (
"encoding/json"
"os"
"path/filepath"
"testing"

"github.com/notaryproject/notation-go/dir"
"github.com/notaryproject/notation-go/verifier/trustpolicy"
"github.com/notaryproject/notation/v2/cmd/notation/internal/display/output"
)

func newInitOpts() *initOpts {
return &initOpts{
printer: output.NewPrinter(os.Stdout, os.Stderr),
name: "test-policy",
registryScopes: []string{wildcardRegistryScope},
trustStores: []string{"ca:test-store"},
trustedIdentities: []string{"x509.subject: C=US, ST=WA, O=acme-rockets.io"},
}
}

func TestRunInit(t *testing.T) {
defer func(old string) { dir.UserConfigDir = old }(dir.UserConfigDir)

t.Run("writes a valid starter policy", func(t *testing.T) {
tempRoot := t.TempDir()
dir.UserConfigDir = tempRoot

if err := runInit(newInitOpts()); err != nil {
t.Fatalf("runInit failed: %v", err)
}

policyPath := filepath.Join(tempRoot, "trustpolicy.oci.json")
data, err := os.ReadFile(policyPath)
if err != nil {
t.Fatalf("expected policy file at %s: %v", policyPath, err)
}
var doc trustpolicy.OCIDocument
if err := json.Unmarshal(data, &doc); err != nil {
t.Fatalf("generated policy is not valid JSON: %v", err)
}
if err := doc.Validate(); err != nil {
t.Fatalf("generated policy did not validate: %v", err)
}
if len(doc.TrustPolicies) != 1 || doc.TrustPolicies[0].Name != "test-policy" {
t.Fatalf("unexpected policy content: %+v", doc)
}
if got := doc.TrustPolicies[0].RegistryScopes; len(got) != 1 || got[0] != wildcardRegistryScope {
t.Fatalf("expected wildcard registry scope by default, got %v", got)
}
})

t.Run("refuses invalid input", func(t *testing.T) {
tempRoot := t.TempDir()
dir.UserConfigDir = tempRoot
opts := newInitOpts()
// wildcard scope cannot be combined with another scope
opts.registryScopes = []string{wildcardRegistryScope, "registry.acme-rockets.io/software/net-monitor"}
if err := runInit(opts); err == nil {
t.Fatal("expected validation error for wildcard combined with another scope, got nil")
}
})

t.Run("force overwrites an existing policy", func(t *testing.T) {
tempRoot := t.TempDir()
dir.UserConfigDir = tempRoot
policyPath := filepath.Join(tempRoot, "trustpolicy.oci.json")
if err := os.WriteFile(policyPath, []byte("existing junk"), 0600); err != nil {
t.Fatalf("seeding existing policy failed: %v", err)
}

opts := newInitOpts()
opts.force = true
if err := runInit(opts); err != nil {
t.Fatalf("runInit with --force failed: %v", err)
}
var doc trustpolicy.OCIDocument
data, err := os.ReadFile(policyPath)
if err != nil {
t.Fatalf("reading overwritten policy failed: %v", err)
}
if err := json.Unmarshal(data, &doc); err != nil {
t.Fatalf("overwritten policy is not valid JSON: %v", err)
}
if err := doc.Validate(); err != nil {
t.Fatalf("overwritten policy did not validate: %v", err)
}
})
}
63 changes: 63 additions & 0 deletions specs/cmd/policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,30 @@ Usage:

Available Commands:
import import OCI trust policy configuration from a JSON file
init initialize OCI trust policy configuration
show show OCI trust policy configuration

Flags:
-h, --help help for policy
```

### notation policy init

```text
Initialize OCI trust policy configuration.

Usage:
notation policy init [flags] --name <policy_name> --trust-store "<store_type>:<store_name>" --trusted-identity "<trusted_identity>"

Flags:
--force override the existing OCI trust policy configuration, never prompt (default --force=false)
-h, --help help for init
-n, --name name of the OCI trust policy
--registry-scope stringArray registry scope the policy applies to, e.g. "registry.acme-rockets.io/software/net-monitor"; defaults to "*" for all registries
--trust-store stringArray trust store in the format "<store_type>:<store_name>"
--trusted-identity stringArray trusted identity, use the format "x509.subject:<subject_of_signing_certificate>" for x509 CA scheme and "<signing_authority_identity>" for x509 signingAuthority scheme
```

### notation policy import

```text
Expand Down Expand Up @@ -114,6 +132,51 @@ Flags:

## Usage

### Initialize trust policy configuration

New users often don't have a trust policy yet and hand-writing `trustpolicy.oci.json` from the specification is error prone. `notation policy init` scaffolds a valid single-statement policy from flags so the file is ready for `notation verify` right away.

```shell
notation policy init --name "wabbit-networks-images" --trust-store "ca:wabbit-networks" --trusted-identity "x509.subject:C=US,ST=WA,O=wabbit-networks.io"
```

The statement is generated with `signatureVerification.level` set to `strict`. Sample output for a successful initialization:

```jsonc
{
"version": "1.0",
"trustPolicies": [
{
"name": "wabbit-networks-images",
"registryScopes": [ "*" ],
"signatureVerification": {
"level": "strict"
},
"trustStores": [ "ca:wabbit-networks" ],
"trustedIdentities": [
"x509.subject:C=US,ST=WA,O=wabbit-networks.io"
]
}
]
}
```

The `--trust-store` and `--trusted-identity` flags can be repeated to configure multiple trust stores or trusted identities. To trust any identity, set `--trusted-identity` to `"*"`; this is not recommended for production and cannot be combined with other values.

Unlike a blob trust policy, an OCI trust policy statement is bound to registry scopes rather than a global flag. By default the generated statement uses the wildcard scope `"*"` so it applies to every registry. Use `--registry-scope` (repeatable) to pin it to specific repositories instead:

```shell
notation policy init --name "wabbit-networks-images" --registry-scope "registry.acme-rockets.io/software/net-monitor" --trust-store "ca:wabbit-networks" --trusted-identity "x509.subject:C=US,ST=WA,O=wabbit-networks.io"
```

The generated policy is validated according to [trust policy properties](https://github.com/notaryproject/notaryproject/specs/trust-store-trust-policy.md#trust-policy-properties) before it is written; if validation fails no file is written and the reason is printed to standard error.

If a trust policy configuration already exists, the command prompts for confirmation before overwriting it. Use the `--force` flag to overwrite without a prompt:

```shell
notation policy init --force --name "wabbit-networks-images" --trust-store "ca:wabbit-networks" --trusted-identity "x509.subject:C=US,ST=WA,O=wabbit-networks.io"
```

### Import trust policy configuration from a JSON file

An example of import trust policy configuration from a JSON file:
Expand Down