Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: verify display handler #1167

Merged
merged 39 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
89adf15
refactor: add inpect display handler
JeyJeyGao Jan 16, 2025
f0756a8
fix: update code
JeyJeyGao Jan 16, 2025
578dea2
test: add unit test
JeyJeyGao Jan 17, 2025
24dea8b
test: add e2e test for tree and json
JeyJeyGao Jan 17, 2025
4031faf
refactor code
JeyJeyGao Jan 17, 2025
37b967a
fix: update E2E
JeyJeyGao Jan 20, 2025
c2697db
fix: resolve comments
JeyJeyGao Jan 20, 2025
edbfbad
fix: update license header
JeyJeyGao Jan 20, 2025
eb078e3
fix: update test
JeyJeyGao Jan 20, 2025
4d3aedd
test: add unit test
JeyJeyGao Jan 20, 2025
8fdc989
test: add unit test for inspect
JeyJeyGao Jan 20, 2025
0640165
test: update test
JeyJeyGao Jan 20, 2025
a1e411f
fix: resolve comemnts
JeyJeyGao Jan 21, 2025
84a1991
fix: test
JeyJeyGao Jan 21, 2025
8083c25
fix: remove envelope media type
JeyJeyGao Jan 21, 2025
5dd55ca
fix: e2e test
JeyJeyGao Jan 21, 2025
25bc6a0
fix: e2e test
JeyJeyGao Jan 21, 2025
fd74cf7
fix: update json output format
JeyJeyGao Jan 21, 2025
f1c2966
fix: update e2e
JeyJeyGao Jan 21, 2025
89b310f
fix: add content type in json output
JeyJeyGao Jan 21, 2025
58603ce
test: add e2e test for invalid timestamp signature
JeyJeyGao Jan 21, 2025
b3a97e5
fix: update e2e test
JeyJeyGao Jan 21, 2025
b108fcd
fix: unit test
JeyJeyGao Jan 21, 2025
721518a
fix: resolve comments
JeyJeyGao Jan 22, 2025
0997875
fix: resolve comments
JeyJeyGao Jan 23, 2025
aef5735
refactor: display package structure
JeyJeyGao Jan 23, 2025
5b28eda
fix: update to use *ocispec.Descriptor
JeyJeyGao Jan 23, 2025
8f5d88f
fix: restore package structure
JeyJeyGao Jan 23, 2025
1adc392
fix: restore "NewInpsectHandler" function name
JeyJeyGao Jan 23, 2025
da16190
fix: move internal/tree to tree handlers package
JeyJeyGao Jan 23, 2025
7cf59d4
fix: resolve comments
JeyJeyGao Feb 6, 2025
60035a7
fix: udpate new to newNode
JeyJeyGao Feb 7, 2025
f30481e
refactor: verify display handler
JeyJeyGao Feb 7, 2025
86260ae
test: add unit test for metadata output
JeyJeyGao Feb 8, 2025
7b4961f
Merge remote-tracking branch 'upstream/main' into refactor/verify_out…
JeyJeyGao Feb 8, 2025
a029bd2
fix: update code
JeyJeyGao Feb 8, 2025
1f6e1c0
fix: resolve comment
JeyJeyGao Feb 8, 2025
e33dc04
fix: resolve comments
JeyJeyGao Feb 10, 2025
0485b7f
fix: add an empty line between warning and final message
JeyJeyGao Feb 11, 2025
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
7 changes: 7 additions & 0 deletions cmd/notation/internal/display/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (

"github.com/notaryproject/notation/cmd/notation/internal/display/metadata"
"github.com/notaryproject/notation/cmd/notation/internal/display/metadata/json"
"github.com/notaryproject/notation/cmd/notation/internal/display/metadata/text"
"github.com/notaryproject/notation/cmd/notation/internal/display/metadata/tree"
"github.com/notaryproject/notation/cmd/notation/internal/display/output"
"github.com/notaryproject/notation/cmd/notation/internal/option"
Expand All @@ -40,3 +41,9 @@ func NewInpsectHandler(printer *output.Printer, format option.Format) (metadata.
}
return nil, fmt.Errorf("unrecognized output format %s", format.CurrentType)
}

// NewVerifyHandler creates a new metadata VerifyHandler for printing
// veriifcation result and warnings.
func NewVerifyHandler(printer *output.Printer) metadata.VerifyHandler {
return text.NewVerifyHandler(printer)
}
17 changes: 17 additions & 0 deletions cmd/notation/internal/display/metadata/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package metadata

