diff --git a/cmd/notation/verify.go b/cmd/notation/verify.go index 367c02b38..6199db2f9 100644 --- a/cmd/notation/verify.go +++ b/cmd/notation/verify.go @@ -1,31 +1,23 @@ 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" + notationregistry "github.com/notaryproject/notation-go/registry" + "github.com/notaryproject/notation-go/verification" "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/internal/ioutil" "github.com/spf13/cobra" + "oras.land/oras-go/v2/registry" ) type verifyOpts struct { - RemoteFlagOpts - signatures []string - certs []string - certFiles []string - pull bool - reference string + SecureFlagOpts + reference string + pluginConfig string } func verifyCommand(opts *verifyOpts) *cobra.Command { @@ -33,21 +25,13 @@ func verifyCommand(opts *verifyOpts) *cobra.Command { opts = &verifyOpts{} } command := &cobra.Command{ - Use: "verify [reference]", - Short: "Verify OCI artifacts", - Long: `Verify OCI artifacts + Use: "verify [flags] ", + Short: "Verifies OCI Artifacts", + Long: `Verifies OCI Artifacts Prerequisite: a trusted certificate needs to be generated or added using the command "notation cert". -Example - Verify a signature using the trusted certificate: - notation verify /: - -Example - Verify a signature associated with an OCI artifact identified by the digest: - notation verify /@ - -Example - Verify a signature using a trusted certificate in a specified path: - notation verify --cert-file /: -`, +notation verify [--plugin-config =,...] [--username ] [--password ] `, Args: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { return errors.New("missing reference") @@ -59,119 +43,75 @@ Example - Verify a signature using a trusted certificate in a specified path: 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().StringVarP(&opts.pluginConfig, "plugin-config", "c", "", "list of comma-separated {key}={value} pairs that are passed as is to the plugin") return command } func runVerify(command *cobra.Command, opts *verifyOpts) error { - // initialize - verifier, err := getVerifier(opts) + // resolve the given reference and set the digest. + ref, err := resolveReference(command, opts) if err != nil { return err } - manifestDesc, err := getManifestDescriptorFromContext(command.Context(), &opts.RemoteFlagOpts, opts.reference) + + // initialize verifier. + verifier, err := getVerifier(opts, ref) 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)) - } - } - - // core process - if err := verifySignatures(command.Context(), verifier, manifestDesc, sigPaths); err != nil { + // set up verification plugin config. + configs, err := cmd.ParseFlagPluginConfig(opts.pluginConfig) + if err != nil { return err } - // write out - fmt.Println(manifestDesc.Digest) - return nil + // core verify process. + ctx := verification.WithPluginConfig(command.Context(), configs) + outcomes, err := verifier.Verify(ctx, ref.String()) + + // write out. + return ioutil.PrintVerificationResults(os.Stdout, outcomes, err, ref.Reference) } -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") +func getVerifier(opts *verifyOpts, ref registry.Reference) (*verification.Verifier, error) { + authClient, plainHTTP, err := getAuthClient(&opts.SecureFlagOpts, ref) + if err != nil { + return nil, err } - 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 - 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 - } - return lastErr + repo := notationregistry.NewRepositoryClient(authClient, ref, plainHTTP) + + return verification.NewVerifier(repo) } -func getVerifier(opts *verifyOpts) (notation.Verifier, error) { - certPaths, err := appendCertPathFromName(opts.certFiles, opts.certs) +func resolveReference(command *cobra.Command, opts *verifyOpts) (registry.Reference, error) { + ref, err := registry.ParseReference(opts.reference) if err != nil { - return nil, err + return registry.Reference{}, 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) - } + + if isDigestReference(opts.reference) { + return ref, nil } - return signature.NewVerifierFromFiles(certPaths) + + // Resolve tag reference to digest reference. + manifestDesc, err := getManifestDescriptorFromReference(command.Context(), &opts.SecureFlagOpts, opts.reference) + if err != nil { + return registry.Reference{}, err + } + + ref.Reference = manifestDesc.Digest.String() + return ref, nil } -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) +func isDigestReference(reference string) bool { + parts := strings.SplitN(reference, "/", 2) + if len(parts) == 1 { + return false } - return paths, nil + + index := strings.Index(parts[1], "@") + return index != -1 } diff --git a/cmd/notation/verify_test.go b/cmd/notation/verify_test.go index 64c98f27d..c3f03ee6b 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,15 @@ 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, + pluginConfig: "key1=val1,key2=val2", } if err := command.ParseFlags([]string{ expected.reference, "--plain-http", - "--pull=false", - "--media-type=mediaT"}); err != nil { + "--plugin-config", expected.pluginConfig}); err != nil { t.Fatalf("Parse Flag failed: %v", err) } if err := command.Args(command, command.Flags().Args()); err != nil { diff --git a/go.mod b/go.mod index a25c2ff6b..235c264ff 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-20220825204002-c680a09ffe64 // indirect ) diff --git a/go.sum b/go.sum index cc7e667db..07166ac5d 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,18 @@ +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/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= github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= 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= @@ -21,20 +29,36 @@ 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= github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= 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.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-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-20220825204002-c680a09ffe64 h1:UiNENfZ8gDvpiWw7IpOMQ27spWmThO1RwwdQVbJahJM= golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/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.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..9f7eca634 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,28 @@ func PrintCertificateMap(w io.Writer, v []config.CertificateReference) error { } return tw.Flush() } + +func PrintVerificationResults(w io.Writer, v []*verification.SignatureVerificationOutcome, resultErr error, digest string) error { + tw := newTabWriter(w) + + if resultErr == nil { + fmt.Fprintf(tw, "Signature verification succeeded for %s\n", digest) + // TODO[https://github.com/notaryproject/notation/issues/304]: print out failed validations as warnings. + return nil + } + + fmt.Fprintf(tw, "ERROR: %s\n\n", resultErr.Error()) + printOutcomes(tw, v, digest) + tw.Flush() + + return resultErr +} + +func printOutcomes(tw *tabwriter.Writer, outcomes []*verification.SignatureVerificationOutcome, digest string) { + fmt.Printf("Signature verification failed for all the %d signatures associated with digest: %s\n\n", len(outcomes), digest) + + // TODO[https://github.com/notaryproject/notation/issues/304]: print out detailed errors in debug mode. + for idx, outcome := range outcomes { + fmt.Printf("Signature #%d : %s\n", idx+1, outcome.Error.Error()) + } +}