diff --git a/cmd/notation/verify.go b/cmd/notation/verify.go index f8b81628c..3caf7c6f3 100644 --- a/cmd/notation/verify.go +++ b/cmd/notation/verify.go @@ -1,31 +1,24 @@ package main import ( - "context" "errors" "fmt" "os" + "strings" - "github.com/notaryproject/notation-go" - "github.com/notaryproject/notation-go/dir" - "github.com/notaryproject/notation-go/signature" - "github.com/notaryproject/notation/internal/cmd" - "github.com/notaryproject/notation/internal/envelope" - "github.com/notaryproject/notation/internal/slices" - "github.com/notaryproject/notation/pkg/cache" - "github.com/notaryproject/notation/pkg/configutil" - "github.com/opencontainers/go-digest" + "github.com/notaryproject/notation-go/registry" + "github.com/notaryproject/notation-go/verification" + "github.com/notaryproject/notation/internal/ioutil" + + orasregistry "oras.land/oras-go/v2/registry" "github.com/spf13/cobra" ) type verifyOpts struct { - RemoteFlagOpts - signatures []string - certs []string - certFiles []string - pull bool - reference string + SecureFlagOpts + reference string + config []string } func verifyCommand(opts *verifyOpts) *cobra.Command { @@ -33,8 +26,10 @@ func verifyCommand(opts *verifyOpts) *cobra.Command { opts = &verifyOpts{} } command := &cobra.Command{ - Use: "verify [reference]", + Use: "verify ", Short: "Verifies OCI Artifacts", + Long: `Verifies OCI Artifacts: + notation verify [--config =] [--username ] [--password ] `, Args: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { return errors.New("missing reference") @@ -46,120 +41,48 @@ func verifyCommand(opts *verifyOpts) *cobra.Command { return runVerify(cmd, opts) }, } - setFlagSignature(command.Flags(), &opts.signatures) - command.Flags().StringSliceVarP(&opts.certs, "cert", "c", []string{}, "certificate names for verification") - command.Flags().StringSliceVar(&opts.certFiles, cmd.PflagCertFile.Name, []string{}, "certificate files for verification") - command.Flags().BoolVar(&opts.pull, "pull", true, "pull remote signatures before verification") opts.ApplyFlags(command.Flags()) + command.Flags().StringSliceVar(&opts.config, "config", nil, "verification plugin config (accepts multiple inputs)") return command } func runVerify(command *cobra.Command, opts *verifyOpts) error { - // initialize + // initialize. verifier, err := getVerifier(opts) if err != nil { return err } - manifestDesc, err := getManifestDescriptorFromContext(command.Context(), &opts.RemoteFlagOpts, opts.reference) - if err != nil { - return err - } - sigPaths := opts.signatures - if len(sigPaths) == 0 { - if !opts.Local && opts.pull { - if err := pullSignatures(command, opts.reference, &opts.SecureFlagOpts, digest.Digest(manifestDesc.Digest)); err != nil { - return err - } - } - manifestDigest := digest.Digest(manifestDesc.Digest) - sigDigests, err := cache.SignatureDigests(manifestDigest) - if err != nil { - return err - } - for _, sigDigest := range sigDigests { - sigPaths = append(sigPaths, dir.Path.CachedSignature(manifestDigest, sigDigest)) + // set up verification plugin config. + configs := make(map[string]string) + for _, c := range opts.config { + parts := strings.Split(c, "=") + if len(parts) != 2 { + return fmt.Errorf("invalid config option: %s", c) } + configs[parts[0]] = parts[1] } + ctx := verification.WithPluginConfig(command.Context(), configs) - // core process - if err := verifySignatures(command.Context(), verifier, manifestDesc, sigPaths); err != nil { - return err - } + // core verify process. + outcomes, err := verifier.Verify(ctx, opts.reference) - // write out - fmt.Println(manifestDesc.Digest) - return nil + // write out. + return ioutil.PrintVerificationResults(os.Stdout, outcomes, err) } -func verifySignatures(ctx context.Context, verifier notation.Verifier, manifestDesc notation.Descriptor, sigPaths []string) error { - if len(sigPaths) == 0 { - return errors.New("verification failure: no signatures found") - } - - var lastErr error - for _, path := range sigPaths { - sig, err := os.ReadFile(path) - if err != nil { - lastErr = fmt.Errorf("verification failure: %v", err) - continue - } - // pass in nonempty annotations if needed - // TODO: understand media type in a better way - sigMediaType, err := envelope.SpeculateSignatureEnvelopeFormat(sig) - if err != nil { - lastErr = fmt.Errorf("verification failure: %v", err) - continue - } - opts := notation.VerifyOptions{ - SignatureMediaType: sigMediaType, - } - desc, err := verifier.Verify(ctx, sig, opts) - if err != nil { - lastErr = fmt.Errorf("verification failure: %v", err) - continue - } - - if !desc.Equal(manifestDesc) { - lastErr = fmt.Errorf("verification failure: %s", manifestDesc.Digest) - continue - } - return nil +func getVerifier(opts *verifyOpts) (*verification.Verifier, error) { + ref, err := orasregistry.ParseReference(opts.reference) + if err != nil { + return nil, err } - return lastErr -} -func getVerifier(opts *verifyOpts) (notation.Verifier, error) { - certPaths, err := appendCertPathFromName(opts.certFiles, opts.certs) + authClient, plainHTTP, err := getAuthClient(&opts.SecureFlagOpts, ref) if err != nil { return nil, err } - if len(certPaths) == 0 { - cfg, err := configutil.LoadConfigOnce() - if err != nil { - return nil, err - } - if len(cfg.VerificationCertificates.Certificates) == 0 { - return nil, errors.New("trust certificate not specified") - } - for _, ref := range cfg.VerificationCertificates.Certificates { - certPaths = append(certPaths, ref.Path) - } - } - return signature.NewVerifierFromFiles(certPaths) -} -func appendCertPathFromName(paths, names []string) ([]string, error) { - for _, name := range names { - cfg, err := configutil.LoadConfigOnce() - if err != nil { - return nil, err - } - idx := slices.Index(cfg.VerificationCertificates.Certificates, name) - if idx < 0 { - return nil, errors.New("verification certificate not found: " + name) - } - paths = append(paths, cfg.VerificationCertificates.Certificates[idx].Path) - } - return paths, nil + repo := registry.NewRepositoryClient(authClient, ref, plainHTTP) + + return verification.NewVerifier(repo) } diff --git a/cmd/notation/verify_test.go b/cmd/notation/verify_test.go index 64c98f27d..349f638eb 100644 --- a/cmd/notation/verify_test.go +++ b/cmd/notation/verify_test.go @@ -10,30 +10,15 @@ func TestVerifyCommand_BasicArgs(t *testing.T) { command := verifyCommand(opts) expected := &verifyOpts{ reference: "ref", - RemoteFlagOpts: RemoteFlagOpts{ - SecureFlagOpts: SecureFlagOpts{ - Username: "user", - Password: "password", - }, - CommonFlagOpts: CommonFlagOpts{ - MediaType: defaultMediaType, - }, + SecureFlagOpts: SecureFlagOpts{ + Username: "user", + Password: "password", }, - certs: []string{"cert0", "cert1"}, - certFiles: []string{"certfile0", "certfile1"}, - signatures: []string{"sig0", "sig1"}, - pull: true, } if err := command.ParseFlags([]string{ expected.reference, "--username", expected.Username, - "--password", expected.Password, - "-c", expected.certs[0], - "--cert", expected.certs[1], - "--cert-file", expected.certFiles[0], - "--cert-file", expected.certFiles[1], - "--signature", expected.signatures[0], - "-s", expected.signatures[1]}); err != nil { + "--password", expected.Password}); err != nil { t.Fatalf("Parse Flag failed: %v", err) } if err := command.Args(command, command.Flags().Args()); err != nil { @@ -49,24 +34,16 @@ func TestVerifyCommand_MoreArgs(t *testing.T) { command := verifyCommand(opts) expected := &verifyOpts{ reference: "ref", - RemoteFlagOpts: RemoteFlagOpts{ - SecureFlagOpts: SecureFlagOpts{ - PlainHTTP: true, - }, - CommonFlagOpts: CommonFlagOpts{ - MediaType: "mediaT", - }, + SecureFlagOpts: SecureFlagOpts{ + PlainHTTP: true, }, - certs: []string{}, - certFiles: []string{}, - signatures: []string{}, - pull: false, + config: []string{"key1=val1", "key2=val2"}, } if err := command.ParseFlags([]string{ expected.reference, "--plain-http", - "--pull=false", - "--media-type=mediaT"}); err != nil { + "--config", expected.config[0], + "--config", expected.config[1]}); err != nil { t.Fatalf("Parse Flag failed: %v", err) } if err := command.Args(command, command.Flags().Args()); err != nil { diff --git a/docs/hello-signing.md b/docs/hello-signing.md index 17f953c05..f075fbfa2 100644 --- a/docs/hello-signing.md +++ b/docs/hello-signing.md @@ -98,6 +98,10 @@ To get things started quickly, the Notation cli supports self-signed certificate ```bash notation sign --envelope-type cose $IMAGE ``` + To save the generated digest + ``` + export DIGEST=$(notation sign $IMAGE) + ``` - List the image, and any associated signatures @@ -107,12 +111,37 @@ To get things started quickly, the Notation cli supports self-signed certificate ## Verify a Container Image Using Notation Signatures +Notation provides a trust policy for users to specify trusted identities which will sign the artifiacts, and level of signature verification to use. A trust policy is a JSON document, below example works for the current case. + +``` +{ + "version": "1.0", + "trustPolicies": [ + { + "name": "wabbit-networks-images", + "registryScopes": [ + "localhost:5000/net-monitor" + ], + "signatureVerification": { + "level": "strict" + }, + "trustStores": [ + "ca:wabbit-networks.io" + ], + "trustedIdentities": [ + "x509.subject: C=US, ST=WA, L=Seattle, O=Notary" + ] + } + ] +} +``` + To avoid a Trojan Horse attack, and before pulling an artifact into an environment, it is important to verify that the artifact was unmodified after it was created (integrity), and from an trusted entity (authenticity). Notation uses a set of configured public keys that represent trusted entities, to verify the content. The `notation cert generate-test` command created the public key, however it must be explicitly added for verification to succeed. - Attempt to verify the $IMAGE notation signature ```bash - notation verify $IMAGE + notation verify --plain-http $REPO@$DIGEST ``` *The above verification should fail, as you haven't yet configured the keys to trust.* @@ -131,7 +160,7 @@ To avoid a Trojan Horse attack, and before pulling an artifact into an environme - Verify the `net-monitor:v1` notation signature ```bash - notation verify $IMAGE + notation verify --plain-http $REPO@$DIGEST ``` This should now succeed because the image is signed with a trusted public key diff --git a/go.mod b/go.mod index d5fe4e43e..0a1f89dde 100644 --- a/go.mod +++ b/go.mod @@ -15,13 +15,17 @@ require ( ) require ( + github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/fxamacker/cbor/v2 v2.4.0 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect + github.com/go-ldap/ldap/v3 v3.4.4 // indirect github.com/golang-jwt/jwt/v4 v4.4.2 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/opencontainers/distribution-spec/specs-go v0.0.0-20220620172159-4ab4752c3b86 // indirect github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect github.com/oras-project/artifacts-spec v1.0.0-rc.2 // indirect github.com/x448/float16 v0.8.4 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect golang.org/x/sys v0.0.0-20220731174439-a90be440212d // indirect ) diff --git a/go.sum b/go.sum index 54a1f0722..d668b9fb8 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,8 @@ +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= +github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/distribution/v3 v3.0.0-20220729163034-26163d82560f h1:3NCYdjXycNd/Xn/iICZzmxkiDX1e1cjTHjbMAz+wRVk= github.com/distribution/distribution/v3 v3.0.0-20220729163034-26163d82560f/go.mod h1:28YO/VJk9/64+sTGNuYaBjWxrXTPrj0C0XmgTIOjxX4= @@ -7,16 +10,16 @@ github.com/docker/docker-credential-helpers v0.6.4 h1:axCks+yV+2MR3/kZhAmy07yC56 github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c= github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= +github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= +github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= +github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/notaryproject/notation-core-go v0.0.0-20220926023941-e7aa03204754 h1:GDZ4iH72knZwsgcOUz6y22vZ/mV+tCxeBl/I9jVWIi8= -github.com/notaryproject/notation-core-go v0.0.0-20220926023941-e7aa03204754/go.mod h1:s8DZptmN1rZS0tBLTPt/w+d4o6eAcGWTYYJlXaJhQ4U= github.com/notaryproject/notation-core-go v0.0.0-20220926064152-c65f5c6b3537 h1:z2GiPNoJtMQdI2hqDO4a7XpCPf7+1J7/MJYpF8C8AAM= github.com/notaryproject/notation-core-go v0.0.0-20220926064152-c65f5c6b3537/go.mod h1:s8DZptmN1rZS0tBLTPt/w+d4o6eAcGWTYYJlXaJhQ4U= -github.com/notaryproject/notation-go v0.10.0-alpha.3.0.20220926044148-68dfda13bb1d h1:C/4OCvmaxrCSruAEFHwCMH55e44aWVLWN4JeL/a5+vQ= -github.com/notaryproject/notation-go v0.10.0-alpha.3.0.20220926044148-68dfda13bb1d/go.mod h1:sgM4TsGODz1H+TfwUfzyhKHROO62iX6Kr9LgOc/+buk= github.com/notaryproject/notation-go v0.10.0-alpha.3.0.20220926065242-9f4d8598695b h1:HiGlp0EHAGwO+51BvyY13q/vKPYIQ6PqLta4WRY1jQU= github.com/notaryproject/notation-go v0.10.0-alpha.3.0.20220926065242-9f4d8598695b/go.mod h1:Dp4v+xlRfZ6XcStAOSo9VhOtevpZJg70wA5bOiR+B/4= github.com/opencontainers/distribution-spec/specs-go v0.0.0-20220620172159-4ab4752c3b86 h1:Oumw+lPnO8qNLTY2mrqPJZMoGExLi/0h/DdikoLTXVU= @@ -27,6 +30,7 @@ github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3 github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/oras-project/artifacts-spec v1.0.0-rc.2 h1:9SMCNSxkJEHqWGDiMCuy6TXHgvjgwXGdXZZGXLKQvVE= github.com/oras-project/artifacts-spec v1.0.0-rc.2/go.mod h1:Xch2aLzSwtkhbFFN6LUzTfLtukYvMMdXJ4oZ8O7BOdc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= @@ -35,17 +39,30 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/veraison/go-cose v1.0.0-rc.1.0.20220824135457-9d2fab636b83 h1:g8vDfnNOPcGzg6mnlBGc0J5t5lAJkaepXqbc9qFRnFs= github.com/veraison/go-cose v1.0.0-rc.1.0.20220824135457-9d2fab636b83/go.mod h1:7ziE85vSq4ScFTg6wyoMXjucIGOf4JkFEZi/an96Ct4= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220731174439-a90be440212d h1:Sv5ogFZatcgIMMtBSTTAgMYsicp25MXBubjXNDKwm80= golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= oras.land/oras-go/v2 v2.0.0-rc.3 h1:O4GeIwJ9Ge7rbCkqa/M7DLrL55ww+ZEc+Rhc63OYitU= oras.land/oras-go/v2 v2.0.0-rc.3/go.mod h1:PrY+cCglzK/DrQoJUtxbYVbL94ZHecVS3eJR01RglpE= diff --git a/internal/ioutil/print.go b/internal/ioutil/print.go index 3e62c1c33..90993858a 100644 --- a/internal/ioutil/print.go +++ b/internal/ioutil/print.go @@ -7,6 +7,7 @@ import ( "github.com/notaryproject/notation-go/config" "github.com/notaryproject/notation-go/plugin/manager" + "github.com/notaryproject/notation-go/verification" ) func newTabWriter(w io.Writer) *tabwriter.Writer { @@ -52,3 +53,29 @@ func PrintCertificateMap(w io.Writer, v []config.CertificateReference) error { } return tw.Flush() } + +func PrintVerificationResults(w io.Writer, v []*verification.SignatureVerificationOutcome, resultErr error) error { + tw := newTabWriter(w) + + overallResult := "success" + if resultErr != nil { + overallResult = "failure" + } + fmt.Fprintf(tw, "OVERALL RESULT: %s\n", overallResult) + + if resultErr != nil { + fmt.Fprintf(tw, "ERROR: %s\n", resultErr.Error()) + printOutcomes(tw, v) + } + + return tw.Flush() +} + +func printOutcomes(tw *tabwriter.Writer, outcomes []*verification.SignatureVerificationOutcome) { + if len(outcomes) > 0 { + fmt.Fprintln(tw, "DETAILED ERRORS:") + for _, outcome := range outcomes { + fmt.Println(outcome.Error) + } + } +} \ No newline at end of file