diff --git a/cmd/notation/blob/policy/cmd.go b/cmd/notation/blob/policy/cmd.go index e6fd65e60..2a8402d7b 100644 --- a/cmd/notation/blob/policy/cmd.go +++ b/cmd/notation/blob/policy/cmd.go @@ -29,6 +29,7 @@ func Cmd() *cobra.Command { command.AddCommand( importCmd(), showCmd(), + initCmd(), ) return command diff --git a/cmd/notation/blob/policy/init.go b/cmd/notation/blob/policy/init.go new file mode 100644 index 000000000..27e50d487 --- /dev/null +++ b/cmd/notation/blob/policy/init.go @@ -0,0 +1,115 @@ +// 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/cmd/notation/internal/display" + "github.com/notaryproject/notation/cmd/notation/internal/display/output" + "github.com/notaryproject/notation/internal/osutil" + "github.com/spf13/cobra" +) + +type initOpts struct { + printer *output.Printer + name string + trustStores []string + trustedIdentities []string + force bool + global bool +} + +func initCmd() *cobra.Command { + opts := initOpts{} + command := &cobra.Command{ + Use: `init [flags] --name --trust-store ":" --trusted-identity ""`, + Short: "Initialize blob trust policy configuration", + Long: `Initialize blob trust policy configuration. + +Example - init a blob trust policy configuration with a trust store and a trusted identity: + notation blob policy init --name examplePolicy --trust-store ca:exampleStore --trusted-identity "x509.subject: C=US, ST=WA, O=acme-rockets.io" +`, + 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 blob trust policy") + 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 blob trust policy configuration, never prompt (default --force=false)") + command.Flags().BoolVar(&opts.global, "global", false, "set the policy as the global policy (default --global=false)") + command.MarkFlagRequired("name") + command.MarkFlagRequired("trust-store") + command.MarkFlagRequired("trusted-identity") + return command +} + +func runInit(opts *initOpts) error { + blobPolicy := trustpolicy.BlobDocument{ + Version: "1.0", + TrustPolicies: []trustpolicy.BlobTrustPolicy{ + { + Name: opts.name, + SignatureVerification: trustpolicy.SignatureVerification{ + VerificationLevel: trustpolicy.LevelStrict.Name, + }, + TrustStores: opts.trustStores, + TrustedIdentities: opts.trustedIdentities, + GlobalPolicy: opts.global, + }, + }, + } + if err := blobPolicy.Validate(); err != nil { + return fmt.Errorf("invalid blob policy: %w", err) + } + + // optional confirmation + if _, err := trustpolicy.LoadBlobDocument(); err == nil { + if !opts.force { + confirmed, err := display.AskForConfirmation(os.Stdin, "The blob 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 blob trust policy configuration will be overwritten\n") + } + } + + policyPath, err := dir.ConfigFS().SysPath(dir.PathBlobTrustPolicy) + if err != nil { + return fmt.Errorf("failed to obtain path of blob trust policy configuration: %w", err) + } + policyJSON, err := json.MarshalIndent(blobPolicy, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal blob trust policy: %w", err) + } + if err = osutil.WriteFile(policyPath, policyJSON); err != nil { + return fmt.Errorf("failed to write blob trust policy configuration: %w", err) + } + + return opts.printer.Printf("Successfully initialized blob trust policy file to %s.\n", policyPath) +} diff --git a/specs/proposals/blob-signing.md b/specs/proposals/blob-signing.md index ebdb71f8a..f72f44919 100644 --- a/specs/proposals/blob-signing.md +++ b/specs/proposals/blob-signing.md @@ -72,7 +72,7 @@ For file-based distribution, such as SBOMs or release artifacts shared via a web - Set up trust policy for blobs with a new command `notation blob policy init`. This command streamlines the process, eliminating the need for users to consult documentation for the correct trust policy format and preventing the accidental use of policies intended for OCI artifact verification. ```shell - notation blob policy init --name "myBlobPolicy" --trust-store "ca:myCACerts" --trust-identity "x509.subject:C=US,ST=WA,O=wabbit-network.io" + notation blob policy init --name "myBlobPolicy" --trust-store "ca:myCACerts" --trusted-identity "x509.subject:C=US,ST=WA,O=wabbit-network.io" ``` Show the policies configured for verifying blobs: @@ -243,7 +243,7 @@ For registry-based distribution, such as using an OCI-compliant container regist - Set up the trust policy: ```shell - notation blob policy init --name "myBlobPolicy" --trust-store "ca:myCACerts" --trust-identity "x509.subject:C=US,ST=WA,O=wabbit-network.io" + notation blob policy init --name "myBlobPolicy" --trust-store "ca:myCACerts" --trusted-identity "x509.subject:C=US,ST=WA,O=wabbit-network.io" ``` - Download the blob and signature using ORAS tool: @@ -279,19 +279,19 @@ The following commands are available for managing blob trust poliies: - Initialize blob trust policy configuration: ```shell - notation blob policy init --name "myBlobPolicy" --trust-store "ca:myCACerts" --trust-identity "x509.subject:C=US,ST=WA,O=wabbit-network.io" + notation blob policy init --name "myBlobPolicy" --trust-store "ca:myCACerts" --trusted-identity "x509.subject:C=US,ST=WA,O=wabbit-network.io" ``` - Initialize the blob trust policy configuration and set the policy specified by the `--name` flag as the global policy. ```shell - notation blob policy init --global --name "myBlobPolicy" --trust-store "ca:myCACerts" --trust-identity "x509.subject:C=US,ST=WA,O=wabbit-network.io" + notation blob policy init --global --name "myBlobPolicy" --trust-store "ca:myCACerts" --trusted-identity "x509.subject:C=US,ST=WA,O=wabbit-network.io" ``` - Overwrite an existing policy with a prompt: ```shell - notation blob policy init --name "myBlobPolicy" --trust-store "ca:myCACerts" --trust-identity "x509.subject:C=US,ST=WA,O=wabbit-network.io" + notation blob policy init --name "myBlobPolicy" --trust-store "ca:myCACerts" --trusted-identity "x509.subject:C=US,ST=WA,O=wabbit-network.io" ``` If the blob policy named `myBlobPolicy` has already been initialized before, running this command will prompt the user to confirm whether they want to overwrite the existing blob policy. @@ -299,7 +299,7 @@ The following commands are available for managing blob trust poliies: - Overwrite an existing policy with a prompt using the flag `--force`: ```shell - notation blob policy init --force --name "myBlobPolicy" --trust-store "ca:myCACerts" --trust-identity "x509.subject:C=US,ST=WA,O=wabbit-network.io" + notation blob policy init --force --name "myBlobPolicy" --trust-store "ca:myCACerts" --trusted-identity "x509.subject:C=US,ST=WA,O=wabbit-network.io" ``` - Show the blob policy: diff --git a/test/e2e/suite/command/blob/policy.go b/test/e2e/suite/command/blob/policy.go index 8c203a710..90295e284 100644 --- a/test/e2e/suite/command/blob/policy.go +++ b/test/e2e/suite/command/blob/policy.go @@ -222,4 +222,135 @@ var _ = Describe("blob trust policy maintainer", func() { }) }) }) + + When("initializing trust policy", func() { + Context("without existing policy", func() { + opts := Opts() + + It("should fail when no name flag is provided", func() { + Host(opts, func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.ExpectFailure(). + Exec("blob", "policy", "init", "--trust-store", "ca:example-store", "--trusted-identity", "x509.subject: CN=example"). + MatchErrKeyWords("required flag(s)", "name", "not set") + }) + }) + + It("should fail when no trust-store flag is provided", func() { + Host(opts, func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.ExpectFailure(). + Exec("blob", "policy", "init", "--name", "example-policy", "--trusted-identity", "x509.subject: CN=example"). + MatchErrKeyWords("required flag(s)", "trust-store", "not set") + }) + }) + + It("should fail when no trusted-identity flag is provided", func() { + Host(opts, func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.ExpectFailure(). + Exec("blob", "policy", "init", "--name", "example-policy", "--trust-store", "ca:example-store"). + MatchErrKeyWords("required flag(s)", "trusted-identity", "not set") + }) + }) + + It("should fail when invalid trusted-identity format is provided", func() { + Host(opts, func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.ExpectFailure(). + Exec("blob", "policy", "init", + "--name", "example-policy", + "--trust-store", "ca:example-store", + "--trusted-identity", "invalid"). + MatchErrKeyWords("invalid blob policy") + }) + }) + + It("should fail when directory doesn't have write permission", func() { + Host(opts, func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + // Create the notation config directory if it doesn't exist + configDir := vhost.AbsolutePath(NotationDirName) + err := os.MkdirAll(configDir, 0755) + Expect(err).NotTo(HaveOccurred()) + + // Remove write permissions from the directory + err = os.Chmod(configDir, 0500) // r-x for owner, no write + Expect(err).NotTo(HaveOccurred()) + defer os.Chmod(configDir, 0755) // Restore permissions after test + + notation.ExpectFailure(). + Exec("blob", "policy", "init", + "--name", "example-policy", + "--trust-store", "ca:example-store", + "--trusted-identity", "x509.subject: C=example,ST=example,O=example"). + MatchErrKeyWords("failed to write blob trust policy configuration") + }) + }) + + It("should successfully initialize policy when all required flags are provided", func() { + Host(opts, func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.Exec("blob", "policy", "init", + "--name", "example-policy", + "--global", + "--trust-store", "ca:example-store", + "--trust-store", "ca:example-store2", + "--trusted-identity", "x509.subject: C=example,ST=example,O=example", + "--trusted-identity", "x509.subject: C=example2,ST=example,O=example"). + MatchKeyWords("Successfully initialized blob trust policy file") + + // Verify the policy was created + notation.Exec("blob", "policy", "show"). + MatchContent(`{ + "version": "1.0", + "trustPolicies": [ + { + "name": "example-policy", + "signatureVerification": { + "level": "strict" + }, + "trustStores": [ + "ca:example-store", + "ca:example-store2" + ], + "trustedIdentities": [ + "x509.subject: C=example,ST=example,O=example", + "x509.subject: C=example2,ST=example,O=example" + ], + "globalPolicy": true + } + ] +}`) + }) + }) + }) + + Context("with existing policy", func() { + opts := Opts(AddBlobTrustPolicyOption(validBlobTrustPolicyName)) + + It("should canceled when trying to initialize with existing policy", func() { + Host(opts, func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.Exec("blob", "policy", "init", + "--name", "new-policy", + "--trust-store", "ca:new-store", + "--trusted-identity", "x509.subject: C=example,ST=example,O=example"). + MatchKeyWords("The blob trust policy configuration already exists") + }) + }) + + It("should successfully initialize policy with force flag when policy exists", func() { + Host(opts, func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.Exec("blob", "policy", "init", + "--name", "new-policy", + "--trust-store", "ca:new-store", + "--trusted-identity", "x509.subject: C=example, ST=example, O=example", + "--force"). + MatchKeyWords("Successfully initialized blob trust policy file") + + // Verify the new policy was created and replaced the old one + notation.Exec("blob", "policy", "show"). + MatchKeyWords( + "new-policy", + "ca:new-store", + "x509.subject: C=example, ST=example, O=example", + ) + }) + }) + }) + }) }) diff --git a/test/e2e/suite/scenario/blob.go b/test/e2e/suite/scenario/blob.go new file mode 100644 index 000000000..7c01b52b6 --- /dev/null +++ b/test/e2e/suite/scenario/blob.go @@ -0,0 +1,86 @@ +// 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 scenario_test + +import ( + "os" + "path/filepath" + + . "github.com/notaryproject/notation/test/e2e/internal/notation" + "github.com/notaryproject/notation/test/e2e/internal/utils" + . "github.com/notaryproject/notation/test/e2e/suite/common" + . "github.com/onsi/ginkgo/v2" +) + +var _ = Describe("notation blob", Serial, func() { + It("signing and verifying with policy init command", func() { + Host(Opts(), func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { + workDir := vhost.AbsolutePath() + + // create a file to be signed + content := "hello, world" + blobPath := filepath.Join(workDir, "hello.txt") + if err := os.WriteFile(blobPath, []byte(content), 0644); err != nil { + Fail(err.Error()) + } + + // generate a testing key pair + notation.Exec("cert", "generate-test", "--default", "testcert"). + MatchKeyWords( + "Successfully added testcert.crt to named store testcert of type ca", + "testcert: added to the key list", + ) + + // sign the file + notation.WithWorkDir(workDir).Exec("blob", "sign", blobPath). + MatchKeyWords(SignSuccessfully) + + // policy init + notation.Exec("blob", "policy", "init", + "--name", "testpolicy", + "--trust-store", "ca:testcert", + "--trusted-identity", "x509.subject: CN=testcert,O=Notary,L=Seattle,ST=WA,C=US"). + MatchKeyWords( + "Successfully initialized blob trust policy file to", + ) + + notation.Exec("blob", "policy", "show"). + MatchContent(`{ + "version": "1.0", + "trustPolicies": [ + { + "name": "testpolicy", + "signatureVerification": { + "level": "strict" + }, + "trustStores": [ + "ca:testcert" + ], + "trustedIdentities": [ + "x509.subject: CN=testcert,O=Notary,L=Seattle,ST=WA,C=US" + ] + } + ] +}`) + + // verify the blob signature hello.txt.jws.sig + sigPath := blobPath + ".jws.sig" + notation.Exec("blob", "verify", + "--signature", sigPath, + "--policy-name", "testpolicy", + blobPath). + MatchKeyWords(VerifySuccessfully) + }) + }) +})