Skip to content

Commit e3c2931

Browse files
JeyJeyGaoFeynmanZhou
authored andcommitted
refactor: verify display handler (notaryproject#1167)
Refactor: - move the verify command output related logic to be `VerifyHandler` Example: ```sh notation verify notationreg.azurecr.io/hello-world:v1 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 notationreg.azurecr.io/hello-world@sha256:d37ada95d47ad12224c205a938129df7a3e52345828b4fa27b03a98825d1e2e7 The artifact was signed with the following user metadata. KEY VALUE foo bar ``` Resolve part of notaryproject#1151 --------- Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
1 parent 6ed786e commit e3c2931

7 files changed

Lines changed: 212 additions & 50 deletions

File tree

cmd/notation/internal/display/handler.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424

2525
"github.com/notaryproject/notation/cmd/notation/internal/display/metadata"
2626
"github.com/notaryproject/notation/cmd/notation/internal/display/metadata/json"
27+
"github.com/notaryproject/notation/cmd/notation/internal/display/metadata/text"
2728
"github.com/notaryproject/notation/cmd/notation/internal/display/metadata/tree"
2829
"github.com/notaryproject/notation/cmd/notation/internal/display/output"
2930
"github.com/notaryproject/notation/cmd/notation/internal/option"
@@ -40,3 +41,9 @@ func NewInpsectHandler(printer *output.Printer, format option.Format) (metadata.
4041
}
4142
return nil, fmt.Errorf("unrecognized output format %s", format.CurrentType)
4243
}
44+
45+
// NewVerifyHandler creates a new metadata VerifyHandler for printing
46+
// veriifcation result and warnings.
47+
func NewVerifyHandler(printer *output.Printer) metadata.VerifyHandler {
48+
return text.NewVerifyHandler(printer)
49+
}

cmd/notation/internal/display/metadata/interface.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package metadata
1818

1919
import (
2020
"github.com/notaryproject/notation-core-go/signature"
21+
"github.com/notaryproject/notation-go"
2122
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
2223
)
2324