import (
"github.com/notaryproject/notation-core-go/signature"
"github.com/notaryproject/notation-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)

Expand All @@ -37,3 +38,19 @@ type InspectHandler interface {
// InspectSignature inspects a signature to get it ready to be rendered.
InspectSignature(manifestDesc ocispec.Descriptor, envelope signature.Envelope) error
}

// VerifyHandler is a handler for rendering metadata information of
// verification outcome.
//
// It only supports text format for now.
type VerifyHandler interface {
Renderer

// OnResolvingTagReference outputs the tag reference warning.
OnResolvingTagReference(reference string)

// OnVerifySucceeded sets the successful verification result for the handler.
//
// outcomes must not be nil or empty.
OnVerifySucceeded(outcomes []*notation.VerificationOutcome, digestReference string)
JeyJeyGao marked this conversation as resolved.
Show resolved Hide resolved
}
109 changes: 109 additions & 0 deletions cmd/notation/internal/display/metadata/text/verify.go
JeyJeyGao marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package text provides the text output in human-readable format for metadata
// information.
package text
JeyJeyGao marked this conversation as resolved.
Show resolved Hide resolved

import (
"fmt"
"reflect"
"text/tabwriter"

"github.com/notaryproject/notation-go"
"github.com/notaryproject/notation-go/verifier/trustpolicy"
"github.com/notaryproject/notation/cmd/notation/internal/display/output"
)

// VerifyHandler is a handler for rendering output for verify command in
// human-readable format.
type VerifyHandler struct {
printer *output.Printer

outcome *notation.VerificationOutcome
digestReference string
hasWarning bool
}

// NewVerifyHandler creates a VerifyHandler to render verification results in
// human-readable format.
func NewVerifyHandler(printer *output.Printer) *VerifyHandler {
return &VerifyHandler{
printer: printer,
}
}

// OnResolvingTagReference outputs the tag reference warning.
func (h *VerifyHandler) OnResolvingTagReference(reference string) {
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)
h.hasWarning = true
}

// OnVerifySucceeded sets the successful verification result for the handler.
//
// outcomes must not be nil or empty.
func (h *VerifyHandler) OnVerifySucceeded(outcomes []*notation.VerificationOutcome, digestReference string) {
h.outcome = outcomes[0]
h.digestReference = digestReference
}

// Render prints out the verification results in human-readable format.
func (h *VerifyHandler) Render() error {
// write out on success
// print out warning for any failed result with logged verification action
for _, result := range h.outcome.VerificationResults {
if result.Error != nil {
// at this point, the verification action has to be logged and
// it's failed
h.printer.PrintErrorf("Warning: %v was set to %q and failed with error: %v\n", result.Type, result.Action, result.Error)
h.hasWarning = true
}
}
if h.hasWarning {
// print a newline to separate the warning from the final message
h.printer.Println()
}
if reflect.DeepEqual(h.outcome.VerificationLevel, trustpolicy.LevelSkip) {
h.printer.Println("Trust policy is configured to skip signature verification for", h.digestReference)
} else {
h.printer.Println("Successfully verified signature for", h.digestReference)
h.printMetadataIfPresent(h.outcome)
}
return nil
}

func (h *VerifyHandler) 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()

if len(metadata) > 0 {
h.printer.Println("\nThe artifact was signed with the following user metadata.")
h.printMetadataMap(metadata)
}
}

// printMetadataMap prints out metadata given the metatdata map
//
// The metadata is additional information of text output.
func (h *VerifyHandler) printMetadataMap(metadata map[string]string) error {
tw := tabwriter.NewWriter(h.printer, 0, 0, 3, ' ', 0)
fmt.Fprintln(tw, "\nKEY\tVALUE\t")

for k, v := range metadata {
fmt.Fprintf(tw, "%v\t%v\t\n", k, v)
}

return tw.Flush()
}
57 changes: 57 additions & 0 deletions cmd/notation/internal/display/metadata/text/verify_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package text

import (
"bytes"
"encoding/json"
"testing"

"github.com/notaryproject/notation-core-go/signature"
"github.com/notaryproject/notation-go"
"github.com/notaryproject/notation/cmd/notation/internal/display/output"
"github.com/notaryproject/notation/internal/envelope"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)

func TestPrintMetadataIfPresent(t *testing.T) {
payload := &envelope.Payload{
TargetArtifact: ocispec.Descriptor{
Annotations: map[string]string{
"foo": "bar",
},
},
}
payloadBytes, _ := json.Marshal(payload)

outcome := &notation.VerificationOutcome{
EnvelopeContent: &signature.EnvelopeContent{
Payload: signature.Payload{
Content: payloadBytes,
},
},
}

t.Run("with metadata", func(t *testing.T) {
buf := bytes.Buffer{}
printer := output.NewPrinter(&buf, &buf)
h := NewVerifyHandler(printer)
h.printMetadataIfPresent(outcome)
got := buf.String()
expected := "\nThe artifact was signed with the following user metadata.\n\nKEY VALUE \nfoo bar \n"
if got != expected {
t.Errorf("unexpected output: %q", got)
}
})
}
11 changes: 10 additions & 1 deletion cmd/notation/internal/display/output/print.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func (p *Printer) Println(a ...any) error {
return nil
}

