Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 39 additions & 13 deletions cmd/notation/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}

Expand Down Expand Up @@ -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) {
Expand All @@ -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
}
}
7 changes: 6 additions & 1 deletion cmd/notation/verify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package main
import (
"reflect"
"testing"

"github.com/notaryproject/notation/internal/cmd"
)

func TestVerifyCommand_BasicArgs(t *testing.T) {
Expand All @@ -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,
Expand All @@ -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 {
Expand Down
19 changes: 17 additions & 2 deletions internal/cmd/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import (
"github.com/spf13/pflag"
)

const (
OutputPlaintext = "text"
OutputJson = "json"
)

var (
PflagKey = &pflag.Flag{
Name: "key",
Expand Down Expand Up @@ -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
Expand Down
16 changes: 15 additions & 1 deletion internal/ioutil/print.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ioutil

import (
"encoding/json"
"fmt"
"io"
"text/tabwriter"
Expand Down Expand Up @@ -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")
Expand All @@ -42,4 +44,16 @@ 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
}
25 changes: 24 additions & 1 deletion specs/commandline/verify.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down Expand Up @@ -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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the verification fails?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for verification failure, no json is written to stdout, and the failure is logged to stderr.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@byronchien If JSON is printed only if the verification passes, what's the meaning of showing "result": "Success"?

/cc @priteshbandi @yizha1

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When verification fails, I expect that the result would be failure or failed, and the failure reason will be included in the JSON object so that other scripts can parse it correctly.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this PR is merged, I've created #546 to track.

Copy link
Contributor

@priteshbandi priteshbandi Feb 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

result can have two values, one is skip and other one is success. IMO for failure case displaying JSON for wont be useful because it wont contain any useful information which automation/script can use(non-zero exit code should suffice to show failure)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSON output is usually consumed by scripts or programs. How can them obtain the structured error message and detailed verification outcomes?

Copy link
Contributor

@priteshbandi priteshbandi Feb 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To display error message/detailed verification output we need more than result field something like resultReason because result will only say failure. Also, in current state it wont be of much help since as there is only one genuine/expected failure use case i.e failed signature verification for all the signatures. Apart from genuine/expected error, all other errors should always be emitted as stdErr.

Are you suggest we should emit json for expected failure use cases or all failure use cases?


Moved conversation to #546 (comment)

}
```

On unsuccessful verification, nothing is written to `stdout`, and the failure is logged to `stderr`.
41 changes: 41 additions & 0 deletions test/e2e/suite/command/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})
})
})