@@ -37,3 +38,19 @@ type InspectHandler interface {
3738
// InspectSignature inspects a signature to get it ready to be rendered.
3839
InspectSignature(manifestDesc ocispec.Descriptor, envelope signature.Envelope) error
3940
}
41+
42+
// VerifyHandler is a handler for rendering metadata information of
43+
// verification outcome.
44+
//
45+
// It only supports text format for now.
46+
type VerifyHandler interface {
47+
Renderer
48+
49+
// OnResolvingTagReference outputs the tag reference warning.
50+
OnResolvingTagReference(reference string)
51+
52+
// OnVerifySucceeded sets the successful verification result for the handler.
53+
//
54+
// outcomes must not be nil or empty.
55+
OnVerifySucceeded(outcomes []*notation.VerificationOutcome, digestReference string)
56+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Copyright The Notary Project Authors.
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
// Package text provides the text output in human-readable format for metadata
15+
// information.
16+
package text
17+
18+
import (
19+
"fmt"
20+
"reflect"
21+
"text/tabwriter"
22+
23+
"github.com/notaryproject/notation-go"
24+
"github.com/notaryproject/notation-go/verifier/trustpolicy"
25+
"github.com/notaryproject/notation/cmd/notation/internal/display/output"
26+
)
27+
28+
// VerifyHandler is a handler for rendering output for verify command in
29+
// human-readable format.
30+
type VerifyHandler struct {
31+
printer *output.Printer
32+
33+
outcome *notation.VerificationOutcome
34+
digestReference string
35+
hasWarning bool
36+
}
37+
38+
// NewVerifyHandler creates a VerifyHandler to render verification results in
39+
// human-readable format.
40+
func NewVerifyHandler(printer *output.Printer) *VerifyHandler {
41+
return &VerifyHandler{
42+
printer: printer,
43+
}
44+
}
45+
46+
// OnResolvingTagReference outputs the tag reference warning.
47+
func (h *VerifyHandler) OnResolvingTagReference(reference string) {
48+
h.printer.PrintErrorf("Warning: Always verify 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", reference)
49+
h.hasWarning = true
50+
}
51+
52+
// OnVerifySucceeded sets the successful verification result for the handler.
53+
//
54+
// outcomes must not be nil or empty.
55+
func (h *VerifyHandler) OnVerifySucceeded(outcomes []*notation.VerificationOutcome, digestReference string) {
56+
h.outcome = outcomes[0]
57+
h.digestReference = digestReference
58+
}
59+
60+
// Render prints out the verification results in human-readable format.
61+
func (h *VerifyHandler) Render() error {
62+
// write out on success
63+
// print out warning for any failed result with logged verification action
64+
for _, result := range h.outcome.VerificationResults {
65+
if result.Error != nil {
66+
// at this point, the verification action has to be logged and
67+
// it's failed
68+
h.printer.PrintErrorf("Warning: %v was set to %q and failed with error: %v\n", result.Type, result.Action, result.Error)
69+
h.hasWarning = true
70+
}
71+
}
72+
if h.hasWarning {
73+
// print a newline to separate the warning from the final message
74+
h.printer.Println()
75+
}
76+
if reflect.DeepEqual(h.outcome.VerificationLevel, trustpolicy.LevelSkip) {
77+
h.printer.Println("Trust policy is configured to skip signature verification for", h.digestReference)
78+
} else {
79+
h.printer.Println("Successfully verified signature for", h.digestReference)
80+
h.printMetadataIfPresent(h.outcome)
81+
}
82+
return nil
83+
}
84+
85+
func (h *VerifyHandler) printMetadataIfPresent(outcome *notation.VerificationOutcome) {
86+
// the signature envelope is parsed as part of verification.
87+
// since user metadata is only printed on successful verification,
88+
// this error can be ignored
89+
metadata, _ := outcome.UserMetadata()
90+
91+
if len(metadata) > 0 {
92+
h.printer.Println("\nThe artifact was signed with the following user metadata.")
93+
h.printMetadataMap(metadata)
94+
}
95+
}
96+
97+
// printMetadataMap prints out metadata given the metatdata map
98+
//
99+
// The metadata is additional information of text output.
100+
func (h *VerifyHandler) printMetadataMap(metadata map[string]string) error {
101+
tw := tabwriter.NewWriter(h.printer, 0, 0, 3, ' ', 0)
102+
fmt.Fprintln(tw, "\nKEY\tVALUE\t")
103+
104+
for k, v := range metadata {
105+
fmt.Fprintf(tw, "%v\t%v\t\n", k, v)
106+
}
107+
108+
return tw.Flush()
109+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright The Notary Project Authors.
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package text
15+
16+
import (
17+
"bytes"
18+
"encoding/json"
19+
"testing"
20+
21+
"github.com/notaryproject/notation-core-go/signature"
22+
"github.com/notaryproject/notation-go"
23+
"github.com/notaryproject/notation/cmd/notation/internal/display/output"
24+
"github.com/notaryproject/notation/internal/envelope"
25+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
26+
)
27+
28+
func TestPrintMetadataIfPresent(t *testing.T) {
29+
payload := &envelope.Payload{
30+
TargetArtifact: ocispec.Descriptor{
31+
Annotations: map[string]string{
32+
"foo": "bar",
33+
},
34+
},
35+
}
36+
payloadBytes, _ := json.Marshal(payload)
37+
38+
outcome := &notation.VerificationOutcome{
39+
EnvelopeContent: &signature.EnvelopeContent{
40+
Payload: signature.Payload{
41+
Content: payloadBytes,
42+
},
43+
},
44+
}
45+
46+
t.Run("with metadata", func(t *testing.T) {
47+
buf := bytes.Buffer{}
48+
printer := output.NewPrinter(&buf, &buf)
49+
h := NewVerifyHandler(printer)
50+
h.printMetadataIfPresent(outcome)
51+
got := buf.String()
52+
expected := "\nThe artifact was signed with the following user metadata.\n\nKEY VALUE \nfoo bar \n"
53+
if got != expected {
54+
t.Errorf("unexpected output: %q", got)
55+
}
56+
})
57+
}

cmd/notation/internal/display/output/print.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func (p *Printer) Println(a ...any) error {
7171
return nil
7272
}
7373

74-
// Printf prints objects concurrent-safely with newline.
74+
// Printf prints objects concurrent-safely.
7575
func (p *Printer) Printf(format string, a ...any) error {
7676
p.lock.Lock()
7777
defer p.lock.Unlock()
@@ -85,6 +85,15 @@ func (p *Printer) Printf(format string, a ...any) error {
8585
return nil
8686
}
8787

88+
// PrintErrorf prints objects to error output concurrent-safely.
89+
func (p *Printer) PrintErrorf(format string, a ...any) error {
90+
p.lock.Lock()
91+
defer p.lock.Unlock()
92+
93+
_, err := fmt.Fprintf(p.err, format, a...)
94+
return err
95+
}
96+
8897
// PrintPrettyJSON prints object to out in JSON format.
8998
func PrintPrettyJSON(out io.Writer, object any) error {
9099
encoder := json.NewEncoder(out)

cmd/notation/verify.go

Lines changed: 12 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import (
1919
"fmt"
2020
"io/fs"
2121
"os"
22-
"reflect"
2322

2423
"github.com/notaryproject/notation-core-go/revocation/purpose"
2524
"github.com/notaryproject/notation-go"
@@ -28,9 +27,11 @@ import (
2827
"github.com/notaryproject/notation-go/verifier"
2928
"github.com/notaryproject/notation-go/verifier/trustpolicy"
3029
"github.com/notaryproject/notation-go/verifier/truststore"
30+
"github.com/notaryproject/notation/cmd/notation/internal/display"
3131
"github.com/notaryproject/notation/cmd/notation/internal/experimental"
32+
"github.com/notaryproject/notation/cmd/notation/internal/option"
3233
"github.com/notaryproject/notation/internal/cmd"
33-
"github.com/notaryproject/notation/internal/ioutil"
34+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
3435
"github.com/spf13/cobra"
3536

3637
clirev "github.com/notaryproject/notation/internal/revocation"
@@ -39,6 +40,7 @@ import (
3940
type verifyOpts struct {
4041
cmd.LoggingFlagOpts
4142
SecureFlagOpts
43+
option.Common
4244
reference string
4345
pluginConfig []string
4446
userMetadata []string
@@ -87,6 +89,7 @@ Example - [Experimental] Verify a signature on an OCI artifact identified by a t
8789
if opts.ociLayout {
8890
opts.inputType = inputTypeOCILayout
8991
}
92+
opts.Common.Parse(cmd)
9093
return experimental.CheckFlagsAndWarn(cmd, "allow-referrers-api", "oci-layout", "scope")
9194
},
9295
RunE: func(cmd *cobra.Command, args []string) error {
@@ -116,6 +119,8 @@ func runVerify(command *cobra.Command, opts *verifyOpts) error {
116119
// set log level
117120
ctx := opts.LoggingFlagOpts.InitializeLogger(command.Context())
118121

122+
displayHandler := display.NewVerifyHandler(opts.Printer)
123+
119124
// initialize
120125
sigVerifier, err := getVerifier(ctx)
121126
if err != nil {
@@ -142,8 +147,9 @@ func runVerify(command *cobra.Command, opts *verifyOpts) error {
142147
if err != nil {
143148
return err
144149
}
145-
// resolve the given reference and set the digest
146-
_, resolvedRef, err := resolveReferenceWithWarning(ctx, opts.inputType, reference, sigRepo, "verify")
150+
_, resolvedRef, err := resolveReference(ctx, opts.inputType, reference, sigRepo, func(ref string, manifestDesc ocispec.Descriptor) {
151+
displayHandler.OnResolvingTagReference(ref)
152+
})
147153
if err != nil {
148154
return err
149155
}
@@ -159,8 +165,8 @@ func runVerify(command *cobra.Command, opts *verifyOpts) error {
159165
if err != nil {
160166
return err
161167
}
162-
reportVerificationSuccess(outcomes, resolvedRef)
163-
return nil
168+
displayHandler.OnVerifySucceeded(outcomes, resolvedRef)
169+
return displayHandler.Render()
164170
}
165171

166172
func checkVerificationFailure(outcomes []*notation.VerificationOutcome, printOut string, err error) error {
@@ -195,37 +201,6 @@ func checkVerificationFailure(outcomes []*notation.VerificationOutcome, printOut
195201
return nil
196202
}
197203

198-
func reportVerificationSuccess(outcomes []*notation.VerificationOutcome, printout string) {
199-
// write out on success
200-
outcome := outcomes[0]
201-
// print out warning for any failed result with logged verification action
202-
for _, result := range outcome.VerificationResults {
203-
if result.Error != nil {
204-
// at this point, the verification action has to be logged and
205-
// it's failed
206-
fmt.Fprintf(os.Stderr, "Warning: %v was set to %q and failed with error: %v\n", result.Type, result.Action, result.Error)
207-
}
208-
}
209-
if reflect.DeepEqual(outcome.VerificationLevel, trustpolicy.LevelSkip) {
210-
fmt.Println("Trust policy is configured to skip signature verification for", printout)
211-
} else {
212-
fmt.Println("Successfully verified signature for", printout)
213-
printMetadataIfPresent(outcome)
214-
}
215-
}
216-
217-
func printMetadataIfPresent(outcome *notation.VerificationOutcome) {
218-
// the signature envelope is parsed as part of verification.
219-
// since user metadata is only printed on successful verification,
220-
// this error can be ignored
221-
metadata, _ := outcome.UserMetadata()
222-
223-
if len(metadata) > 0 {
224-
fmt.Println("\nThe artifact was signed with the following user metadata.")
225-
ioutil.PrintMetadataMap(os.Stdout, metadata)
226-
}
227-
}
228-
229204
func getVerifier(ctx context.Context) (notation.Verifier, error) {
230205
// revocation check
231206
revocationCodeSigningValidator, err := clirev.NewRevocationValidator(ctx, purpose.CodeSigning)

internal/ioutil/print.go

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,18 +48,6 @@ func PrintKeyMap(w io.Writer, target *string, v []config.KeySuite) error {
4848
return tw.Flush()
4949
}
5050

51-
// PrintMetadataMap prints out metadata given the metatdata map
52-
func PrintMetadataMap(w io.Writer, metadata map[string]string) error {
53-
tw := newTabWriter(w)
54-
fmt.Fprintln(tw, "\nKEY\tVALUE\t")
55-
56-
for k, v := range metadata {
57-
fmt.Fprintf(tw, "%v\t%v\t\n", k, v)
58-
}
59-
60-
return tw.Flush()
61-
}
62-
6351
// PrintCertMap lists certificate files in the trust store given array of cert
6452
// paths
6553
func PrintCertMap(w io.Writer, certPaths []string) error {

0 commit comments

Comments
 (0)