// Printf prints objects concurrent-safely with newline.
// Printf prints objects concurrent-safely.
func (p *Printer) Printf(format string, a ...any) error {
p.lock.Lock()
defer p.lock.Unlock()
Expand All @@ -85,6 +85,15 @@ func (p *Printer) Printf(format string, a ...any) error {
return nil
}

// PrintErrorf prints objects to error output concurrent-safely.
func (p *Printer) PrintErrorf(format string, a ...any) error {
p.lock.Lock()
defer p.lock.Unlock()

_, err := fmt.Fprintf(p.err, format, a...)
return err
}

// PrintPrettyJSON prints object to out in JSON format.
func PrintPrettyJSON(out io.Writer, object any) error {
encoder := json.NewEncoder(out)
Expand Down
49 changes: 12 additions & 37 deletions cmd/notation/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
"fmt"
"io/fs"
"os"
"reflect"

"github.com/notaryproject/notation-core-go/revocation/purpose"
"github.com/notaryproject/notation-go"
Expand All @@ -28,9 +27,11 @@ import (
"github.com/notaryproject/notation-go/verifier"
"github.com/notaryproject/notation-go/verifier/trustpolicy"
"github.com/notaryproject/notation-go/verifier/truststore"
"github.com/notaryproject/notation/cmd/notation/internal/display"
"github.com/notaryproject/notation/cmd/notation/internal/experimental"
"github.com/notaryproject/notation/cmd/notation/internal/option"
"github.com/notaryproject/notation/internal/cmd"
"github.com/notaryproject/notation/internal/ioutil"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/spf13/cobra"

clirev "github.com/notaryproject/notation/internal/revocation"
Expand All @@ -39,6 +40,7 @@ import (
type verifyOpts struct {
cmd.LoggingFlagOpts
SecureFlagOpts
option.Common
reference string
pluginConfig []string
userMetadata []string
Expand Down Expand Up @@ -87,6 +89,7 @@ Example - [Experimental] Verify a signature on an OCI artifact identified by a t
if opts.ociLayout {
opts.inputType = inputTypeOCILayout
}
opts.Common.Parse(cmd)
return experimental.CheckFlagsAndWarn(cmd, "allow-referrers-api", "oci-layout", "scope")
},
RunE: func(cmd *cobra.Command, args []string) error {
Expand Down Expand Up @@ -116,6 +119,8 @@ func runVerify(command *cobra.Command, opts *verifyOpts) error {
// set log level
ctx := opts.LoggingFlagOpts.InitializeLogger(command.Context())

displayHandler := display.NewVerifyHandler(opts.Printer)

// initialize
sigVerifier, err := getVerifier(ctx)
if err != nil {
Expand All @@ -142,8 +147,9 @@ func runVerify(command *cobra.Command, opts *verifyOpts) error {
if err != nil {
return err
}
// resolve the given reference and set the digest
_, resolvedRef, err := resolveReferenceWithWarning(ctx, opts.inputType, reference, sigRepo, "verify")
_, resolvedRef, err := resolveReference(ctx, opts.inputType, reference, sigRepo, func(ref string, manifestDesc ocispec.Descriptor) {
displayHandler.OnResolvingTagReference(ref)
})
if err != nil {
return err
}
Expand All @@ -159,8 +165,8 @@ func runVerify(command *cobra.Command, opts *verifyOpts) error {
if err != nil {
return err
}
reportVerificationSuccess(outcomes, resolvedRef)
return nil
displayHandler.OnVerifySucceeded(outcomes, resolvedRef)
return displayHandler.Render()
}

func checkVerificationFailure(outcomes []*notation.VerificationOutcome, printOut string, err error) error {
Expand Down Expand Up @@ -195,37 +201,6 @@ func checkVerificationFailure(outcomes []*notation.VerificationOutcome, printOut
return nil
}

func reportVerificationSuccess(outcomes []*notation.VerificationOutcome, printout string) {
// write out on success
outcome := outcomes[0]
// print out warning for any failed result with logged verification action
for _, result := range outcome.VerificationResults {
if result.Error != nil {
// at this point, the verification action has to be logged and
// it's failed
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", printout)
} else {
fmt.Println("Successfully verified signature for", printout)
printMetadataIfPresent(outcome)
}
}

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()

if len(metadata) > 0 {
fmt.Println("\nThe artifact was signed with the following user metadata.")
ioutil.PrintMetadataMap(os.Stdout, metadata)
}
}

func getVerifier(ctx context.Context) (notation.Verifier, error) {
// revocation check
revocationCodeSigningValidator, err := clirev.NewRevocationValidator(ctx, purpose.CodeSigning)
Expand Down
12 changes: 0 additions & 12 deletions internal/ioutil/print.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,6 @@ func PrintKeyMap(w io.Writer, target *string, v []config.KeySuite) error {
return tw.Flush()
}

// PrintMetadataMap prints out metadata given the metatdata map
func PrintMetadataMap(w io.Writer, metadata map[string]string) error {
tw := newTabWriter(w)
fmt.Fprintln(tw, "\nKEY\tVALUE\t")

for k, v := range metadata {
fmt.Fprintf(tw, "%v\t%v\t\n", k, v)
}

return tw.Flush()
}

// PrintCertMap lists certificate files in the trust store given array of cert
// paths
func PrintCertMap(w io.Writer, certPaths []string) error {
Expand Down
Loading