diff --git a/cmd/notation/policy/cmd.go b/cmd/notation/policy/cmd.go index a243b1dea..c0d0e6061 100644 --- a/cmd/notation/policy/cmd.go +++ b/cmd/notation/policy/cmd.go @@ -23,6 +23,7 @@ func Cmd() *cobra.Command { } command.AddCommand( + initCmd(), showCmd(), importCmd(), ) diff --git a/cmd/notation/policy/init.go b/cmd/notation/policy/init.go new file mode 100644 index 000000000..a33ca8655 --- /dev/null +++ b/cmd/notation/policy/init.go @@ -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 --trust-store ":" --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 \":\"") + command.Flags().StringArrayVar(&opts.trustedIdentities, "trusted-identity", nil, "trusted identity, use the format \"x509.subject:\" for x509 CA scheme and \"\" 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) +} diff --git a/cmd/notation/policy/init_test.go b/cmd/notation/policy/init_test.go new file mode 100644 index 000000000..2c5b31330 --- /dev/null +++ b/cmd/notation/policy/init_test.go @@ -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) + } + }) +} diff --git a/specs/cmd/policy.md b/specs/cmd/policy.md index 8bb7772c1..baddea464 100644 --- a/specs/cmd/policy.md +++ b/specs/cmd/policy.md @@ -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 --trust-store ":" --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 ":" + --trusted-identity stringArray trusted identity, use the format "x509.subject:" for x509 CA scheme and "" for x509 signingAuthority scheme +``` + ### notation policy import ```text @@ -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: