diff --git a/.github/ISSUE_TEMPLATE/bug-or-issue.yaml b/.github/ISSUE_TEMPLATE/bug-or-issue.yaml index fe07ea791..1898068ea 100644 --- a/.github/ISSUE_TEMPLATE/bug-or-issue.yaml +++ b/.github/ISSUE_TEMPLATE/bug-or-issue.yaml @@ -67,4 +67,4 @@ body: - type: markdown attributes: value: | - If you want to contribute to this project, we will be happy to guide you through out contribution process especially when you already have a good proposal or understanding of how to fix this issue. Join us at https://slack.cncf.io/ and choose #notary-v2 channel. + If you want to contribute to this project, we will be happy to guide you through out contribution process especially when you already have a good proposal or understanding of how to fix this issue. Join us at https://slack.cncf.io/ and choose #notary-project channel. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 751718f42..84d1cc294 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -14,4 +14,4 @@ blank_issues_enabled: false contact_links: - name: Ask a question url: https://slack.cncf.io/ - about: "Join #notary-v2 channel on CNCF Slack" \ No newline at end of file + about: "Join #notary-project channel on CNCF Slack" \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature-request.yaml b/.github/ISSUE_TEMPLATE/feature-request.yaml index 7f5d0b180..3f6b555b3 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yaml +++ b/.github/ISSUE_TEMPLATE/feature-request.yaml @@ -60,4 +60,4 @@ body: - type: markdown attributes: value: | - If you want to contribute to this project, we will be happy to guide you through out contribution process especially when you already have a good proposal or understanding of how to imrpove the functionality. Join us at https://slack.cncf.io/ and choose #notary-v2 channel. + If you want to contribute to this project, we will be happy to guide you through out contribution process especially when you already have a good proposal or understanding of how to imrpove the functionality. Join us at https://slack.cncf.io/ and choose #notary-project channel. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 53b1ec954..d721c4392 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: fail-fast: true steps: - name: Set up Go ${{ matrix.go-version }} - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: go-version: ${{ matrix.go-version }} - name: Check out code diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0c63dc0be..424ce4de2 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,7 +24,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 - name: Set up Go ${{ matrix.go-version }} environment - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: go-version: ${{ matrix.go-version }} check-latest: true diff --git a/.github/workflows/dev-release.yml b/.github/workflows/dev-release.yml index 6ddadf3f3..331f200ec 100644 --- a/.github/workflows/dev-release.yml +++ b/.github/workflows/dev-release.yml @@ -15,7 +15,7 @@ jobs: fail-fast: true steps: - name: Set up Go ${{ matrix.go-version }} - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: go-version: ${{ matrix.go-version }} - name: Checkout diff --git a/.github/workflows/release-github.yml b/.github/workflows/release-github.yml index 310e10c3b..96a5ac22c 100644 --- a/.github/workflows/release-github.yml +++ b/.github/workflows/release-github.yml @@ -15,7 +15,7 @@ jobs: fail-fast: true steps: - name: Set up Go ${{ matrix.go-version }} - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: go-version: ${{ matrix.go-version }} - name: Checkout diff --git a/CODEOWNERS b/CODEOWNERS index eb0aab212..2d47f2394 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1,3 @@ -* @notaryproject/notation-maintainers +# Repo-Level Owners (in alphabetical order) +# Note: This is only for the notaryproject/notation repo +* @gokarnm @JeyJeyGao @justincormack @niazfk @patrickzheng200 @priteshbandi @rgnote @shizhMSFT @stevelasker diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 5e592690a..000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,95 +0,0 @@ -# Contributing - -Notary v2 is [Apache 2.0 licensed](https://github.com/notaryproject/notation/blob/main/LICENSE) and -accepts contributions via GitHub pull requests. This document outlines -some of the conventions on to make it easier to get your contribution -accepted. - -We gratefully welcome improvements to issues and documentation as well as to -code. - -## Certificate of Origin - -By contributing to this project, you agree to the Developer Certificate of -Origin (DCO). This document was created by the Linux Kernel community and is a -simple statement that you, as a contributor, have the legal right to make the -contribution. - -We require all commits to be signed. By signing off with your signature, you -certify that you wrote the patch or otherwise have the right to contribute the -material by the rules of the [DCO](https://github.com/apps/dco): - -`Signed-off-by: Jane Doe ` - -The signature must contain your real name *(sorry, no pseudonyms or anonymous contributions)*. -If your `user.name` and `user.email` are configured in your Git config, -you can sign your commit automatically with `git commit -s`. - -As our project is about code-signing we also highly appreciate it if you sign your commits using a GPG key. :smile: - -## Communications - -For realtime communications we use Slack: To join the conversation, simply -join the [CNCF](https://slack.cncf.io/) Slack workspace and use the -[#notary-v2](https://cloud-native.slack.com/messages/notary-v2/) channel. - -To discuss ideas and specifications we use [Github -Discussions](https://github.com/notaryproject/notaryproject/discussions). - -## Understanding Notary v2 - -This project is composed of: - -- [notation](https://github.com/notaryproject/notation): The Notary v2 CLI and Docker plugins -- [notation-go-lib](https://github.com/notaryproject/notation-go): A collection of libraries for supporting Notation sign, verify of oci artifacts. Based on Notary V2 standard. -- [notaryproject](https://github.com/notaryproject/notaryproject): The Notary v2 requirements and scenarios to frame the scope of the Notary project -- [tuf-notary](https://github.com/notaryproject/tuf): Integration of Notary v2 and TUF - -Also consider checking out our [roadmap](https://github.com/notaryproject/roadmap). - -### Understanding the code - -We are using the following [project-layout](https://github.com/golang-standards/project-layout). - -### How to run the test suite - -Prerequisites: - -- go >= 1.17 - -You can run the unit tests by simply doing - -```bash -make test -``` - -## Acceptance policy - -These things will make a PR more likely to be accepted: - -- a well-described requirement -- tests for new code -- tests for old code! -- new code and tests follow the conventions in old code and tests -- a good commit message ([see below](#format-of-the-commit-message)) -- all code must abide [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments) -- names should abide [What's in a name](https://talks.golang.org/2014/names.slide#1) -- code must build on both Linux, Windows and Darwin, via plain `go build` -- code should have appropriate test coverage and tests should be written - to work with `go test` - -In general, we will merge a PR once one maintainer has endorsed it. -For substantial changes, more people may become involved, and you might -get asked to resubmit the PR or divide the changes into more than one PR. - -### Format of the Commit Message - -We prefer the following rules for good commit messages: - -- Limit the subject to 50 characters and write as the continuation - of the sentence "If applied, this commit will ..." -- Explain what and why in the body, if more than a trivial change; - wrap it at 72 characters. - -The [following article](https://chris.beams.io/posts/git-commit/#seven-rules) -has some more helpful advice on documenting your work. diff --git a/MAINTAINERS b/MAINTAINERS new file mode 100644 index 000000000..9314b5bcb --- /dev/null +++ b/MAINTAINERS @@ -0,0 +1,14 @@ +# Org-Level Maintainers (in alphabetical order) +# Pattern: [First Name] [Last Name] <[Email Address]> ([GitHub Handle]) +Justin Cormack (@justincormack) +Niaz Khan (@niazfk) +Steve Lasker (@stevelasker) + +# Repo-Level Maintainers (in alphabetical order) +# Note: This is for the notaryproject/notation repo +Junjie Gao (@JeyJeyGao) +Milind Gokarn (@gokarnm) +Patrick Zheng (@patrickzheng200) +Pritesh Bandi (@priteshbandi) +Rakesh Gariganti (@rgnote) +Shiwei Zhang (@shizhMSFT) diff --git a/README.md b/README.md index 7bed5adb0..0ac883ce1 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,14 @@ [![codecov](https://codecov.io/gh/notaryproject/notation/branch/main/graph/badge.svg)](https://codecov.io/gh/notaryproject/notation) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/notaryproject/notation/badge)](https://api.securityscorecards.dev/projects/github.com/notaryproject/notation) -Notation is a CLI project to add signatures as standard items in the registry ecosystem, and to build a set of simple tooling for signing and verifying these signatures. This should be viewed as similar security to checking git commit signatures, although the signatures are generic and can be used for additional purposes. Notation is an implementation of the [Notary v2 specifications][notaryv2-specs]. +Notation is a CLI project to add signatures as standard items in the registry ecosystem, and to build a set of simple tooling for signing and verifying these signatures. This should be viewed as similar security to checking git commit signatures, although the signatures are generic and can be used for additional purposes. Notation is an implementation of the [Notary project specifications][notaryv2-specs]. ## Table of Contents - [Documents](#documents) - [Community](#community) - [Development and Contributing](#development-and-contributing) - - [Notary v2 Community Meeting](#notary-v2-community-meeting) + - [Notary project Community Meeting](#notary-project-community-meeting) - [Release Management](#release-management) - [Support](#support) - [Code of Conduct](#code-of-conduct) @@ -27,14 +27,14 @@ Notation is a CLI project to add signatures as standard items in the registry ec ### Development and Contributing - [Build Notation from source code](/building.md) -- [Governance for Notation](https://github.com/notaryproject/notary/blob/master/GOVERNANCE.md) -- [Maintainers and reviewers list](https://github.com/notaryproject/notary/blob/master/MAINTAINERS) -- Regular conversations for Notation occur on the [Cloud Native Computing Slack](https://slack.cncf.io/) **notary-v2** channel. +- [Governance for Notation](https://github.com/notaryproject/.github/blob/master/GOVERNANCE.md) +- [Maintainers and reviewers list](https://github.com/notaryproject/notation/blob/main/CODEOWNERS) +- Regular conversations for Notation occur on the [Cloud Native Computing Slack](https://slack.cncf.io/) **notary-project** channel. -### Notary v2 Community Meeting +### Notary project Community Meeting -- Mondays 5-6 PM Pacific time, 8-9 PM US Eastern, 8-9 AM Shanghai -- Thursdays 9-10 AM Pacific time, 12 PM US Eastern, 5 PM UK +- Mondays 5-6 PM PDT, 4-5 PM PST, 8-9 PM EDT, 7-8 PM EST, 8-9 AM Shanghai +- Thursdays 9-10 AM PDT, 8-9 AM PST, 12 PM EDT, 11 AM EST, 5 PM UK Join us at [Zoom Dial-in link](https://zoom.us/my/cncfnotaryproject) / Passcode: 77777. Please see the [CNCF Calendar](https://www.cncf.io/calendar/) for community meeting details. Meeting notes are captured on [hackmd.io](https://hackmd.io/_vrqBGAOSUC_VWvFzWruZw). diff --git a/cmd/notation/inspect.go b/cmd/notation/inspect.go new file mode 100644 index 000000000..15e6b167c --- /dev/null +++ b/cmd/notation/inspect.go @@ -0,0 +1,302 @@ +package main + +import ( + "crypto/sha1" + b64 "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "os" + "strings" + "strconv" + "time" + + "github.com/notaryproject/notation-core-go/signature" + "github.com/notaryproject/notation-go/plugin/proto" + "github.com/notaryproject/notation-go/registry" + "github.com/notaryproject/notation/internal/cmd" + "github.com/notaryproject/notation/internal/envelope" + "github.com/notaryproject/notation/internal/ioutil" + "github.com/notaryproject/notation/internal/tree" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/spf13/cobra" +) + +type inspectOpts struct { + cmd.LoggingFlagOpts + SecureFlagOpts + reference string + outputFormat string +} + +type inspectOutput struct { + MediaType string `json:"mediaType"` + Signatures []signatureOutput +} + +type signatureOutput struct { + MediaType string `json:"mediaType"` + Digest string `json:"digest"` + SignatureAlgorithm string `json:"signatureAlgorithm"` + SignedAttributes map[string]string `json:"signedAttributes"` + UserDefinedAttributes map[string]string `json:"userDefinedAttributes"` + UnsignedAttributes map[string]string `json:"unsignedAttributes"` + Certificates []certificateOutput `json:"certificates"` + SignedArtifact ocispec.Descriptor `json:"signedArtifact"` +} + +type certificateOutput struct { + SHA1Fingerprint string `json:"SHA1Fingerprint"` + IssuedTo string `json:"issuedTo"` + IssuedBy string `json:"issuedBy"` + Expiry string `json:"expiry"` +} + +func inspectCommand(opts *inspectOpts) *cobra.Command { + if opts == nil { + opts = &inspectOpts{} + } + command := &cobra.Command{ + Use: "inspect [reference]", + Short: "Inspect all signatures associated with the signed artifact", + Long: `Inspect all signatures associated with the signed artifact. + +Example - Inspect signatures on an OCI artifact identified by a digest: + notation inspect /@ + +Example - Inspect signatures on an OCI artifact identified by a tag (Notation will resolve tag to digest): + notation inspect /: + +Example - Inspect signatures on an OCI artifact identified by a digest and output as json: + notation inspect --output json /@ +`, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("missing reference") + } + opts.reference = args[0] + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + return runInspect(cmd, opts) + }, + } + + opts.LoggingFlagOpts.ApplyFlags(command.Flags()) + opts.SecureFlagOpts.ApplyFlags(command.Flags()) + cmd.SetPflagOutput(command.Flags(), &opts.outputFormat, cmd.PflagOutputUsage) + return command +} + +func runInspect(command *cobra.Command, opts *inspectOpts) error { + // set log level + ctx := opts.LoggingFlagOpts.SetLoggerLevel(command.Context()) + + if opts.outputFormat != cmd.OutputJSON && opts.outputFormat != cmd.OutputPlaintext { + return fmt.Errorf("unrecognized output format %s", opts.outputFormat) + } + + // initialize + reference := opts.reference + sigRepo, err := getSignatureRepository(ctx, &opts.SecureFlagOpts, reference) + if err != nil { + return err + } + + manifestDesc, ref, err := getManifestDescriptor(ctx, &opts.SecureFlagOpts, reference, sigRepo) + if err != nil { + return err + } + + // reference is a digest reference + if err := ref.ValidateReferenceAsDigest(); err != nil { + fmt.Fprintf(os.Stderr, "Warning: Always inspect the artifact using digest(@sha256:...) rather than a tag(:%s) because resolved digest may not point to the same signed artifact, as tags are mutable.\n", ref.Reference) + ref.Reference = manifestDesc.Digest.String() + } + + output := inspectOutput{MediaType: manifestDesc.MediaType, Signatures: []signatureOutput{}} + skippedSignatures := false + err = sigRepo.ListSignatures(ctx, manifestDesc, func(signatureManifests []ocispec.Descriptor) error { + for _, sigManifestDesc := range signatureManifests { + sigBlob, sigDesc, err := sigRepo.FetchSignatureBlob(ctx, sigManifestDesc) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: unable to fetch signature %s due to error: %v\n", sigManifestDesc.Digest.String(), err) + skippedSignatures = true + continue + } + + sigEnvelope, err := signature.ParseEnvelope(sigDesc.MediaType, sigBlob) + if err != nil { + logSkippedSignature(sigManifestDesc, err) + skippedSignatures = true + continue + } + + envelopeContent, err := sigEnvelope.Content() + if err != nil { + logSkippedSignature(sigManifestDesc, err) + skippedSignatures = true + continue + } + + signedArtifactDesc, err := envelope.DescriptorFromSignaturePayload(&envelopeContent.Payload) + if err != nil { + logSkippedSignature(sigManifestDesc, err) + skippedSignatures = true + continue + } + + signatureAlgorithm, err := proto.EncodeSigningAlgorithm(envelopeContent.SignerInfo.SignatureAlgorithm) + if err != nil { + logSkippedSignature(sigManifestDesc, err) + skippedSignatures = true + continue + } + + sig := signatureOutput{ + MediaType: sigDesc.MediaType, + Digest: sigManifestDesc.Digest.String(), + SignatureAlgorithm: string(signatureAlgorithm), + SignedAttributes: getSignedAttributes(opts.outputFormat, envelopeContent), + UserDefinedAttributes: signedArtifactDesc.Annotations, + UnsignedAttributes: getUnsignedAttributes(envelopeContent), + Certificates: getCertificates(opts.outputFormat, envelopeContent), + SignedArtifact: *signedArtifactDesc, + } + + // clearing annotations from the SignedArtifact field since they're already + // displayed as UserDefinedAttributes + sig.SignedArtifact.Annotations = nil + + output.Signatures = append(output.Signatures, sig) + } + return nil + }) + + if err != nil { + return err + } + + err = printOutput(opts.outputFormat, ref.String(), output) + if err != nil { + return err + } + + if skippedSignatures { + return errors.New("at least one signature was skipped and not displayed") + } + + return nil +} + +func logSkippedSignature(sigDesc ocispec.Descriptor, err error) { + fmt.Fprintf(os.Stderr, "Warning: Skipping signature %s because of error: %v\n", sigDesc.Digest.String(), err) +} + +func getSignedAttributes(outputFormat string, envContent *signature.EnvelopeContent) map[string]string { + signedAttributes := map[string]string{ + "signingScheme": string(envContent.SignerInfo.SignedAttributes.SigningScheme), + "signingTime": formatTimestamp(outputFormat, envContent.SignerInfo.SignedAttributes.SigningTime), + "expiry": formatTimestamp(outputFormat, envContent.SignerInfo.SignedAttributes.Expiry), + } + + for _, attribute := range envContent.SignerInfo.SignedAttributes.ExtendedAttributes { + signedAttributes[fmt.Sprint(attribute.Key)] = fmt.Sprint(attribute.Value) + } + + return signedAttributes +} + +func getUnsignedAttributes(envContent *signature.EnvelopeContent) map[string]string { + unsignedAttributes := map[string]string{} + + if envContent.SignerInfo.UnsignedAttributes.TimestampSignature != nil { + unsignedAttributes["timestampSignature"] = b64.StdEncoding.EncodeToString(envContent.SignerInfo.UnsignedAttributes.TimestampSignature) + } + + if envContent.SignerInfo.UnsignedAttributes.SigningAgent != "" { + unsignedAttributes["signingAgent"] = envContent.SignerInfo.UnsignedAttributes.SigningAgent + } + + return unsignedAttributes +} + +func formatTimestamp(outputFormat string, t time.Time) string { + switch outputFormat { + case cmd.OutputJSON: + return t.Format(time.RFC3339) + default: + return t.Format(time.ANSIC) + } +} + +func getCertificates(outputFormat string, envContent *signature.EnvelopeContent) []certificateOutput { + certificates := []certificateOutput{} + + for _, cert := range envContent.SignerInfo.CertificateChain { + h := sha1.Sum(cert.Raw) + fingerprint := strings.ToLower(hex.EncodeToString(h[:])) + + certificate := certificateOutput{ + SHA1Fingerprint: fingerprint, + IssuedTo: cert.Subject.String(), + IssuedBy: cert.Issuer.String(), + Expiry: formatTimestamp(outputFormat, cert.NotAfter), + } + + certificates = append(certificates, certificate) + } + + return certificates +} + +func printOutput(outputFormat string, ref string, output inspectOutput) error { + if outputFormat == cmd.OutputJSON { + return ioutil.PrintObjectAsJSON(output) + } + + fmt.Println("Inspecting all signatures for signed artifact") + root := tree.New(ref) + cncfSigNode := root.Add(registry.ArtifactTypeNotation) + + for _, signature := range output.Signatures { + sigNode := cncfSigNode.Add(signature.Digest) + sigNode.AddPair("media type", signature.MediaType) + sigNode.AddPair("signature algorithm", signature.SignatureAlgorithm) + + signedAttributesNode := sigNode.Add("signed attributes") + addMapToTree(signedAttributesNode, signature.SignedAttributes) + + userDefinedAttributesNode := sigNode.Add("user defined attributes") + addMapToTree(userDefinedAttributesNode, signature.UserDefinedAttributes) + + unsignedAttributesNode := sigNode.Add("unsigned attributes") + addMapToTree(unsignedAttributesNode, signature.UnsignedAttributes) + + certListNode := sigNode.Add("certificates") + for _, cert := range signature.Certificates { + certNode := certListNode.AddPair("SHA1 fingerprint", cert.SHA1Fingerprint) + certNode.AddPair("issued to", cert.IssuedTo) + certNode.AddPair("issued by", cert.IssuedBy) + certNode.AddPair("expiry", cert.Expiry) + } + + artifactNode := sigNode.Add("signed artifact") + artifactNode.AddPair("media type", signature.SignedArtifact.MediaType) + artifactNode.AddPair("digest", signature.SignedArtifact.Digest.String()) + artifactNode.AddPair("size", strconv.FormatInt(signature.SignedArtifact.Size, 10)) + } + + root.Print() + return nil +} + +func addMapToTree(node *tree.Node, m map[string]string) { + if len(m) > 0 { + for k, v := range m { + node.AddPair(k, v) + } + } else { + node.Add("(empty)") + } +} diff --git a/cmd/notation/inspect_test.go b/cmd/notation/inspect_test.go new file mode 100644 index 000000000..e143c3003 --- /dev/null +++ b/cmd/notation/inspect_test.go @@ -0,0 +1,71 @@ +package main + +import ( + "testing" + + "github.com/notaryproject/notation/internal/cmd" +) + +func TestInspectCommand_SecretsFromArgs(t *testing.T) { + opts := &inspectOpts{} + command := inspectCommand(opts) + expected := &inspectOpts{ + reference: "ref", + SecureFlagOpts: SecureFlagOpts{ + Password: "password", + PlainHTTP: true, + Username: "user", + }, + outputFormat: cmd.OutputPlaintext, + } + if err := command.ParseFlags([]string{ + "--password", expected.Password, + expected.reference, + "-u", expected.Username, + "--plain-http", + "--output", "text"}); err != nil { + t.Fatalf("Parse Flag failed: %v", err) + } + if err := command.Args(command, command.Flags().Args()); err != nil { + t.Fatalf("Parse Args failed: %v", err) + } + if *opts != *expected { + t.Fatalf("Expect inspect opts: %v, got: %v", expected, opts) + } +} + +func TestInspectCommand_SecretsFromEnv(t *testing.T) { + t.Setenv(defaultUsernameEnv, "user") + t.Setenv(defaultPasswordEnv, "password") + opts := &inspectOpts{} + expected := &inspectOpts{ + reference: "ref", + SecureFlagOpts: SecureFlagOpts{ + Password: "password", + Username: "user", + }, + outputFormat: cmd.OutputJSON, + } + command := inspectCommand(opts) + if err := command.ParseFlags([]string{ + expected.reference, + "--output", "json"}); err != nil { + t.Fatalf("Parse Flag failed: %v", err) + } + if err := command.Args(command, command.Flags().Args()); err != nil { + t.Fatalf("Parse Args failed: %v", err) + } + if *opts != *expected { + t.Fatalf("Expect inspect opts: %v, got: %v", expected, opts) + } +} + +func TestInspectCommand_MissingArgs(t *testing.T) { + command := inspectCommand(nil) + if err := command.ParseFlags(nil); err != nil { + t.Fatalf("Parse Flag failed: %v", err) + } + if err := command.Args(command, command.Flags().Args()); err == nil { + t.Fatal("Parse Args expected error, but ok") + } +} diff --git a/cmd/notation/key.go b/cmd/notation/key.go index 92e6c2530..aff21f636 100644 --- a/cmd/notation/key.go +++ b/cmd/notation/key.go @@ -87,7 +87,7 @@ func keyAddCommand(opts *keyAddOpts) *cobra.Command { }, } opts.LoggingFlagOpts.ApplyFlags(command.Flags()) - command.Flags().StringVarP(&opts.plugin, "plugin", "p", "", "signing plugin name") + command.Flags().StringVar(&opts.plugin, "plugin", "", "signing plugin name") command.MarkFlagRequired("plugin") command.Flags().StringVar(&opts.id, "id", "", "key id (required if --plugin is set)") diff --git a/cmd/notation/main.go b/cmd/notation/main.go index 5f0f3df39..8dc6529bf 100644 --- a/cmd/notation/main.go +++ b/cmd/notation/main.go @@ -4,13 +4,14 @@ import ( "os" "github.com/notaryproject/notation/cmd/notation/cert" + "github.com/notaryproject/notation/cmd/notation/policy" "github.com/spf13/cobra" ) func main() { cmd := &cobra.Command{ Use: "notation", - Short: "Notation - Notary V2 - a tool to sign and verify artifacts", + Short: "Notation - a tool to sign and verify artifacts", SilenceUsage: true, } cmd.AddCommand( @@ -18,11 +19,13 @@ func main() { verifyCommand(nil), listCommand(nil), cert.Cmd(), + policy.Cmd(), keyCommand(), pluginCommand(), loginCommand(nil), logoutCommand(nil), versionCommand(), + inspectCommand(nil), ) if err := cmd.Execute(); err != nil { os.Exit(1) diff --git a/cmd/notation/policy/cmd.go b/cmd/notation/policy/cmd.go new file mode 100644 index 000000000..a8dba8183 --- /dev/null +++ b/cmd/notation/policy/cmd.go @@ -0,0 +1,18 @@ +package policy + +import "github.com/spf13/cobra" + +func Cmd() *cobra.Command { + command := &cobra.Command{ + Use: "policy [command]", + Short: "[Preview] Manage trust policy configuration", + Long: "[Preview] Manage trust policy configuration for signature verification.", + } + + command.AddCommand( + showCmd(), + importCmd(), + ) + + return command +} diff --git a/cmd/notation/policy/import.go b/cmd/notation/policy/import.go new file mode 100644 index 000000000..f3a0efc41 --- /dev/null +++ b/cmd/notation/policy/import.go @@ -0,0 +1,83 @@ +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/cmdutil" + "github.com/notaryproject/notation/internal/osutil" + "github.com/spf13/cobra" +) + +type importOpts struct { + filePath string + force bool +} + +func importCmd() *cobra.Command { + var opts importOpts + command := &cobra.Command{ + Use: "import [flags] ", + Short: "[Preview] Import trust policy configuration from a JSON file", + Long: `[Preview] Import trust policy configuration from a JSON file. + +** This command is in preview and under development. ** + +Example - Import trust policy configuration from a file: + notation policy import my_policy.json +`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.filePath = args[0] + return runImport(cmd, opts) + }, + } + command.Flags().BoolVar(&opts.force, "force", false, "override the existing trust policy configuration, never prompt") + return command +} + +func runImport(command *cobra.Command, opts importOpts) error { + // optional confirmation + if !opts.force { + if _, err := trustpolicy.LoadDocument(); err == nil { + confirmed, err := cmdutil.AskForConfirmation(os.Stdin, "Existing trust policy configuration found, do you want to overwrite it?", opts.force) + if err != nil { + return err + } + if !confirmed { + return nil + } + } + } else { + fmt.Fprintf(os.Stderr, "Warning: existing trust policy configuration file will be overwritten") + } + + // read configuration + policyJSON, err := os.ReadFile(opts.filePath) + if err != nil { + return fmt.Errorf("failed to read trust policy file: %w", err) + } + + // parse and validate + var doc trustpolicy.Document + if err = json.Unmarshal(policyJSON, &doc); err != nil { + return fmt.Errorf("failed to parse trust policy configuration: %w", err) + } + if err = doc.Validate(); err != nil { + return fmt.Errorf("failed to validate trust policy: %w", err) + } + + // write + policyPath, err := dir.ConfigFS().SysPath(dir.PathTrustPolicy) + if err != nil { + return fmt.Errorf("failed to obtain path of trust policy file: %w", err) + } + if err = osutil.WriteFile(policyPath, policyJSON); err != nil { + return fmt.Errorf("failed to write trust policy file: %w", err) + } + _, err = fmt.Fprintln(os.Stdout, "Trust policy configuration imported successfully.") + return err +} diff --git a/cmd/notation/policy/show.go b/cmd/notation/policy/show.go new file mode 100644 index 000000000..d74e6fa5f --- /dev/null +++ b/cmd/notation/policy/show.go @@ -0,0 +1,64 @@ +package policy + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/notaryproject/notation-go/dir" + "github.com/notaryproject/notation-go/verifier/trustpolicy" + "github.com/spf13/cobra" +) + +type showOpts struct { +} + +func showCmd() *cobra.Command { + var opts showOpts + command := &cobra.Command{ + Use: "show [flags]", + Short: "[Preview] Show trust policy configuration", + Long: `[Preview] Show trust policy configuration. + +** This command is in preview and under development. ** + +Example - Show current trust policy configuration: + notation policy show + +Example - Save current trust policy configuration to a file: + notation policy show > my_policy.json +`, + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + return runShow(cmd, opts) + }, + } + return command +} + +func runShow(command *cobra.Command, opts showOpts) error { + // get policy file path + policyPath, err := dir.ConfigFS().SysPath(dir.PathTrustPolicy) + if err != nil { + return fmt.Errorf("failed to obtain path of trust policy configuration file: %w", err) + } + + // core process + policyJSON, err := os.ReadFile(policyPath) + if err != nil { + return fmt.Errorf("failed to load trust policy configuration, you may import one via `notation policy import `: %w", err) + } + var doc trustpolicy.Document + if err = json.Unmarshal(policyJSON, &doc); err == nil { + err = doc.Validate() + } + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error()) + fmt.Fprintf(os.Stderr, "Existing trust policy configuration is invalid, you may update or create a new one via `notation policy import `\n") + // not returning to show the invalid policy configuration + } + + // show policy content + _, err = os.Stdout.Write(policyJSON) + return err +} diff --git a/cmd/notation/registry.go b/cmd/notation/registry.go index 40788d003..ea25eba4b 100644 --- a/cmd/notation/registry.go +++ b/cmd/notation/registry.go @@ -57,22 +57,18 @@ func getSignatureRepositoryForSign(ctx context.Context, opts *SecureFlagOpts, re } // Notation enforces the following two paths during Sign process: - // 1. OCI artifact manifest uses the Referrers API + // 1. OCI artifact manifest uses the Referrers API. // Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#listing-referrers - // 2. OCI image manifest uses the Referrers Tag Schema + // 2. OCI image manifest uses the Referrers API and automatically fallback + // to Referrers Tag Schema if Referrers API is not supported. // Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#referrers-tag-schema if !ociImageManifest { - logger.Info("Use OCI artifact manifest and Referrers API to store signature") + logger.Info("Use OCI artifact manifest to store signature") // ping Referrers API if err := pingReferrersAPI(ctx, remoteRepo); err != nil { return nil, err } logger.Info("Successfully pinged Referrers API on target registry") - } else { - logger.Info("Use OCI image manifest and Referrers Tag Schema to store signature") - if err := remoteRepo.SetReferrersCapability(false); err != nil { - return nil, err - } } repositoryOpts := notationregistry.RepositoryOptions{ OCIImageManifest: ociImageManifest, @@ -204,7 +200,7 @@ func pingReferrersAPI(ctx context.Context, remoteRepo *remote.Repository) error // A 404 returned by Referrers API indicates that Referrers API is // not supported. logger.Infof("failed to ping Referrers API with error: %v", err) - errMsg := "Target registry does not support the Referrers API. Try the flag `--signature-manifest image` to store signatures using OCI image manifest for backwards compatibility" + errMsg := "Target registry does not support the Referrers API. Try removing the flag `--signature-manifest artifact` to store signatures using OCI image manifest" return notationerrors.ErrorReferrersAPINotSupported{Msg: errMsg} } return nil diff --git a/cmd/notation/sign.go b/cmd/notation/sign.go index b8fbb15c6..7567b4949 100644 --- a/cmd/notation/sign.go +++ b/cmd/notation/sign.go @@ -46,7 +46,7 @@ func signCommand(opts *signOpts) *cobra.Command { Prerequisite: a signing key needs to be configured using the command "notation key". -Example - Sign an OCI artifact using the default signing key, with the default JWS envelope: +Example - Sign an OCI artifact using the default signing key, with the default JWS envelope, and use OCI image manifest to store the signature: notation sign /@ Example - Sign an OCI artifact using the default signing key, with the COSE envelope: @@ -61,8 +61,8 @@ Example - Sign an OCI artifact identified by a tag (Notation will resolve tag to Example - Sign an OCI artifact stored in a registry and specify the signature expiry duration, for example 24 hours notation sign --expiry 24h /@ -Example - Sign an OCI artifact and use OCI image manifest to store the signature, with the default JWS envelope: - notation sign --signature-manifest image /@ +Example - [Experimental] Sign an OCI artifact and use OCI artifact manifest to store the signature: + notation sign --signature-manifest artifact /@ `, Args: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { @@ -80,11 +80,11 @@ Example - Sign an OCI artifact and use OCI image manifest to store the signature }, } opts.LoggingFlagOpts.ApplyFlags(command.Flags()) - opts.SignerFlagOpts.ApplyFlags(command.Flags()) + opts.SignerFlagOpts.ApplyFlagsToCommand(command) opts.SecureFlagOpts.ApplyFlags(command.Flags()) cmd.SetPflagExpiry(command.Flags(), &opts.expiry) cmd.SetPflagPluginConfig(command.Flags(), &opts.pluginConfig) - command.Flags().StringVar(&opts.signatureManifest, "signature-manifest", signatureManifestArtifact, "manifest type for signature. options: \"artifact\", \"image\"") + command.Flags().StringVar(&opts.signatureManifest, "signature-manifest", signatureManifestImage, "[Experimental] manifest type for signature. options: \"image\", \"artifact\"") cmd.SetPflagUserMetadata(command.Flags(), &opts.userMetadata, cmd.PflagUserMetadataSignUsage) return command } @@ -94,7 +94,7 @@ func runSign(command *cobra.Command, cmdOpts *signOpts) error { ctx := cmdOpts.LoggingFlagOpts.SetLoggerLevel(command.Context()) // initialize - signer, err := cmd.GetSigner(&cmdOpts.SignerFlagOpts) + signer, err := cmd.GetSigner(ctx, &cmdOpts.SignerFlagOpts) if err != nil { return err } @@ -112,8 +112,8 @@ func runSign(command *cobra.Command, cmdOpts *signOpts) error { _, err = notation.Sign(ctx, signer, sigRepo, opts) if err != nil { var errorPushSignatureFailed notation.ErrorPushSignatureFailed - if errors.As(err, &errorPushSignatureFailed) { - return fmt.Errorf("%v. Target registry does not seem to support OCI artifact manifest. Try the flag `--signature-manifest image` to store signatures using OCI image manifest for backwards compatibility", err) + if errors.As(err, &errorPushSignatureFailed) && !ociImageManifest { + return fmt.Errorf("%v. Possible reason: target registry does not support OCI artifact manifest. Try removing the flag `--signature-manifest artifact` to store signatures using OCI image manifest", err) } return err } diff --git a/cmd/notation/sign_test.go b/cmd/notation/sign_test.go index e6e69e8d2..41b6569b4 100644 --- a/cmd/notation/sign_test.go +++ b/cmd/notation/sign_test.go @@ -23,7 +23,7 @@ func TestSignCommand_BasicArgs(t *testing.T) { Key: "key", SignatureFormat: envelope.JWS, }, - signatureManifest: "artifact", + signatureManifest: "image", } if err := command.ParseFlags([]string{ expected.reference, @@ -87,7 +87,7 @@ func TestSignCommand_CorrectConfig(t *testing.T) { }, expiry: 365 * 24 * time.Hour, pluginConfig: []string{"key0=val0", "key1=val1"}, - signatureManifest: "artifact", + signatureManifest: "image", } if err := command.ParseFlags([]string{ expected.reference, @@ -123,6 +123,214 @@ func TestSignCommand_CorrectConfig(t *testing.T) { } } +func TestSignCommmand_OnDemandKeyOptions(t *testing.T) { + opts := &signOpts{} + command := signCommand(opts) + expected := &signOpts{ + reference: "ref", + SecureFlagOpts: SecureFlagOpts{ + Username: "user", + Password: "password", + }, + SignerFlagOpts: cmd.SignerFlagOpts{ + KeyID: "keyID", + PluginName: "pluginName", + SignatureFormat: envelope.JWS, + }, + signatureManifest: "image", + } + if err := command.ParseFlags([]string{ + expected.reference, + "-u", expected.Username, + "--password", expected.Password, + "--id", expected.KeyID, + "--plugin", expected.PluginName}); err != nil { + t.Fatalf("Parse Flag failed: %v", err) + } + if err := command.Args(command, command.Flags().Args()); err != nil { + t.Fatalf("Parse args failed: %v", err) + } + if !reflect.DeepEqual(*expected, *opts) { + t.Fatalf("Expect sign opts: %v, got: %v", expected, opts) + } +} + +func TestSignCommmand_OnDemandKeyBadOptions(t *testing.T) { + t.Run("error when using id and plugin options with key", func(t *testing.T) { + opts := &signOpts{} + command := signCommand(opts) + expected := &signOpts{ + reference: "ref", + SecureFlagOpts: SecureFlagOpts{ + Username: "user", + Password: "password", + }, + SignerFlagOpts: cmd.SignerFlagOpts{ + KeyID: "keyID", + PluginName: "pluginName", + Key: "keyName", + SignatureFormat: envelope.JWS, + }, + signatureManifest: "image", + } + if err := command.ParseFlags([]string{ + expected.reference, + "-u", expected.Username, + "--password", expected.Password, + "--id", expected.KeyID, + "--plugin", expected.PluginName, + "--key", expected.Key}); err != nil { + t.Fatalf("Parse Flag failed: %v", err) + } + if err := command.Args(command, command.Flags().Args()); err != nil { + t.Fatalf("Parse args failed: %v", err) + } + if !reflect.DeepEqual(*expected, *opts) { + t.Fatalf("Expect sign opts: %v, got: %v", expected, opts) + } + err := command.ValidateFlagGroups() + if err == nil || err.Error() != "if any flags in the group [key id] are set none of the others can be; [id key] were all set" { + t.Fatalf("Didn't get the expected error, but got: %v", err) + } + }) + t.Run("error when using key and id options", func(t *testing.T) { + opts := &signOpts{} + command := signCommand(opts) + expected := &signOpts{ + reference: "ref", + SecureFlagOpts: SecureFlagOpts{ + Username: "user", + Password: "password", + }, + SignerFlagOpts: cmd.SignerFlagOpts{ + KeyID: "keyID", + Key: "keyName", + SignatureFormat: envelope.JWS, + }, + signatureManifest: "image", + } + if err := command.ParseFlags([]string{ + expected.reference, + "-u", expected.Username, + "--password", expected.Password, + "--id", expected.KeyID, + "--key", expected.Key}); err != nil { + t.Fatalf("Parse Flag failed: %v", err) + } + if err := command.Args(command, command.Flags().Args()); err != nil { + t.Fatalf("Parse args failed: %v", err) + } + if !reflect.DeepEqual(*expected, *opts) { + t.Fatalf("Expect sign opts: %v, got: %v", expected, opts) + } + err := command.ValidateFlagGroups() + if err == nil || err.Error() != "if any flags in the group [id plugin] are set they must all be set; missing [plugin]" { + t.Fatalf("Didn't get the expected error, but got: %v", err) + } + }) + t.Run("error when using key and plugin options", func(t *testing.T) { + opts := &signOpts{} + command := signCommand(opts) + expected := &signOpts{ + reference: "ref", + SecureFlagOpts: SecureFlagOpts{ + Username: "user", + Password: "password", + }, + SignerFlagOpts: cmd.SignerFlagOpts{ + PluginName: "pluginName", + Key: "keyName", + SignatureFormat: envelope.JWS, + }, + signatureManifest: "image", + } + if err := command.ParseFlags([]string{ + expected.reference, + "-u", expected.Username, + "--password", expected.Password, + "--plugin", expected.PluginName, + "--key", expected.Key}); err != nil { + t.Fatalf("Parse Flag failed: %v", err) + } + if err := command.Args(command, command.Flags().Args()); err != nil { + t.Fatalf("Parse args failed: %v", err) + } + if !reflect.DeepEqual(*expected, *opts) { + t.Fatalf("Expect sign opts: %v, got: %v", expected, opts) + } + err := command.ValidateFlagGroups() + if err == nil || err.Error() != "if any flags in the group [id plugin] are set they must all be set; missing [id]" { + t.Fatalf("Didn't get the expected error, but got: %v", err) + } + }) + t.Run("error when using id option and not plugin", func(t *testing.T) { + opts := &signOpts{} + command := signCommand(opts) + expected := &signOpts{ + reference: "ref", + SecureFlagOpts: SecureFlagOpts{ + Username: "user", + Password: "password", + }, + SignerFlagOpts: cmd.SignerFlagOpts{ + KeyID: "keyID", + SignatureFormat: envelope.JWS, + }, + signatureManifest: "image", + } + if err := command.ParseFlags([]string{ + expected.reference, + "-u", expected.Username, + "--password", expected.Password, + "--id", expected.KeyID}); err != nil { + t.Fatalf("Parse Flag failed: %v", err) + } + if err := command.Args(command, command.Flags().Args()); err != nil { + t.Fatalf("Parse args failed: %v", err) + } + if !reflect.DeepEqual(*expected, *opts) { + t.Fatalf("Expect sign opts: %v, got: %v", expected, opts) + } + err := command.ValidateFlagGroups() + if err == nil || err.Error() != "if any flags in the group [id plugin] are set they must all be set; missing [plugin]" { + t.Fatalf("Didn't get the expected error, but got: %v", err) + } + }) + t.Run("error when using plugin option and not id", func(t *testing.T) { + opts := &signOpts{} + command := signCommand(opts) + expected := &signOpts{ + reference: "ref", + SecureFlagOpts: SecureFlagOpts{ + Username: "user", + Password: "password", + }, + SignerFlagOpts: cmd.SignerFlagOpts{ + PluginName: "pluginName", + SignatureFormat: envelope.JWS, + }, + signatureManifest: "image", + } + if err := command.ParseFlags([]string{ + expected.reference, + "-u", expected.Username, + "--password", expected.Password, + "--plugin", expected.PluginName}); err != nil { + t.Fatalf("Parse Flag failed: %v", err) + } + if err := command.Args(command, command.Flags().Args()); err != nil { + t.Fatalf("Parse args failed: %v", err) + } + if !reflect.DeepEqual(*expected, *opts) { + t.Fatalf("Expect sign opts: %v, got: %v", expected, opts) + } + err := command.ValidateFlagGroups() + if err == nil || err.Error() != "if any flags in the group [id plugin] are set they must all be set; missing [id]" { + t.Fatalf("Didn't get the expected error, but got: %v", err) + } + }) +} + func TestSignCommand_MissingArgs(t *testing.T) { cmd := signCommand(nil) if err := cmd.ParseFlags(nil); err != nil { diff --git a/cmd/notation/verify.go b/cmd/notation/verify.go index 11bcace99..182bc1cca 100644 --- a/cmd/notation/verify.go +++ b/cmd/notation/verify.go @@ -26,13 +26,6 @@ type verifyOpts struct { reference string pluginConfig []string userMetadata []string - outputFormat string -} - -type verifyOutput struct { - Reference string `json:"reference"` - UserMetadata map[string]string `json:"userMetadata,omitempty"` - Result string `json:"result"` } func verifyCommand(opts *verifyOpts) *cobra.Command { @@ -59,19 +52,14 @@ Example - Verify a signature on an OCI artifact identified by a tag (Notation w opts.reference = args[0] return nil }, - RunE: func(cmnd *cobra.Command, args []string) error { - if opts.outputFormat != cmd.OutputJson && opts.outputFormat != cmd.OutputPlaintext { - return fmt.Errorf("unrecognized output format: %v", opts.outputFormat) - } - - return runVerify(cmnd, opts) + RunE: func(cmd *cobra.Command, args []string) error { + return runVerify(cmd, opts) }, } opts.LoggingFlagOpts.ApplyFlags(command.Flags()) opts.SecureFlagOpts.ApplyFlags(command.Flags()) command.Flags().StringArrayVar(&opts.pluginConfig, "plugin-config", nil, "{key}={value} pairs that are passed as it is to a plugin, if the verification is associated with a verification plugin, refer plugin documentation to set appropriate values") cmd.SetPflagUserMetadata(command.Flags(), &opts.userMetadata, cmd.PflagUserMetadataVerifyUsage) - cmd.SetPflagOutput(command.Flags(), &opts.outputFormat, cmd.PflagOutputUsage) return command } @@ -144,8 +132,13 @@ func runVerify(command *cobra.Command, opts *verifyOpts) error { fmt.Fprintf(os.Stderr, "Warning: %v was set to %q and failed with error: %v\n", result.Type, result.Action, result.Error) } } - - return printResult(opts.outputFormat, ref.String(), outcome) + if reflect.DeepEqual(outcome.VerificationLevel, trustpolicy.LevelSkip) { + fmt.Println("Trust policy is configured to skip signature verification for", ref.String()) + } else { + fmt.Println("Successfully verified signature for", ref.String()) + printMetadataIfPresent(outcome) + } + return nil } func resolveReference(ctx context.Context, opts *SecureFlagOpts, reference string, sigRepo notationregistry.Repository, fn func(registry.Reference, ocispec.Descriptor)) (registry.Reference, error) { @@ -167,33 +160,14 @@ func resolveReference(ctx context.Context, opts *SecureFlagOpts, reference strin return ref, nil } -func printResult(outputFormat, reference string, outcome *notation.VerificationOutcome) error { - if reflect.DeepEqual(outcome.VerificationLevel, trustpolicy.LevelSkip) { - switch outputFormat { - case cmd.OutputJson: - output := verifyOutput{Reference: reference, Result: "SkippedByTrustPolicy", UserMetadata: map[string]string{}} - return ioutil.PrintObjectAsJSON(output) - default: - fmt.Println("Trust policy is configured to skip signature verification for", reference) - return nil - } - } - +func printMetadataIfPresent(outcome *notation.VerificationOutcome) { // the signature envelope is parsed as part of verification. // since user metadata is only printed on successful verification, // this error can be ignored metadata, _ := outcome.UserMetadata() - switch outputFormat { - case cmd.OutputJson: - output := verifyOutput{Reference: reference, Result: "Success", UserMetadata: metadata} - return ioutil.PrintObjectAsJSON(output) - default: - fmt.Println("Successfully verified signature for", reference) - if len(metadata) > 0 { - fmt.Println("\nThe artifact was signed with the following user metadata.") - ioutil.PrintMetadataMap(os.Stdout, metadata) - } - return nil + if len(metadata) > 0 { + fmt.Println("\nThe artifact was signed with the following user metadata.") + ioutil.PrintMetadataMap(os.Stdout, metadata) } } diff --git a/cmd/notation/verify_test.go b/cmd/notation/verify_test.go index 70b2b683e..69d879f25 100644 --- a/cmd/notation/verify_test.go +++ b/cmd/notation/verify_test.go @@ -3,8 +3,6 @@ package main import ( "reflect" "testing" - - "github.com/notaryproject/notation/internal/cmd" ) func TestVerifyCommand_BasicArgs(t *testing.T) { @@ -17,7 +15,6 @@ func TestVerifyCommand_BasicArgs(t *testing.T) { Password: "password", }, pluginConfig: []string{"key1=val1"}, - outputFormat: cmd.OutputPlaintext, } if err := command.ParseFlags([]string{ expected.reference, @@ -43,14 +40,12 @@ func TestVerifyCommand_MoreArgs(t *testing.T) { PlainHTTP: true, }, pluginConfig: []string{"key1=val1", "key2=val2"}, - outputFormat: cmd.OutputJson, } if err := command.ParseFlags([]string{ expected.reference, "--plain-http", "--plugin-config", "key1=val1", - "--plugin-config", "key2=val2", - "--output", "json"}); err != nil { + "--plugin-config", "key2=val2"}); err != nil { t.Fatalf("Parse Flag failed: %v", err) } if err := command.Args(command, command.Flags().Args()); err != nil { diff --git a/cmd/notation/version.go b/cmd/notation/version.go index 369af3e5b..9e18cf5c2 100644 --- a/cmd/notation/version.go +++ b/cmd/notation/version.go @@ -21,7 +21,7 @@ func versionCommand() *cobra.Command { } func runVersion() { - fmt.Printf("Notation: Notary v2, A tool to sign, store, and verify artifacts.\n\n") + fmt.Printf("Notation - a tool to sign and verify artifacts.\n\n") fmt.Printf("Version: %s\n", version.GetVersion()) fmt.Printf("Go version: %s\n", runtime.Version()) diff --git a/go.mod b/go.mod index e2f4ce21b..524b47fc6 100644 --- a/go.mod +++ b/go.mod @@ -4,14 +4,14 @@ go 1.20 require ( github.com/docker/docker-credential-helpers v0.7.0 - github.com/notaryproject/notation-core-go v1.0.0-rc.1 - github.com/notaryproject/notation-go v1.0.0-rc.1.0.20230208032042-6ef3544efa06 + github.com/notaryproject/notation-core-go v1.0.0-rc.2 + github.com/notaryproject/notation-go v1.0.0-rc.3 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.0-rc2 github.com/sirupsen/logrus v1.9.0 github.com/spf13/cobra v1.6.1 github.com/spf13/pflag v1.0.5 - oras.land/oras-go/v2 v2.0.0 + oras.land/oras-go/v2 v2.0.2 ) require ( @@ -24,8 +24,8 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/veraison/go-cose v1.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect - golang.org/x/crypto v0.5.0 // indirect - golang.org/x/mod v0.7.0 // indirect + golang.org/x/crypto v0.6.0 // indirect + golang.org/x/mod v0.8.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.4.0 // indirect + golang.org/x/sys v0.5.0 // indirect ) diff --git a/go.sum b/go.sum index 8221a58d2..7beb80273 100644 --- a/go.sum +++ b/go.sum @@ -20,10 +20,10 @@ github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/notaryproject/notation-core-go v1.0.0-rc.1 h1:ACi0gr6mD1bzp9+gu3P0meJ/N6iWHlyM9zgtdnooNAA= -github.com/notaryproject/notation-core-go v1.0.0-rc.1/go.mod h1:n8Gbvl9sKa00KptkKEL5XKUyMTIALe74QipKauE2rj4= -github.com/notaryproject/notation-go v1.0.0-rc.1.0.20230208032042-6ef3544efa06 h1:0AuNQ3303yvINJSEzHUrLHSsJOyAEJvCGUit44GhERk= -github.com/notaryproject/notation-go v1.0.0-rc.1.0.20230208032042-6ef3544efa06/go.mod h1:B/26FcjJ9GVXm1j7z+/pWKck80LdFi3KiX4Zu7gixB8= +github.com/notaryproject/notation-core-go v1.0.0-rc.2 h1:nNJuXa12jVNSSETjGNJEcZgv1NwY5ToYPo+c0P9syCI= +github.com/notaryproject/notation-core-go v1.0.0-rc.2/go.mod h1:ASoc9KbJkSHLbKhO96lb0pIEWJRMZq9oprwBSZ0EAx0= +github.com/notaryproject/notation-go v1.0.0-rc.3 h1:J93pnI42xw6UzeeCn8a5r3j1n8n5nHjnM3GwrsHzjkQ= +github.com/notaryproject/notation-go v1.0.0-rc.3/go.mod h1:IlP9GVzPUavxljgJIWoHY0GY1unlqfee7tIiCbSem1w= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= @@ -46,10 +46,10 @@ github.com/veraison/go-cose v1.0.0/go.mod h1:7ziE85vSq4ScFTg6wyoMXjucIGOf4JkFEZi github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= -golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= -golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -57,8 +57,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -66,5 +66,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -oras.land/oras-go/v2 v2.0.0 h1:+LRAz92WF7AvYQsQjPEAIw3Xb2zPPhuydjpi4pIHmc0= -oras.land/oras-go/v2 v2.0.0/go.mod h1:iVExH1NxrccIxjsiq17L91WCZ4KIw6jVQyCLsZsu1gc= +oras.land/oras-go/v2 v2.0.2 h1:3aSQdJ7EUC0ft2e9PjJB9Jzastz5ojPA4LzZ3Q4YbUc= +oras.land/oras-go/v2 v2.0.2/go.mod h1:PWnWc/Kyyg7wUTUsDHshrsJkzuxXzreeMd6NrfdnFSo= diff --git a/internal/cmd/flags.go b/internal/cmd/flags.go index f7a62a1b0..6c73eadee 100644 --- a/internal/cmd/flags.go +++ b/internal/cmd/flags.go @@ -13,14 +13,14 @@ import ( const ( OutputPlaintext = "text" - OutputJson = "json" + OutputJSON = "json" ) var ( PflagKey = &pflag.Flag{ Name: "key", Shorthand: "k", - Usage: "signing key name, for a key previously added to notation's key list.", + Usage: "signing key name, for a key previously added to notation's key list. This is mutually exclusive with the --id and --plugin flags", } SetPflagKey = func(fs *pflag.FlagSet, p *string) { fs.StringVarP(p, PflagKey.Name, PflagKey.Shorthand, "", PflagKey.Usage) @@ -41,13 +41,20 @@ var ( fs.StringVar(p, PflagSignatureFormat.Name, defaultSignatureFormat, PflagSignatureFormat.Usage) } - PflagTimestamp = &pflag.Flag{ - Name: "timestamp", - Shorthand: "t", - Usage: "timestamp the signed signature via the remote TSA", + PflagID = &pflag.Flag{ + Name: "id", + Usage: "key id (required if --plugin is set). This is mutually exclusive with the --key flag", } - SetPflagTimestamp = func(fs *pflag.FlagSet, p *string) { - fs.StringVarP(p, PflagTimestamp.Name, PflagTimestamp.Shorthand, "", PflagTimestamp.Usage) + SetPflagID = func(fs *pflag.FlagSet, p *string) { + fs.StringVar(p, PflagID.Name, "", PflagID.Usage) + } + + PflagPlugin = &pflag.Flag{ + Name: "plugin", + Usage: "signing plugin name (required if --id is set). This is mutually exclusive with the --key flag", + } + SetPflagPlugin = func(fs *pflag.FlagSet, p *string) { + fs.StringVar(p, PflagPlugin.Name, "", PflagPlugin.Usage) } PflagExpiry = &pflag.Flag{ @@ -90,7 +97,7 @@ var ( Name: "output", Shorthand: "o", } - PflagOutputUsage = fmt.Sprintf("output format, options: '%s', '%s'", OutputJson, OutputPlaintext) + PflagOutputUsage = fmt.Sprintf("output format, options: '%s', '%s'", OutputJSON, OutputPlaintext) SetPflagOutput = func(fs *pflag.FlagSet, p *string, usage string) { fs.StringVarP(p, PflagOutput.Name, PflagOutput.Shorthand, OutputPlaintext, usage) } diff --git a/internal/cmd/options.go b/internal/cmd/options.go index bf74cacd7..2e925e255 100644 --- a/internal/cmd/options.go +++ b/internal/cmd/options.go @@ -5,6 +5,7 @@ import ( "github.com/notaryproject/notation/internal/trace" "github.com/sirupsen/logrus" + "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -12,12 +13,20 @@ import ( type SignerFlagOpts struct { Key string SignatureFormat string + KeyID string + PluginName string } // ApplyFlags set flags and their default values for the FlagSet -func (opts *SignerFlagOpts) ApplyFlags(fs *pflag.FlagSet) { +func (opts *SignerFlagOpts) ApplyFlagsToCommand(command *cobra.Command) { + fs := command.Flags() SetPflagKey(fs, &opts.Key) SetPflagSignatureFormat(fs, &opts.SignatureFormat) + SetPflagID(fs, &opts.KeyID) + SetPflagPlugin(fs, &opts.PluginName) + command.MarkFlagsRequiredTogether("id", "plugin") + command.MarkFlagsMutuallyExclusive("key", "id") + command.MarkFlagsMutuallyExclusive("key", "plugin") } // LoggingFlagOpts option struct. diff --git a/internal/cmd/signer.go b/internal/cmd/signer.go index 189565133..e17ea3743 100644 --- a/internal/cmd/signer.go +++ b/internal/cmd/signer.go @@ -12,7 +12,18 @@ import ( ) // GetSigner returns a signer according to the CLI context. -func GetSigner(opts *SignerFlagOpts) (notation.Signer, error) { +func GetSigner(ctx context.Context, opts *SignerFlagOpts) (notation.Signer, error) { + // Check if using on-demand key + if opts.KeyID != "" && opts.PluginName != "" && opts.Key == "" { + // Construct a signer from on-demand key + mgr := plugin.NewCLIManager(dir.PluginFS()) + plugin, err := mgr.Get(ctx, opts.PluginName) + if err != nil { + return nil, err + } + return signer.NewFromPlugin(plugin, opts.KeyID, map[string]string{}) + } + // Construct a signer from preconfigured key pair in config.json // if key name is provided as the CLI argument key, err := configutil.ResolveKey(opts.Key) @@ -26,7 +37,7 @@ func GetSigner(opts *SignerFlagOpts) (notation.Signer, error) { // corresponds to an external key if key.ExternalKey != nil { mgr := plugin.NewCLIManager(dir.PluginFS()) - plugin, err := mgr.Get(context.Background(), key.PluginName) + plugin, err := mgr.Get(ctx, key.PluginName) if err != nil { return nil, err } diff --git a/internal/envelope/envelope.go b/internal/envelope/envelope.go index 868e3363a..aeb6f62dc 100644 --- a/internal/envelope/envelope.go +++ b/internal/envelope/envelope.go @@ -1,18 +1,30 @@ package envelope import ( + "encoding/json" + "errors" "fmt" + "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-core-go/signature/cose" "github.com/notaryproject/notation-core-go/signature/jws" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) -// Supported envelope format. const ( + // Supported envelope format. COSE = "cose" JWS = "jws" + + // MediaTypePayloadV1 is the supported content type for signature's payload. + MediaTypePayloadV1 = "application/vnd.cncf.notary.payload.v1+json" ) +// Payload describes the content that gets signed. +type Payload struct { + TargetArtifact ocispec.Descriptor `json:"targetArtifact"` +} + // GetEnvelopeMediaType converts the envelope type to mediaType name. func GetEnvelopeMediaType(sigFormat string) (string, error) { switch sigFormat { @@ -23,3 +35,34 @@ func GetEnvelopeMediaType(sigFormat string) (string, error) { } return "", fmt.Errorf("signature format %q not supported", sigFormat) } + +// ValidatePayloadContentType validates signature payload's content type. +func ValidatePayloadContentType(payload *signature.Payload) error { + switch payload.ContentType { + case MediaTypePayloadV1: + return nil + default: + return fmt.Errorf("payload content type %q not supported", payload.ContentType) + } +} + +// DescriptorFromPayload parses a signature payload and returns the descriptor +// that was signed. Note: the descriptor was signed but may not be trusted +func DescriptorFromSignaturePayload(payload *signature.Payload) (*ocispec.Descriptor, error) { + if payload == nil { + return nil, errors.New("empty payload") + } + + err := ValidatePayloadContentType(payload) + if err != nil { + return nil, err + } + + var parsedPayload Payload + err = json.Unmarshal(payload.Content, &parsedPayload) + if err != nil { + return nil, errors.New("failed to unmarshall the payload content to Payload") + } + + return &parsedPayload.TargetArtifact, nil +} diff --git a/internal/ioutil/print.go b/internal/ioutil/print.go index 96e452212..25075ca83 100644 --- a/internal/ioutil/print.go +++ b/internal/ioutil/print.go @@ -34,7 +34,6 @@ func PrintKeyMap(w io.Writer, target *string, v []config.KeySuite) error { return tw.Flush() } -// PrintMetadataMap prints a map to a given Writer as a table func PrintMetadataMap(w io.Writer, metadata map[string]string) error { tw := newTabWriter(w) fmt.Fprintln(tw, "\nKEY\tVALUE\t") diff --git a/internal/tree/tree.go b/internal/tree/tree.go new file mode 100644 index 000000000..4cf224057 --- /dev/null +++ b/internal/tree/tree.go @@ -0,0 +1,57 @@ +package tree + +import ( + "fmt" +) + +const ( + treeItemPrefix = "├── " + treeItemPrefixLast = "└── " + subTreePrefix = "│ " + subTreePrefixLast = " " +) + +// represents a Node in a tree +type Node struct { + Value string + Children []*Node +} + +// creates a new Node with the given value +func New(value string) *Node { + return &Node{Value: value} +} + +// adds a new child node with the given value +func (parent *Node) Add(value string) *Node { + node := New(value) + parent.Children = append(parent.Children, node) + return node +} + +// adds a new child node with the formatted pair as the value +func (parent *Node) AddPair(key string, value string) *Node { + return parent.Add(key + ": " + value) +} + +// prints the tree represented by the root node +func (root *Node) Print() { + print("", "", "", root) +} + +func print(prefix string, itemMarker string, nextPrefix string, n *Node) { + fmt.Println(prefix + itemMarker + n.Value) + + nextItemPrefix := treeItemPrefix + nextSubTreePrefix := subTreePrefix + + if len(n.Children) > 0 { + for i, child := range n.Children { + if i == len(n.Children)-1 { + nextItemPrefix = treeItemPrefixLast + nextSubTreePrefix = subTreePrefixLast + } + print(nextPrefix, nextItemPrefix, nextPrefix+nextSubTreePrefix, child) + } + } +} diff --git a/internal/tree/tree_test.go b/internal/tree/tree_test.go new file mode 100644 index 000000000..b0284a582 --- /dev/null +++ b/internal/tree/tree_test.go @@ -0,0 +1,83 @@ +package tree + +import ( + "reflect" + "testing" +) + +func TestNodeCreation(t *testing.T) { + node := New("root") + expected := Node{Value: "root"} + + if !reflect.DeepEqual(*node, expected) { + t.Fatalf("expected %+v, got %+v", expected, *node) + } +} + +func TestNodeAdd(t *testing.T) { + root := New("root") + root.Add("child") + + if !root.ContainsChild("child") { + t.Error("expected root to have child node with value 'child'") + t.Fatalf("actual root: %+v", root) + } +} + +func TestNodeAddPair(t *testing.T) { + root := New("root") + root.AddPair("key", "value") + + if !root.ContainsChild("key: value") { + t.Error("expected root to have child node with value 'key: value'") + t.Fatalf("actual root: %+v", root) + } +} + +func ExampleRootPrint() { + root := New("root") + root.Print() + + // Output: + // root +} + +func ExampleSingleLayerPrint() { + root := New("root") + root.Add("child1") + root.Add("child2") + root.Print() + + // Output: + // root + // ├── child1 + // └── child2 +} + +func ExampleMultiLayerPrint() { + root := New("root") + child1 := root.Add("child1") + child1.AddPair("key", "value") + child2 := root.Add("child2") + child2.Add("child2.1") + child2.Add("child2.2") + root.Print() + + // Output: + // root + // ├── child1 + // │ └── key: value + // └── child2 + // ├── child2.1 + // └── child2.2 +} + +func (n *Node) ContainsChild(value string) bool { + for _, child := range n.Children { + if child.Value == value { + return true + } + } + + return false +} diff --git a/internal/version/version.go b/internal/version/version.go index 50be17375..944e9936d 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -2,7 +2,7 @@ package version var ( // Version shows the current notation version, optionally with pre-release. - Version = "v1.0.0-rc.1" + Version = "v1.0.0-rc.3" // BuildMetadata stores the build metadata. // diff --git a/notation b/notation deleted file mode 100755 index a9a700c6e..000000000 Binary files a/notation and /dev/null differ diff --git a/specs/commandline/key.md b/specs/commandline/key.md index de83f5bfb..020f190d3 100644 --- a/specs/commandline/key.md +++ b/specs/commandline/key.md @@ -37,8 +37,8 @@ Flags: --default mark as default -h, --help help for add --id string key id (required if --plugin is set) - -p, --plugin string signing plugin name - -c, --plugin-config stringArray {key}={value} pairs that are passed as it is to a plugin, refer plugin's documentation to set appropriate values + --plugin string signing plugin name + --plugin-config stringArray {key}={value} pairs that are passed as it is to a plugin, refer plugin's documentation to set appropriate values -v, --verbose verbose mode ``` diff --git a/specs/commandline/plugin.md b/specs/commandline/plugin.md index 5e17f62ae..58aeddd4e 100644 --- a/specs/commandline/plugin.md +++ b/specs/commandline/plugin.md @@ -16,6 +16,8 @@ Usage: Available Commands: list List installed plugins + install Installs a plugin + remove Removes a plugin Flags: -h, --help help for plugin @@ -36,15 +38,54 @@ Aliases: list, ls ``` +### notation plugin install + +```text +Installs a plugin + +Usage: + notation plugin install [flags] + +Flags: + -h, --help help for install + -f, --force force the installation of a plugin + +Aliases: + install, add +``` + +### notation plugin remove + +```text +Removes a plugin + +Usage: + notation plugin remove [flags] + +Flags: + -h, --help help for remove + +Aliases: + remove, rm, uninstall, delete +``` + ## Usage ### Install a plugin -Currently there is no subcommand available for plugin installation. Plugin publisher should provide instructions to download and install the plugin. +```shell +notation plugin install +``` + +Upon successful execution, the plugin is copied to plugins directory and name+version of plugin is displayed. If the plugin directory does not exist, it will be created. When an existing plugin is detected, the versions are compared and if the existing plugin is a lower version then it is replaced by the newer version. ### Uninstall a plugin -Currently there is no subcommand available for plugin un-installation. Plugin publisher should provide instructions to uninstall the plugin. +```shell +notation plugin remove +``` + +Upon successful execution, the plugin is removed from the plugins directory. If the plugin is not found, an error is returned showing the syntax for the plugin list command to show the installed plugins. ### List installed plugins @@ -58,5 +99,5 @@ An example of output from `notation plugin list`: ```text NAME DESCRIPTION VERSION CAPABILITIES ERROR -azure-kv Sign artifacts with keys in Azure Key Vault v0.3.1-alpha.1 [SIGNATURE_GENERATOR.RAW] +azure-kv Sign artifacts with keys in Azure Key Vault v0.5.0-rc.1 [SIGNATURE_GENERATOR.RAW] ``` diff --git a/specs/commandline/policy.md b/specs/commandline/policy.md new file mode 100644 index 000000000..56abd236a --- /dev/null +++ b/specs/commandline/policy.md @@ -0,0 +1,160 @@ +# notation policy + +## Description + +As part of signature verification workflow, users need to configure the trust policy configuration file to specify trusted identities that signed the artifacts, the level of signature verification to use and other settings. For more details, see [trust policy specification and examples](https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.2/specs/trust-store-trust-policy.md#trust-policy). + +The `notation policy` command provides a user-friendly way to manage trust policies. It allows users to show trust policy configuration, import/export a trust policy configuration file from/to a JSON file. To get started user can refer to the following trust policy configuration sample. In this sample, there are four policies configured for different requirements: + +- The Policy named "wabbit-networks-images" is for verifying images signed by Wabbit Networks and stored in two repositories `registry.acme-rockets.io/software/net-monitor` and `registry.acme-rockets.io/software/net-logger`. +- Policy named "unsigned-image" is for skipping the verification on unsigned images stored in repository `registry.acme-rockets.io/software/unsigned/net-utils`. +- Policy "allow-expired-images" is for logging instead of failing expired images stored in repository `registry.acme-rockets.io/software/legacy/metrics`. +- Policy "global-policy-for-all-other-images" is for verifying any other images that signed by the ACME Rockets. + +```jsonc +{ + "version": "1.0", + "trustPolicies": [ + { + "name": "wabbit-networks-images", + "registryScopes": [ + "registry.acme-rockets.io/software/net-monitor", + "registry.acme-rockets.io/software/net-logger" + ], + "signatureVerification": { + "level": "strict" + }, + "trustStores": [ + "ca:wabbit-networks", + ], + "trustedIdentities": [ + "x509.subject: C=US, ST=WA, L=Seattle, O=wabbit-networks.io, OU=Security Tools" + ] + }, + { + "name": "unsigned-image", + "registryScopes": [ "registry.acme-rockets.io/software/unsigned/net-utils" ], + "signatureVerification": { + "level" : "skip" + } + }, + { + "name": "allow-expired-images", + "registryScopes": [ "registry.acme-rockets.io/software/legacy/metrics" ], + "signatureVerification": { + "level" : "strict", + "override" : { + "expiry" : "log" + } + }, + "trustStores": ["ca:acme-rockets"], + "trustedIdentities": ["*"] + }, + { + "name": "global-policy-for-all-other-images", + "registryScopes": [ "*" ], + "signatureVerification": { + "level": "strict" + }, + "trustStores": [ + "ca:acme-rockets" + ], + "trustedIdentities": [ + "x509.subject: C=US, ST=WA, L=Seattle, O=acme-rockets.io, CN=SecureBuilder" + ] + } + ] +} +``` + +## Outline + +### notation policy command + +```text +Manage trust policy configuration for signature verification. + +Usage: + notation policy [command] + +Available Commands: + import import trust policy configuration from a JSON file + show show trust policy configuration + +Flags: + -h, --help help for policy +``` + +### notation policy import + +```text +Import trust policy configuration from a JSON file + +Usage: + notation policy import [flags] + +Flags: + --force override the existing trust policy configuration, never prompt + -h, --help help for import +``` + +### notation policy show + +```text +Show trust policy configuration + +Usage: + notation policy show [flags] + +Flags: + -h, --help help for show +``` + +## Usage + +### Import trust policy configuration from a JSON file + +An example of import trust policy configuration from a JSON file: + +```shell +notation policy import ./my_policy.json +``` + +The trust policy configuration in the JSON file should be validated according to [trust policy properties](https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.2/specs/trust-store-trust-policy.md#trust-policy-properties). A successful message should be printed out if trust policy configuration are imported successfully. Error logs including the reason should be printed out if the importing fails. + +If there is an existing trust policy configuration, prompt for users to confirm whether discarding existing configuration or not. Users can use `--force` flag to discard existing trust policy configuration without prompt. + +### Show trust policies + +Use the following command to show trust policy configuration: + +```shell +notation policy show +``` + +Upon successful execution, the trust policy configuration are printed out to standard output. If trust policy is not configured or is malformed, users should receive an error message via standard error output, and a tip to import trust policy configuration from a JSON file. + +### Export trust policy configuration into a JSON file + +Users can redirect the output of command `notation policy show` to a JSON file. + +```shell +notation policy show > ./trust_policy.json +``` + +### Update trust policy configuration + +The steps to update trust policy configuration: + +1. Export trust policy configuration into a JSON file. + + ```shell + notation policy show > ./trust_policy.json + ``` + +2. Edit the exported JSON file "trust_policy.json", update trust policy configuration and save the file. +3. Import trust policy configuration from the file. + + ```shell + notation policy import ./trust_policy.json + ``` diff --git a/specs/commandline/sign.md b/specs/commandline/sign.md index 70aa13e44..5cb19f4ba 100644 --- a/specs/commandline/sign.md +++ b/specs/commandline/sign.md @@ -31,12 +31,14 @@ Flags: -d, --debug debug mode -e, --expiry duration optional expiry that provides a "best by use" time for the artifact. The duration is specified in minutes(m) and/or hours(h). For example: 12h, 30m, 3h20m -h, --help help for sign - -k, --key string signing key name, for a key previously added to notation's key list. + --id string key id (required if --plugin is set). This is mutually exclusive with the --key flag + -k, --key string signing key name, for a key previously added to notation's key list. This is mutually exclusive with the --id and --plugin flags -p, --password string password for registry operations (default to $NOTATION_PASSWORD if not specified) --plain-http registry access via plain HTTP - --plugin-config stringArray {key}={value} pairs that are passed as it is to a plugin, refer plugin's documentation to set appropriate values + --plugin string signing plugin name. This is mutually exclusive with the --key flag + --plugin-config stringArray {key}={value} pairs that are passed as it is to a plugin, refer plugin's documentation to set appropriate values. --signature-format string signature envelope format, options: "jws", "cose" (default "jws") - --signature-manifest string manifest type for signature, options: "image", "artifact" (default "artifact") + --signature-manifest string [Experimental] manifest type for signature, options: "image", "artifact" (default "image") -u, --username string username for registry operations (default to $NOTATION_USERNAME if not specified) -m, --user-metadata stringArray {key}={value} pairs that are added to the signature payload -v, --verbose verbose mode @@ -44,7 +46,7 @@ Flags: ## Use OCI image manifest to store signatures -By default, Notation uses [OCI artifact manifest][oci-artifact-manifest] to store signatures in registries. For registries that don't support `OCI artifact` or [Referrers API][oci-referers-api] is not enabled, users SHOULD use flag `--signature-manifest image` to force Notation to store the signatures using [OCI image manifest][oci-image-spec]. +By default, Notation uses [OCI image manifest][oci-image-spec] to store signatures in registries. Users can use [OCI artifact manifest][oci-artifact-manifest] by enabling the `--signature-manifest artifact` flag. This is an experimental feature, which is not intended for production use and may change or be removed in future versions. When using OCI artifact manifest to store the signature, the registry is REQUIRED to support both `OCI artifact` and [Referrers API][oci-referers-api]. Note that there is no deterministic way to determine whether a registry supports `OCI artifact` or not. The following response status contained in error messages MAY indicate that the registry doesn't support `OCI artifact`. @@ -52,9 +54,7 @@ Note that there is no deterministic way to determine whether a registry supports ### Set config property for OCI image manifest -OCI image manifest requires additional property `config` of type `descriptor`, which is not required by OCI artifact manifest. Notation creates a default config descriptor for the user if flag `--signature-manifest image` is used. - -Notation uses empty JSON object `{}` as the default configuration content, and thus the default `config` property is fixed, as following: +OCI image manifest requires additional property `config` of type `descriptor`, which is not required by OCI artifact manifest. When signing with OCI image manifest, Notation uses empty JSON object `{}` as the default configuration content, and thus the `config` property is fixed, as following: ```json "config": { @@ -66,7 +66,7 @@ Notation uses empty JSON object `{}` as the default configuration content, and t ## Usage -### Sign an OCI artifact +### Sign an OCI artifact by adding new key ```shell # Prerequisites: @@ -87,6 +87,12 @@ $ notation sign localhost:5000/net-monitor@sha256:b94d27b9934d3e08a52e52d7da7dab Successfully signed localhost:5000/net-monitor@sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9 ``` +### Sign an OCI artifact with on-demand remote key + +```shell +notation sign --plugin --id /@ +``` + ### Sign an OCI artifact using COSE signature format ```shell @@ -154,10 +160,10 @@ Warning: Always sign the artifact using digest(`@sha256:...`) rather than a tag( Successfully signed localhost:5000/net-monitor@sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9 ``` -### Sign an artifact and store the signature using OCI image manifest +### [Experimental] Sign an artifact and store the signature using OCI artifact manifest ```shell -notation sign --signature-manifest image /@ +notation sign --signature-manifest artifact /@ ``` [oci-artifact-manifest]: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/artifact.md diff --git a/specs/commandline/verify.md b/specs/commandline/verify.md index 5659ce7c2..3741cb966 100644 --- a/specs/commandline/verify.md +++ b/specs/commandline/verify.md @@ -37,12 +37,11 @@ Usage: Flags: -d, --debug debug mode -h, --help help for verify - -o, --output string output format, options: 'json', 'text' (default "text") -p, --password string password for registry operations (default to $NOTATION_PASSWORD if not specified) --plain-http registry access via plain HTTP --plugin-config stringArray {key}={value} pairs that are passed as it is to a plugin, if the verification is associated with a verification plugin, refer plugin documentation to set appropriate values - -m, --user-metadata stringArray user defined {key}={value} pairs that must be present in the signature for successful verification if provided -u, --username string username for registry operations (default to $NOTATION_USERNAME if not specified) + -m, --user-metadata stringArray user defined {key}={value} pairs that must be present in the signature for successful verification if provided -v, --verbose verbose mode ``` @@ -169,25 +168,3 @@ An example of output messages for a successful verification: Warning: Always verify the artifact using digest(@sha256:...) rather than a tag(:v1) because resolved digest may not point to the same signed artifact, as tags are mutable. Successfully verified signature for localhost:5000/net-monitor@sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9 ``` - -### Verify signatures on an OCI artifact with json output - -Use the `--output` flag to format successful verification output in json. - -```shell -notation verify --output json localhost:5000/net-monitor@sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9 -``` - -An example of output messages for a successful verification: - -```text -{ - "reference": "localhost:5000/net-monitor@sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", - "userMetadata": { - "io.wabbit-networks.buildId": "123" - }, - "result": "Success" -} -``` - -On unsuccessful verification, nothing is written to `stdout`, and the failure is logged to `stderr`. diff --git a/specs/commandline/version.md b/specs/commandline/version.md index a6e96e141..a51ee1ae2 100644 --- a/specs/commandline/version.md +++ b/specs/commandline/version.md @@ -7,7 +7,7 @@ Use `notation version` to print notation version information. Upon successful execution, the following information is printed. ```text -Notation: Notary v2, A tool to sign, store, and verify artifacts. +Notation - a tool to sign and verify artifacts. Version: Go version: go @@ -37,7 +37,7 @@ notation version An example output: ```text -Notation: Notary v2, A tool to sign, store, and verify artifacts. +Notation - a tool to sign and verify artifacts. Version: 1.0.0 Go Version: go1.19.2 diff --git a/specs/notation-cli.md b/specs/notation-cli.md index 49e6726d8..5cb9df73e 100644 --- a/specs/notation-cli.md +++ b/specs/notation-cli.md @@ -4,24 +4,24 @@ This spec contains reference information on using notation commands. Each comman ## Notation Commands -| Command | Description | -| ------------------------------------------- | ----------------------------------------- | -| [certificate](./commandline/certificate.md) | Manage certificates in trust store | -| [inspect](./commandline/inspect.md) | Inspect signatures | -| [key](./commandline/key.md) | Manage keys used for signing | -| [list](./commandline/list.md) | List signatures of the signed artifact | -| [login](./commandline/login.md) | Login to registries | -| [logout](./commandline/logout.md) | Log out from the logged in registries | -| [plugin](./commandline/plugin.md) | Manage plugins | -| [sign](./commandline/sign.md) | Sign artifacts | -| [verify](./commandline/verify.md) | Verify artifacts | -| [version](./commandline/version.md) | Print the version of notation CLI | - +| Command | Description | +| ------------------------------------------- | ---------------------------------------------------------------------- | +| [certificate](./commandline/certificate.md) | Manage certificates in trust store | +| [inspect](./commandline/inspect.md) | Inspect signatures | +| [key](./commandline/key.md) | Manage keys used for signing | +| [list](./commandline/list.md) | List signatures of the signed artifact | +| [login](./commandline/login.md) | Login to registries | +| [logout](./commandline/logout.md) | Log out from the logged in registries | +| [plugin](./commandline/plugin.md) | Manage plugins | +| [policy](./commandline/policy.md) | [Preview] Manage trust policy configuration for signature verification | +| [sign](./commandline/sign.md) | Sign artifacts | +| [verify](./commandline/verify.md) | Verify artifacts | +| [version](./commandline/version.md) | Print the version of notation CLI | ## Notation Outline ```text -Notation - Notary V2 - a tool to sign and verify artifacts +Notation - a tool to sign and verify artifacts Usage: notation [command] @@ -34,6 +34,7 @@ Available Commands: login Login to registry logout Log out from the logged in registries plugin Manage plugins + policy [Preview] Manage trust policy configuration for signature verification sign Sign artifacts verify Verify artifacts version Show the notation version information diff --git a/test/e2e/go.mod b/test/e2e/go.mod index 89873fd37..05776d3ee 100644 --- a/test/e2e/go.mod +++ b/test/e2e/go.mod @@ -17,10 +17,10 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/veraison/go-cose v1.0.0-rc.2 // indirect github.com/x448/float16 v0.8.4 // indirect - golang.org/x/net v0.1.0 // indirect + golang.org/x/net v0.7.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.1.0 // indirect - golang.org/x/text v0.4.0 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/text v0.7.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/test/e2e/go.sum b/test/e2e/go.sum index ea35719f5..bdae30519 100644 --- a/test/e2e/go.sum +++ b/test/e2e/go.sum @@ -17,14 +17,14 @@ github.com/veraison/go-cose v1.0.0-rc.2 h1:zH3QmP4N5kwpdGauceIT3aJm8iUyV9OqpUOb+ github.com/veraison/go-cose v1.0.0-rc.2/go.mod h1:7ziE85vSq4ScFTg6wyoMXjucIGOf4JkFEZi/an96Ct4= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/test/e2e/run.sh b/test/e2e/run.sh index 5a59e39bf..b4e9b7ba9 100755 --- a/test/e2e/run.sh +++ b/test/e2e/run.sh @@ -40,7 +40,7 @@ if [ ! -f "$NOTATION_E2E_OLD_BINARY_PATH" ]; then echo "Try to use old notation binary at $NOTATION_E2E_OLD_BINARY_PATH" if [ ! -f $NOTATION_E2E_OLD_BINARY_PATH ]; then - TAG=1.0.0-rc.1 # without 'v' + TAG=1.0.0-rc.2 # without 'v' echo "Didn't find old notation binary locally. Try to download notation v$TAG." TAR_NAME=notation_${TAG}_linux_amd64.tar.gz diff --git a/test/e2e/suite/command/policy.go b/test/e2e/suite/command/policy.go new file mode 100644 index 000000000..79c3abe10 --- /dev/null +++ b/test/e2e/suite/command/policy.go @@ -0,0 +1,188 @@ +package command + +import ( + "os" + "path/filepath" + "strings" + + . "github.com/notaryproject/notation/test/e2e/internal/notation" + "github.com/notaryproject/notation/test/e2e/internal/utils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("trust policy maintainer", func() { + When("showing configuration", func() { + It("should show error and hint if policy doesn't exist", func() { + Host(Opts(), func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.ExpectFailure(). + Exec("policy", "show"). + MatchErrKeyWords("failed to load trust policy configuration", "notation policy import") + }) + }) + + It("should show exist policy", func() { + content, err := os.ReadFile(filepath.Join(NotationE2ETrustPolicyDir, TrustPolicyName)) + Expect(err).NotTo(HaveOccurred()) + Host(Opts(AddTrustPolicyOption(TrustPolicyName)), func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.Exec("policy", "show"). + MatchContent(string(content)) + }) + }) + + It("should display error hint when showing invalid policy", func() { + policyName := "invalid_format_trustpolicy.json" + content, err := os.ReadFile(filepath.Join(NotationE2ETrustPolicyDir, policyName)) + Expect(err).NotTo(HaveOccurred()) + Host(Opts(AddTrustPolicyOption(policyName)), func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.Exec("policy", "show"). + MatchErrKeyWords("existing trust policy configuration is invalid"). + MatchContent(string(content)) + }) + }) + }) + + When("importing configuration without existing trust policy configuration", func() { + opts := Opts() + It("should fail if no file path is provided", func() { + Host(opts, func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.ExpectFailure(). + Exec("policy", "import") + }) + }) + + It("should fail if provided file doesn't exist", func() { + Host(opts, func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.ExpectFailure(). + Exec("policy", "import", "/??/???") + }) + }) + + It("should fail if registry scope is malformed", func() { + Host(opts, func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.ExpectFailure(). + Exec("policy", "import", filepath.Join(NotationE2ETrustPolicyDir, "malformed_registry_scope_trustpolicy.json")) + }) + }) + + It("should fail if store is malformed", func() { + Host(opts, func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.ExpectFailure(). + Exec("policy", "import", filepath.Join(NotationE2ETrustPolicyDir, "malformed_trust_store_trustpolicy.json")) + }) + }) + + It("should fail if identity is malformed", func() { + Host(opts, func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.ExpectFailure(). + Exec("policy", "import", filepath.Join(NotationE2ETrustPolicyDir, "malformed_trusted_identity_trustpolicy.json")) + }) + }) + + It("should import successfully", func() { + Host(opts, func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.Exec("policy", "import", filepath.Join(NotationE2ETrustPolicyDir, TrustPolicyName)) + }) + }) + + It("should import successfully by force", func() { + Host(opts, func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.Exec("policy", "import", filepath.Join(NotationE2ETrustPolicyDir, TrustPolicyName), "--force") + }) + }) + }) + + When("importing configuration with existing trust policy configuration", func() { + opts := Opts(AddTrustPolicyOption(TrustPolicyName)) + It("should fail if no file path is provided", func() { + Host(opts, func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.ExpectFailure(). + Exec("policy", "import") + }) + }) + + It("should fail if provided file doesn't exist", func() { + Host(opts, func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.ExpectFailure(). + Exec("policy", "import", "/??/???", "--force") + }) + }) + + It("should fail if registry scope is malformed", func() { + Host(opts, func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.WithInput(strings.NewReader("Y\n")).ExpectFailure(). + Exec("policy", "import", filepath.Join(NotationE2ETrustPolicyDir, "malformed_registry_scope_trustpolicy.json")) + }) + }) + + It("should fail if store is malformed", func() { + Host(opts, func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.WithInput(strings.NewReader("Y\n")).ExpectFailure(). + Exec("policy", "import", filepath.Join(NotationE2ETrustPolicyDir, "malformed_trust_store_trustpolicy.json")) + }) + }) + + It("should fail if identity is malformed", func() { + Host(opts, func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.WithInput(strings.NewReader("Y\n")).ExpectFailure(). + Exec("policy", "import", filepath.Join(NotationE2ETrustPolicyDir, "malformed_trusted_identity_trustpolicy.json")) + }) + }) + + It("should cancel import with N", func() { + Host(opts, func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.WithInput(strings.NewReader("N\n")).Exec("policy", "import", filepath.Join(NotationE2ETrustPolicyDir, "skip_trustpolicy.json")) + // validate + content, err := os.ReadFile(filepath.Join(NotationE2ETrustPolicyDir, TrustPolicyName)) + Expect(err).NotTo(HaveOccurred()) + notation.Exec("policy", "show").MatchContent(string(content)) + }) + }) + + It("should cancel import by default", func() { + Host(opts, func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.Exec("policy", "import", filepath.Join(NotationE2ETrustPolicyDir, "skip_trustpolicy.json")) + // validate + content, err := os.ReadFile(filepath.Join(NotationE2ETrustPolicyDir, TrustPolicyName)) + Expect(err).NotTo(HaveOccurred()) + notation.Exec("policy", "show").MatchContent(string(content)) + }) + }) + + It("should skip confirmation if existing policy is malformed", func() { + Host(Opts(AddTrustPolicyOption("invalid_format_trustpolicy.json")), func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + policyFileName := "skip_trustpolicy.json" + notation.Exec("policy", "import", filepath.Join(NotationE2ETrustPolicyDir, policyFileName)).MatchKeyWords(). + MatchKeyWords("Trust policy configuration imported successfully.") + // validate + content, err := os.ReadFile(filepath.Join(NotationE2ETrustPolicyDir, policyFileName)) + Expect(err).NotTo(HaveOccurred()) + notation.Exec("policy", "show").MatchContent(string(content)) + }) + }) + + It("should confirm import", func() { + Host(opts, func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + policyFileName := "skip_trustpolicy.json" + notation.WithInput(strings.NewReader("Y\n")).Exec("policy", "import", filepath.Join(NotationE2ETrustPolicyDir, policyFileName)). + MatchKeyWords("Trust policy configuration imported successfully.") + // validate + content, err := os.ReadFile(filepath.Join(NotationE2ETrustPolicyDir, policyFileName)) + Expect(err).NotTo(HaveOccurred()) + notation.Exec("policy", "show").MatchContent(string(content)) + }) + }) + + It("should confirm import by force", func() { + Host(opts, func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + policyFileName := "skip_trustpolicy.json" + notation.Exec("policy", "import", filepath.Join(NotationE2ETrustPolicyDir, policyFileName), "--force"). + MatchKeyWords("Trust policy configuration imported successfully.") + // validate + content, err := os.ReadFile(filepath.Join(NotationE2ETrustPolicyDir, policyFileName)) + Expect(err).NotTo(HaveOccurred()) + notation.Exec("policy", "show").MatchContent(string(content)) + }) + }) + }) +}) diff --git a/test/e2e/suite/command/verify.go b/test/e2e/suite/command/verify.go index 9837c1f64..a2d7236ff 100644 --- a/test/e2e/suite/command/verify.go +++ b/test/e2e/suite/command/verify.go @@ -15,7 +15,7 @@ var _ = Describe("notation verify", func() { OldNotation().Exec("sign", artifact.ReferenceWithDigest()). MatchKeyWords(SignSuccessfully) - notation.Exec("verify", artifact.ReferenceWithDigest()). + notation.Exec("verify", artifact.ReferenceWithDigest(), "-v"). MatchKeyWords(VerifySuccessfully) }) }) @@ -25,7 +25,7 @@ var _ = Describe("notation verify", func() { OldNotation().Exec("sign", artifact.ReferenceWithDigest()). MatchKeyWords(SignSuccessfully) - notation.Exec("verify", artifact.ReferenceWithTag()). + notation.Exec("verify", artifact.ReferenceWithTag(), "-v"). MatchKeyWords(VerifySuccessfully) }) }) @@ -49,45 +49,4 @@ var _ = Describe("notation verify", func() { MatchKeyWords(VerifySuccessfully) }) }) - - It("with added user metadata", func() { - Host(BaseOptions(), func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { - notation.Exec("sign", artifact.ReferenceWithDigest(), "--user-metadata", "io.wabbit-networks.buildId=123"). - MatchKeyWords(SignSuccessfully) - - notation.Exec("verify", artifact.ReferenceWithTag()). - MatchKeyWords( - VerifySuccessfully, - "KEY", - "VALUE", - "io.wabbit-networks.buildId", - "123", - ) - - notation.Exec("verify", artifact.ReferenceWithDigest(), "--user-metadata", "io.wabbit-networks.buildId=123"). - MatchKeyWords( - VerifySuccessfully, - "KEY", - "VALUE", - "io.wabbit-networks.buildId", - "123", - ) - - notation.ExpectFailure().Exec("verify", artifact.ReferenceWithDigest(), "--user-metadata", "io.wabbit-networks.buildId=321"). - MatchErrKeyWords("unable to find specified metadata in the signature") - }) - }) - - It("with json output", func() { - Host(BaseOptions(), func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { - notation.Exec("sign", artifact.ReferenceWithDigest(), "--user-metadata", "io.wabbit-networks.buildId=123"). - MatchKeyWords(SignSuccessfully) - - notation.Exec("verify", artifact.ReferenceWithDigest(), "--output", "json"). - MatchContent(fmt.Sprintf("{\n \"reference\": \"%s\",\n \"userMetadata\": {\n \"io.wabbit-networks.buildId\": \"123\"\n },\n \"result\": \"Success\"\n}\n", artifact.ReferenceWithDigest())) - - notation.ExpectFailure().Exec("verify", artifact.ReferenceWithDigest(), "--user-metadata", "io.wabbit-networks.buildId=321"). - MatchErrKeyWords("unable to find specified metadata in the signature") - }) - }) }) diff --git a/test/e2e/suite/scenario/quickstart.go b/test/e2e/suite/scenario/quickstart.go index d5c298f69..895d2696a 100644 --- a/test/e2e/suite/scenario/quickstart.go +++ b/test/e2e/suite/scenario/quickstart.go @@ -74,7 +74,7 @@ var _ = Describe("notation quickstart E2E test", Ordered, func() { It("Verify the container image with jws format", func() { notation.Exec("verify", artifact.ReferenceWithDigest()). - MatchContent(fmt.Sprintf("Successfully verified signature for %s\n", artifact.ReferenceWithDigest())) + MatchKeyWords(fmt.Sprintf("Successfully verified signature for %s\n", artifact.ReferenceWithDigest())) }) It("Verify the container image with cose format", func() { diff --git a/test/e2e/testdata/config/trustpolicies/invalid_format_trustpolicy.json b/test/e2e/testdata/config/trustpolicies/invalid_format_trustpolicy.json new file mode 100644 index 000000000..ad7b4845c --- /dev/null +++ b/test/e2e/testdata/config/trustpolicies/invalid_format_trustpolicy.json @@ -0,0 +1,9 @@ +{ + "version": "1.0", + "trustPolicies": [ + { + "name": "e2e", + "registryScopes": [ "*" ] + }} + ] +} \ No newline at end of file