From 86aeb0cc94248f6023c099b33fab533ac78e32a3 Mon Sep 17 00:00:00 2001 From: Yi Zha Date: Thu, 9 Feb 2023 21:37:58 +0800 Subject: [PATCH 1/5] doc: update sign.md for OCI image manifest support (#540) updates: - Update the flag name per community discussion - Update the description for using OCI image manifest Signed-off-by: Yi Zha --- specs/commandline/sign.md | 42 ++++++++++++++++----------------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/specs/commandline/sign.md b/specs/commandline/sign.md index 95be17a21..70aa13e44 100644 --- a/specs/commandline/sign.md +++ b/specs/commandline/sign.md @@ -28,25 +28,27 @@ Usage: notation sign [flags] 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. - -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 - --signature-format string signature envelope format, options: 'jws', 'cose' (default "jws") - --signature-manifest string manifest type for signatures. options: artifact, image (default "artifact") - -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 + -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. + -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 + --signature-format string signature envelope format, options: "jws", "cose" (default "jws") + --signature-manifest string manifest type for signature, options: "image", "artifact" (default "artifact") + -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 ``` ## Use OCI image manifest to store signatures -By default, Notation uses [OCI artifact manifest][oci-artifact-manifest] to store signatures in registries. For backward compatibility, Notation supports using `OCI image manifest` to store signatures in registries that partially implement the [OCI Image specification v1.1][oci-image-spec]. Use flag `--signature-manifest image` to force Notation to store the signatures using OCI image manifest. +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]. -Registries MAY not implement or enable the `Referrers API`, which is used by clients to fetch referrers. In the context of Notation, the referrers are signatures. Notation follows the fallback procedure defined in [OCI distribution spec][oci-backward-compatibility] if `Referrers API` is unavailable. +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`. + +- Response status `400 BAD Request` with error code `MANIFEST_INVALID` or `UNSUPPORTED` ### Set config property for OCI image manifest @@ -62,14 +64,6 @@ Notation uses empty JSON object `{}` as the default configuration content, and t } ``` -### When to use OCI image manifest - -[Registry support][registry-support] lists registries with different compatibilities. For registries not supporting `OCI artifact manifest`, users can use flag `--signature-manifest image` to sign artifacts stored in those registries. - -For registries not listed in the page, users can consider using flag `--signature-manifest image` by checking the error message. Note that there is no deterministic way to determine whether a registry supports `OCI artifact manifest` or not. The error message is just for reference. The following response status contained in error messages MAY indicate that the registry doesn't support `OCI artifact manifest`: - -- Response status `400 BAD Request` with error code `MANIFEST_INVALID` or `UNSUPPORTED` - ## Usage ### Sign an OCI artifact @@ -168,6 +162,4 @@ notation sign --signature-manifest image /@ [oci-artifact-manifest]: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/artifact.md [oci-image-spec]: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/spec.md -[oci-backward-compatibility]: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#backwards-compatibility -[registry-support]: https://notaryproject.dev/docs/registrysupport/ -[oras-land]: https://oras.land/ +[oci-referers-api]: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#listing-referrers From 33c228132389d0b0f7b8c13aa25bf124f0d0b485 Mon Sep 17 00:00:00 2001 From: byronchien Date: Thu, 9 Feb 2023 16:28:18 -0800 Subject: [PATCH 2/5] feat: add support for json output for `notation verify` (#527) allows json output for `notation verify`. Fixes https://github.com/notaryproject/roadmap/issues/67 and https://github.com/notaryproject/notation/pull/498 chienb@a07817b52895 notation % ./bin/notation verify $IMAGE --output json { "reference": "localhost:5000/net-monitor@sha256:5a07385af4e6b6af81b0ebfd435aedccdfa3507f0609c658209e1aba57159b2b", "userMetadata": { "foo": "bar" }, "result": "Success" } Signed-off-by: Byron Chien --- cmd/notation/verify.go | 52 ++++++++++++++++++++++++-------- cmd/notation/verify_test.go | 7 ++++- internal/cmd/flags.go | 19 ++++++++++-- internal/ioutil/print.go | 16 +++++++++- specs/commandline/verify.md | 25 ++++++++++++++- test/e2e/suite/command/verify.go | 41 +++++++++++++++++++++++++ 6 files changed, 142 insertions(+), 18 deletions(-) diff --git a/cmd/notation/verify.go b/cmd/notation/verify.go index 182bc1cca..11bcace99 100644 --- a/cmd/notation/verify.go +++ b/cmd/notation/verify.go @@ -26,6 +26,13 @@ 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 { @@ -52,14 +59,19 @@ Example - Verify a signature on an OCI artifact identified by a tag (Notation w opts.reference = args[0] return nil }, - RunE: func(cmd *cobra.Command, args []string) error { - return runVerify(cmd, opts) + 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) }, } 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 } @@ -132,13 +144,8 @@ 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) } } - 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 + + return printResult(opts.outputFormat, ref.String(), outcome) } func resolveReference(ctx context.Context, opts *SecureFlagOpts, reference string, sigRepo notationregistry.Repository, fn func(registry.Reference, ocispec.Descriptor)) (registry.Reference, error) { @@ -160,14 +167,33 @@ func resolveReference(ctx context.Context, opts *SecureFlagOpts, reference strin return ref, nil } -func printMetadataIfPresent(outcome *notation.VerificationOutcome) { +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 + } + } + // 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() - if len(metadata) > 0 { - fmt.Println("\nThe artifact was signed with the following user metadata.") - ioutil.PrintMetadataMap(os.Stdout, metadata) + 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 } } diff --git a/cmd/notation/verify_test.go b/cmd/notation/verify_test.go index 69d879f25..70b2b683e 100644 --- a/cmd/notation/verify_test.go +++ b/cmd/notation/verify_test.go @@ -3,6 +3,8 @@ package main import ( "reflect" "testing" + + "github.com/notaryproject/notation/internal/cmd" ) func TestVerifyCommand_BasicArgs(t *testing.T) { @@ -15,6 +17,7 @@ func TestVerifyCommand_BasicArgs(t *testing.T) { Password: "password", }, pluginConfig: []string{"key1=val1"}, + outputFormat: cmd.OutputPlaintext, } if err := command.ParseFlags([]string{ expected.reference, @@ -40,12 +43,14 @@ 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"}); err != nil { + "--plugin-config", "key2=val2", + "--output", "json"}); err != nil { t.Fatalf("Parse Flag failed: %v", err) } if err := command.Args(command, command.Flags().Args()); err != nil { diff --git a/internal/cmd/flags.go b/internal/cmd/flags.go index 45e086230..5df84003b 100644 --- a/internal/cmd/flags.go +++ b/internal/cmd/flags.go @@ -11,6 +11,11 @@ import ( "github.com/spf13/pflag" ) +const ( + OutputPlaintext = "text" + OutputJson = "json" +) + var ( PflagKey = &pflag.Flag{ Name: "key", @@ -75,11 +80,21 @@ var ( Name: "user-metadata", Shorthand: "m", } - PflagUserMetadataSignUsage = "{key}={value} pairs that are added to the signature payload" + + PflagUserMetadataSignUsage = "{key}={value} pairs that are added to the signature payload" PflagUserMetadataVerifyUsage = "user defined {key}={value} pairs that must be present in the signature for successful verification if provided" - SetPflagUserMetadata = func(fs *pflag.FlagSet, p *[]string, usage string) { + SetPflagUserMetadata = func(fs *pflag.FlagSet, p *[]string, usage string) { fs.StringArrayVarP(p, PflagUserMetadata.Name, PflagUserMetadata.Shorthand, nil, usage) } + + PflagOutput = &pflag.Flag{ + Name: "output", + Shorthand: "o", + } + 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) + } ) // KeyValueSlice is a flag with type int diff --git a/internal/ioutil/print.go b/internal/ioutil/print.go index f7ae97b4d..96e452212 100644 --- a/internal/ioutil/print.go +++ b/internal/ioutil/print.go @@ -1,6 +1,7 @@ package ioutil import ( + "encoding/json" "fmt" "io" "text/tabwriter" @@ -33,6 +34,7 @@ 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") @@ -42,4 +44,16 @@ func PrintMetadataMap(w io.Writer, metadata map[string]string) error { } return tw.Flush() -} \ No newline at end of file +} + +// PrintObjectAsJSON takes an interface and prints it as an indented JSON string +func PrintObjectAsJSON(i interface{}) error { + jsonBytes, err := json.MarshalIndent(i, "", " ") + if err != nil { + return err + } + + fmt.Println(string(jsonBytes)) + + return nil +} diff --git a/specs/commandline/verify.md b/specs/commandline/verify.md index 3741cb966..5659ce7c2 100644 --- a/specs/commandline/verify.md +++ b/specs/commandline/verify.md @@ -37,11 +37,12 @@ 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 - -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 + -u, --username string username for registry operations (default to $NOTATION_USERNAME if not specified) -v, --verbose verbose mode ``` @@ -168,3 +169,25 @@ 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/test/e2e/suite/command/verify.go b/test/e2e/suite/command/verify.go index 02636be0a..9837c1f64 100644 --- a/test/e2e/suite/command/verify.go +++ b/test/e2e/suite/command/verify.go @@ -49,4 +49,45 @@ 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") + }) + }) }) From c47a4522c490f4b1cdcadeb0abd9307b63952394 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Fri, 10 Feb 2023 08:59:04 +0800 Subject: [PATCH 3/5] chore: update sign command descriptions to align with the spec (#543) After discussion with @yizha1, aligning with the spec in this [PR](https://github.com/notaryproject/notation/pull/540). Signed-off-by: Patrick Zheng --- cmd/notation/sign.go | 2 +- internal/cmd/flags.go | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/cmd/notation/sign.go b/cmd/notation/sign.go index 5d8ec3f35..b8fbb15c6 100644 --- a/cmd/notation/sign.go +++ b/cmd/notation/sign.go @@ -84,7 +84,7 @@ Example - Sign an OCI artifact and use OCI image manifest to store the signature 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 signatures. options: artifact, image") + command.Flags().StringVar(&opts.signatureManifest, "signature-manifest", signatureManifestArtifact, "manifest type for signature. options: \"artifact\", \"image\"") cmd.SetPflagUserMetadata(command.Flags(), &opts.userMetadata, cmd.PflagUserMetadataSignUsage) return command } diff --git a/internal/cmd/flags.go b/internal/cmd/flags.go index 5df84003b..f7a62a1b0 100644 --- a/internal/cmd/flags.go +++ b/internal/cmd/flags.go @@ -28,7 +28,7 @@ var ( PflagSignatureFormat = &pflag.Flag{ Name: "signature-format", - Usage: "signature envelope format, options: 'jws', 'cose'", + Usage: "signature envelope format, options: \"jws\", \"cose\"", } SetPflagSignatureFormat = func(fs *pflag.FlagSet, p *string) { defaultSignatureFormat := envelope.JWS @@ -80,7 +80,6 @@ var ( Name: "user-metadata", Shorthand: "m", } - PflagUserMetadataSignUsage = "{key}={value} pairs that are added to the signature payload" PflagUserMetadataVerifyUsage = "user defined {key}={value} pairs that must be present in the signature for successful verification if provided" SetPflagUserMetadata = func(fs *pflag.FlagSet, p *[]string, usage string) { From 54b42cb281ddde60af905b34aadf9c1cd7457f28 Mon Sep 17 00:00:00 2001 From: Pritesh Bandi Date: Wed, 15 Feb 2023 01:00:07 -0800 Subject: [PATCH 4/5] Revert "feat: add support for json output for `notation verify` (#527)" (#551) This reverts commit 33c228132389d0b0f7b8c13aa25bf124f0d0b485. We are reverting #527 because we need to write spec first for json output. Signed-off-by: Pritesh Bandi --- cmd/notation/verify.go | 52 ++++++++------------------------ cmd/notation/verify_test.go | 7 +---- internal/cmd/flags.go | 15 +-------- internal/ioutil/print.go | 16 +--------- specs/commandline/verify.md | 25 +-------------- test/e2e/suite/command/verify.go | 41 ------------------------- 6 files changed, 17 insertions(+), 139 deletions(-) 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/internal/cmd/flags.go b/internal/cmd/flags.go index f7a62a1b0..c03a9d07d 100644 --- a/internal/cmd/flags.go +++ b/internal/cmd/flags.go @@ -11,11 +11,6 @@ import ( "github.com/spf13/pflag" ) -const ( - OutputPlaintext = "text" - OutputJson = "json" -) - var ( PflagKey = &pflag.Flag{ Name: "key", @@ -80,20 +75,12 @@ var ( Name: "user-metadata", Shorthand: "m", } + PflagUserMetadataSignUsage = "{key}={value} pairs that are added to the signature payload" PflagUserMetadataVerifyUsage = "user defined {key}={value} pairs that must be present in the signature for successful verification if provided" SetPflagUserMetadata = func(fs *pflag.FlagSet, p *[]string, usage string) { fs.StringArrayVarP(p, PflagUserMetadata.Name, PflagUserMetadata.Shorthand, nil, usage) } - - PflagOutput = &pflag.Flag{ - Name: "output", - Shorthand: "o", - } - 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) - } ) // KeyValueSlice is a flag with type int diff --git a/internal/ioutil/print.go b/internal/ioutil/print.go index 96e452212..f7ae97b4d 100644 --- a/internal/ioutil/print.go +++ b/internal/ioutil/print.go @@ -1,7 +1,6 @@ package ioutil import ( - "encoding/json" "fmt" "io" "text/tabwriter" @@ -34,7 +33,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") @@ -44,16 +42,4 @@ func PrintMetadataMap(w io.Writer, metadata map[string]string) error { } return tw.Flush() -} - -// PrintObjectAsJSON takes an interface and prints it as an indented JSON string -func PrintObjectAsJSON(i interface{}) error { - jsonBytes, err := json.MarshalIndent(i, "", " ") - if err != nil { - return err - } - - fmt.Println(string(jsonBytes)) - - return nil -} +} \ No newline at end of file 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/test/e2e/suite/command/verify.go b/test/e2e/suite/command/verify.go index 9837c1f64..02636be0a 100644 --- a/test/e2e/suite/command/verify.go +++ b/test/e2e/suite/command/verify.go @@ -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") - }) - }) }) From 00896e46481ca1d8e4273acff10431a41e1b9316 Mon Sep 17 00:00:00 2001 From: Byron Chien Date: Wed, 15 Feb 2023 15:53:46 -0800 Subject: [PATCH 5/5] Add support for notation inspect Signed-off-by: Byron Chien --- cmd/notation/inspect.go | 302 ++++++++++++++++++++++++++ cmd/notation/inspect_test.go | 71 ++++++ cmd/notation/main.go | 1 + internal/cmd/flags.go | 15 +- internal/envelope/envelope.go | 45 +++- internal/ioutil/print.go | 15 +- internal/tree/tree.go | 57 +++++ internal/tree/tree_test.go | 83 +++++++ test/e2e/suite/scenario/quickstart.go | 2 +- 9 files changed, 587 insertions(+), 4 deletions(-) create mode 100644 cmd/notation/inspect.go create mode 100644 cmd/notation/inspect_test.go create mode 100644 internal/tree/tree.go create mode 100644 internal/tree/tree_test.go 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/main.go b/cmd/notation/main.go index 5f0f3df39..32eb1bc2b 100644 --- a/cmd/notation/main.go +++ b/cmd/notation/main.go @@ -23,6 +23,7 @@ func main() { loginCommand(nil), logoutCommand(nil), versionCommand(), + inspectCommand(nil), ) if err := cmd.Execute(); err != nil { os.Exit(1) diff --git a/internal/cmd/flags.go b/internal/cmd/flags.go index c03a9d07d..2bf0c6565 100644 --- a/internal/cmd/flags.go +++ b/internal/cmd/flags.go @@ -11,6 +11,11 @@ import ( "github.com/spf13/pflag" ) +const ( + OutputPlaintext = "text" + OutputJSON = "json" +) + var ( PflagKey = &pflag.Flag{ Name: "key", @@ -75,12 +80,20 @@ var ( Name: "user-metadata", Shorthand: "m", } - PflagUserMetadataSignUsage = "{key}={value} pairs that are added to the signature payload" PflagUserMetadataVerifyUsage = "user defined {key}={value} pairs that must be present in the signature for successful verification if provided" SetPflagUserMetadata = func(fs *pflag.FlagSet, p *[]string, usage string) { fs.StringArrayVarP(p, PflagUserMetadata.Name, PflagUserMetadata.Shorthand, nil, usage) } + + PflagOutput = &pflag.Flag{ + Name: "output", + Shorthand: "o", + } + 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) + } ) // KeyValueSlice is a flag with type int 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 f7ae97b4d..25075ca83 100644 --- a/internal/ioutil/print.go +++ b/internal/ioutil/print.go @@ -1,6 +1,7 @@ package ioutil import ( + "encoding/json" "fmt" "io" "text/tabwriter" @@ -42,4 +43,16 @@ func PrintMetadataMap(w io.Writer, metadata map[string]string) error { } return tw.Flush() -} \ No newline at end of file +} + +// PrintObjectAsJSON takes an interface and prints it as an indented JSON string +func PrintObjectAsJSON(i interface{}) error { + jsonBytes, err := json.MarshalIndent(i, "", " ") + if err != nil { + return err + } + + fmt.Println(string(jsonBytes)) + + return nil +} 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/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() {