github.com/vchain-us/vcn@v0.9.11-0.20210921212052-a2484d23c0b3/pkg/cmd/verify/verify.go (about)

     1  /*
     2   * Copyright (c) 2018-2020 vChain, Inc. All Rights Reserved.
     3   * This software is released under GPL3.
     4   * The full license information can be found under:
     5   * https://www.gnu.org/licenses/gpl-3.0.en.html
     6   *
     7   */
     8  
     9  package verify
    10  
    11  import (
    12  	"context"
    13  	"encoding/json"
    14  	"fmt"
    15  	"regexp"
    16  	"strconv"
    17  	"strings"
    18  
    19  	"github.com/blang/semver"
    20  	"github.com/fatih/color"
    21  	"github.com/spf13/cobra"
    22  	"github.com/spf13/viper"
    23  	"google.golang.org/grpc/metadata"
    24  
    25  	immuschema "github.com/codenotary/immudb/pkg/api/schema"
    26  	"github.com/vchain-us/ledger-compliance-go/schema"
    27  	vcnerr "github.com/vchain-us/vcn/internal/errors"
    28  	"github.com/vchain-us/vcn/pkg/api"
    29  	"github.com/vchain-us/vcn/pkg/bom/artifact"
    30  	"github.com/vchain-us/vcn/pkg/cmd/internal/cli"
    31  	"github.com/vchain-us/vcn/pkg/cmd/internal/types"
    32  	"github.com/vchain-us/vcn/pkg/extractor"
    33  	"github.com/vchain-us/vcn/pkg/meta"
    34  	"github.com/vchain-us/vcn/pkg/signature"
    35  	"github.com/vchain-us/vcn/pkg/store"
    36  )
    37  
    38  type pkg struct {
    39  	Name   string
    40  	Hash   string
    41  	Kind   string
    42  	Md     md `json:"metadata"`
    43  	Status int
    44  }
    45  
    46  type md struct {
    47  	Version  string `json:"version,omitempty"`
    48  	HashType string `json:"hashType"`
    49  }
    50  
    51  var (
    52  	keyRegExp = regexp.MustCompile("0x[0-9a-z]{40}")
    53  )
    54  
    55  func getSignerIDs() []string {
    56  	ids := viper.GetStringSlice("signerID")
    57  	if len(ids) > 0 {
    58  		return ids
    59  	}
    60  	return viper.GetStringSlice("key")
    61  }
    62  
    63  // NewCommand returns the cobra command for `vcn verify`
    64  func NewCommand() *cobra.Command {
    65  	cmd := &cobra.Command{
    66  		Use:     "authenticate",
    67  		Example: "  vcn authenticate /bin/vcn",
    68  		Aliases: []string{"a", "verify", "v"},
    69  		Short:   "Authenticate assets against the blockchain",
    70  		Long: `
    71  Authenticate assets against the blockchain.
    72  
    73  Authentication is the process of matching the hash of a local asset to
    74  a hash on the blockchain.
    75  If matched, the returned result (the authentication) is the blockchain-stored
    76  metadata that’s bound to the matching hash.
    77  Otherwise, the returned result status equals UNKNOWN.
    78  
    79  Note that your assets will not be uploaded but processed locally.
    80  
    81  The exit code will be 0 only if all assets' statuses are equal to TRUSTED.
    82  Otherwise, the exit code will be 1.
    83  
    84  Assets are referenced by the passed ARG(s), with authentication accepting
    85  1 or more ARG(s) at a time. Multiple assets can be authenticated at the
    86  same time while passing them within ARG(s).
    87  
    88  ARG must be one of:
    89    <file>
    90    file://<file>
    91    dir://<directory>
    92    git://<repository>
    93    docker://<image>
    94    podman://<image>
    95    javacom://<java mvn jar or pom.xml>
    96    nodecom://<node component>
    97    gocom://<Go module in name@version format>
    98    pythoncom://<Python module in name@version format>
    99    dotnetcom://<.Net module in name@version format>
   100  Environment variables:
   101  VCN_USER=
   102  VCN_PASSWORD=
   103  VCN_NOTARIZATION_PASSWORD=
   104  VCN_NOTARIZATION_PASSWORD_EMPTY=
   105  VCN_OTP=
   106  VCN_OTP_EMPTY=
   107  VCN_LC_HOST=
   108  VCN_LC_PORT=
   109  VCN_LC_CERT=
   110  VCN_LC_SKIP_TLS_VERIFY=false
   111  VCN_LC_NO_TLS=false
   112  VCN_LC_API_KEY=
   113  VCN_LC_LEDGER=
   114  VCN_SIGNING_PUB_KEY_FILE=
   115  VCN_SIGNING_PUB_KEY=
   116  VCN_ENFORCE_SIGNATURE_VERIFY=
   117  `,
   118  		RunE: runVerify,
   119  		PreRun: func(cmd *cobra.Command, args []string) {
   120  			// Bind to all flags to env vars (after flags were parsed),
   121  			// but only ones retrivied by using viper will be used.
   122  			viper.BindPFlags(cmd.Flags())
   123  		},
   124  		Args: func(cmd *cobra.Command, args []string) error {
   125  			if org := viper.GetString("org"); org != "" {
   126  				if keys := getSignerIDs(); len(keys) > 0 {
   127  					return fmt.Errorf("cannot use both --org and SignerID(s)")
   128  				}
   129  			}
   130  
   131  			alerts, _ := cmd.Flags().GetBool("alerts")
   132  			if alerts {
   133  				if len(args) > 0 {
   134  					return fmt.Errorf("cannot use ARG(s) with --alerts")
   135  				}
   136  				return nil
   137  			}
   138  
   139  			if hash, _ := cmd.Flags().GetString("hash"); hash != "" {
   140  				if len(args) > 0 {
   141  					return fmt.Errorf("cannot use ARG(s) with --hash")
   142  				}
   143  				if alerts {
   144  					return fmt.Errorf("cannot use both --alerts and --hash")
   145  				}
   146  
   147  				return nil
   148  			}
   149  
   150  			if name, _ := cmd.Flags().GetString("name"); name != "" {
   151  				if len(args) > 0 {
   152  					return fmt.Errorf("cannot use ARG(s) with --name")
   153  				}
   154  				return nil
   155  			}
   156  
   157  			return cobra.MinimumNArgs(1)(cmd, args)
   158  		},
   159  	}
   160  
   161  	cmd.SetUsageTemplate(
   162  		strings.Replace(cmd.UsageTemplate(), "{{.UseLine}}", "{{.UseLine}} ARG(s)", 1),
   163  	)
   164  
   165  	cmd.Flags().StringSliceP("signerID", "s", nil, "accept only authentications matching the passed SignerID(s)\n(overrides VCN_SIGNERID env var, if any). It's valid both for blockchain and ledger compliance")
   166  	cmd.Flags().StringSliceP("key", "k", nil, "")
   167  	cmd.Flags().MarkDeprecated("key", "please use --signerID instead")
   168  	cmd.Flags().StringP("org", "I", "", "accept only authentications matching the passed organisation's ID,\nif set no SignerID can be used\n(overrides VCN_ORG env var, if any)")
   169  	cmd.Flags().String("hash", "", "specify a hash to authenticate, if set no ARG(s) can be used")
   170  	cmd.Flags().Bool("alerts", false, "specify to authenticate and monitor for the configured alerts, if set no ARG(s) can be used")
   171  	cmd.Flags().Bool("raw-diff", false, "print raw a diff, if any")
   172  	cmd.Flags().Int("exit-code", meta.VcnDefaultExitCode, meta.VcnExitCode)
   173  	cmd.Flags().String("lc-host", "", meta.VcnLcHostFlagDesc)
   174  	cmd.Flags().String("lc-port", "443", meta.VcnLcPortFlagDesc)
   175  	cmd.Flags().String("lc-cert", "", meta.VcnLcCertPathDesc)
   176  	cmd.Flags().Bool("lc-skip-tls-verify", false, meta.VcnLcSkipTlsVerifyDesc)
   177  	cmd.Flags().Bool("lc-no-tls", false, meta.VcnLcNoTlsDesc)
   178  	cmd.Flags().String("lc-api-key", "", meta.VcnLcApiKeyDesc)
   179  	cmd.Flags().String("lc-ledger", "", meta.VcnLcLedgerDesc)
   180  	cmd.Flags().String("lc-uid", "", meta.VcnLcUidDesc)
   181  	cmd.Flags().String("attach", "", meta.VcnLcAttachmentAuthDesc)
   182  	cmd.Flags().Bool("force", false, meta.VcnLcForceAttachmentDownloadDesc)
   183  	cmd.Flags().String("name", "", "asset name to look up")
   184  	cmd.Flags().String("version", "", "asset version to look up")
   185  	cmd.Flags().Bool("bom", false, "link asset to its dependencies from BoM")
   186  	cmd.Flags().String("bom-trust-level", "trusted", "min trust level: untrusted (unt) / unsupported (uns) / unknown (unk) / trusted (t)")
   187  	cmd.Flags().Float64("bom-max-unsupported", 0, "max number (in %) of unsupported dependencies")
   188  	cmd.Flags().String("bom-file", "", "store BoM in the file for later processing")
   189  	cmd.Flags().Bool("bom-deps-only", false, "authenticate only the dependencies, not the asset")
   190  	cmd.Flags().Bool("bom-what-includes", false, "output all assets that use the specified asset")
   191  	cmd.Flags().StringSlice("bom-container-binary", []string{}, "list of binaries to be executed inside the container - only the relevant dependencies will be processed")
   192  	cmd.Flags().Uint("bom-batch-size", 10, "By default BoM dependencies are authenticated/notarized in batches of up to 10 dependencies each. Use this flag to set a different batch size. A value of 0 will disable batching (all dependencies will be authenticated/notarized at once).")
   193  	// BoM output options
   194  	cmd.Flags().Bool("bom-debug", false, "show extra debug info for BoM processing, also disable progress indicators")
   195  	cmd.Flags().String("bom-spdx", "", "name of the file to output BoM in SPDX format")
   196  	cmd.Flags().String("bom-cyclonedx-json", "", "name of the file to output BoM in CycloneDX JSON format")
   197  	cmd.Flags().String("bom-cyclonedx-xml", "", "name of the file to output BoM in CycloneDX XML format")
   198  	cmd.Flags().String("github-token", "", "Github OAuth token for querying BoM Github package details. Either authenticated or not, requests are subject to Github limits")
   199  
   200  	cmd.Flags().String("signing-pub-key-file", "", meta.VcnSigningPubKeyFileNameDesc)
   201  	cmd.Flags().String("signing-pub-key", "", meta.VcnSigningPubKeyDesc)
   202  	cmd.Flags().Bool("enforce-signature-verify", false, meta.VcnEnforceSignatureVerifyDesc)
   203  	cmd.Flags().MarkHidden("raw-diff")
   204  
   205  	return cmd
   206  }
   207  
   208  // runVerify first determine if the context is LC or blockchain, then call the correct verify
   209  func runVerify(cmd *cobra.Command, args []string) error {
   210  	hashes := make([]string, 0)
   211  	hash, err := cmd.Flags().GetString("hash")
   212  	if err != nil {
   213  		return err
   214  	}
   215  	if hash != "" {
   216  		hashes = append(hashes, strings.ToLower(hash))
   217  	}
   218  
   219  	output, err := cmd.Flags().GetString("output")
   220  	if err != nil {
   221  		return err
   222  	}
   223  
   224  	useAlerts, err := cmd.Flags().GetBool("alerts")
   225  	if err != nil {
   226  		return err
   227  	}
   228  
   229  	cmd.SilenceUsage = true
   230  
   231  	lcHost := viper.GetString("lc-host")
   232  	lcPort := viper.GetString("lc-port")
   233  	lcCert := viper.GetString("lc-cert")
   234  	skipTlsVerify := viper.GetBool("lc-skip-tls-verify")
   235  	noTls := viper.GetBool("lc-no-tls")
   236  	lcApiKey := viper.GetString("lc-api-key")
   237  	lcLedger := viper.GetString("lc-ledger")
   238  	lcUid := viper.GetString("lc-uid")
   239  	lcAttach := viper.GetString("attach")
   240  	lcAttachForce := viper.GetBool("force")
   241  	lcVerbose := viper.GetBool("verbose")
   242  
   243  	signingPubKey, skipLocalPubKeyComp, err := signature.PrepareSignatureParams(
   244  		viper.GetString("signing-pub-key"),
   245  		viper.GetString("signing-pub-key-file"))
   246  	if err != nil {
   247  		return err
   248  	}
   249  	enforceSignatureVerify := viper.GetBool("enforce-signature-verify")
   250  
   251  	//check if an lcUser is present inside the context
   252  	var lcUser *api.LcUser
   253  
   254  	uif, err := api.GetUserFromContext(store.Config().CurrentContext, lcApiKey, lcLedger, signingPubKey)
   255  	if err != nil {
   256  		return err
   257  	}
   258  	if lctmp, ok := uif.(*api.LcUser); ok {
   259  		lcUser = lctmp
   260  	}
   261  
   262  	// It uses flags for CNC context client constructor if at least host is provided. A client with empty api-key is allowed for public authentications
   263  	if lcHost != "" {
   264  		// client from context could be override by the one created from local flags
   265  		lcUser, err = api.NewLcUser(lcApiKey, lcLedger, lcHost, lcPort, lcCert, skipTlsVerify, noTls, signingPubKey)
   266  		if err != nil {
   267  			return err
   268  		}
   269  		// Store the new config
   270  		if lcApiKey != "" {
   271  			if err := store.SaveConfig(); err != nil {
   272  				return err
   273  			}
   274  		}
   275  	}
   276  
   277  	if lcUser != nil {
   278  		var signerID string
   279  		signerIDs := getSignerIDs()
   280  		if len(signerIDs) > 0 {
   281  			signerID = signerIDs[0]
   282  		}
   283  		if lcApiKey == "" && signerID == "" {
   284  			return vcnerr.ErrPubAuthNoSignerID
   285  		}
   286  
   287  		err = lcUser.Client.Connect()
   288  		if err != nil {
   289  			return err
   290  		}
   291  
   292  		if !skipLocalPubKeyComp {
   293  			err = lcUser.CheckConnectionPublicKey(enforceSignatureVerify)
   294  			if err != nil {
   295  				return err
   296  			}
   297  		}
   298  
   299  		name := viper.GetString("name")
   300  		if name != "" {
   301  			// asset selection by name with optional filtering by version
   302  			if hash != "" {
   303  				return fmt.Errorf("cannot specify both the assets name/version and hash")
   304  			}
   305  			version := viper.GetString("version")
   306  			md := metadata.Pairs(meta.VcnLCPluginTypeHeaderName, meta.VcnLCPluginTypeHeaderValue)
   307  			ctx := metadata.NewOutgoingContext(context.Background(), md)
   308  			if signerID == "" {
   309  				signerID = api.GetSignerIDByApiKey(lcUser.Client.ApiKey)
   310  			}
   311  
   312  			zItems, err := lcUser.Client.ZScan(ctx, &immuschema.ZScanRequest{
   313  				Set:    []byte(name),
   314  				NoWait: true,
   315  			})
   316  			if err != nil {
   317  				return fmt.Errorf("cannot get components by name: %w", err)
   318  			}
   319  			var vRange semver.Range
   320  			if version != "" {
   321  				vRange, err = semver.ParseRange(version)
   322  				if err != nil {
   323  					return fmt.Errorf("cannot parse version range expression: %w", err)
   324  				}
   325  			}
   326  			for _, item := range zItems.Entries {
   327  				var p pkg
   328  				err := json.Unmarshal(item.Entry.Value, &p)
   329  				if err != nil {
   330  					return fmt.Errorf("cannot parse JSON: %w", err)
   331  				}
   332  				if version != "" {
   333  					if p.Md.Version != "" {
   334  						ver := strings.TrimPrefix(p.Md.Version, "v")
   335  						v, err := semver.Parse(ver)
   336  						if err != nil {
   337  							fmt.Printf("asset has invalid version %s\n", p.Md.Version)
   338  							continue
   339  						}
   340  						if vRange(v) {
   341  							hashes = append(hashes, p.Hash)
   342  						}
   343  					}
   344  				} else {
   345  					// no version condition - add all
   346  					hashes = append(hashes, p.Hash)
   347  				}
   348  			}
   349  			if len(hashes) == 0 {
   350  				return fmt.Errorf("no assets matching specified name/version found")
   351  			}
   352  		}
   353  
   354  		// any set 'bom-xxx' option, except 'bom-what-includes', implies BoM
   355  		bomFlag := viper.GetBool("bom") ||
   356  			viper.IsSet("bom-file") ||
   357  			viper.IsSet("bom-deps-only") ||
   358  			viper.IsSet("bom-debug") ||
   359  			viper.IsSet("bom-trust-level") ||
   360  			viper.IsSet("bom-max-unsupported") ||
   361  			viper.IsSet("bom-spdx") ||
   362  			viper.IsSet("bom-cyclonedx-json") ||
   363  			viper.IsSet("bom-cyclonedx-xml") ||
   364  			viper.IsSet("bom-container-binary") ||
   365  			viper.IsSet("bom-batch-size")
   366  
   367  		if bomFlag {
   368  			err := lcUser.RequireFeatOrErr(schema.FeatBoM)
   369  			if err != nil {
   370  				return err
   371  			}
   372  		}
   373  
   374  		var bomArtifact artifact.Artifact
   375  		if bomFlag {
   376  			if len(hashes)+len(args) > 1 {
   377  				return fmt.Errorf("asset selection criteria match several assets - BoM can be processed only for single asset")
   378  			}
   379  			if len(hashes)+len(args) < 1 {
   380  				return fmt.Errorf("asset selection criteria don't match any assets - BoM cannot be processed")
   381  			}
   382  
   383  			if len(hashes) > 0 {
   384  				bomArtifact, err = processBOM(lcUser, signerID, output, hashes[0], "")
   385  			} else {
   386  				bomArtifact, err = processBOM(lcUser, signerID, output, "", args[0])
   387  			}
   388  			if err != nil {
   389  				return err
   390  			}
   391  		}
   392  
   393  		if !viper.GetBool("bom-deps-only") {
   394  			// by hash
   395  			if len(hashes) > 0 {
   396  				for _, hash := range hashes {
   397  					a := &api.Artifact{
   398  						Hash: hash,
   399  					}
   400  					if viper.GetBool("bom-what-includes") {
   401  						a.IncludedIn, err = GetIncluded(hash, signerID, lcUser)
   402  						if err != nil {
   403  							return err
   404  						}
   405  					}
   406  					err = lcVerify(cmd, a, lcUser, signerID, lcUid, lcAttach, lcAttachForce, lcVerbose, output)
   407  					if err != nil {
   408  						return err
   409  					}
   410  				}
   411  			} else {
   412  				artifacts, err := extractor.Extract([]string{args[0]})
   413  				if err != nil {
   414  					return err
   415  				}
   416  				for _, a := range artifacts {
   417  					if viper.GetBool("bom-what-includes") {
   418  						a.IncludedIn, err = GetIncluded(a.Hash, signerID, lcUser)
   419  						if err != nil {
   420  							return err
   421  						}
   422  					}
   423  					if bomArtifact != nil {
   424  						a.Deps = DepsToPackageDetails(bomArtifact.Dependencies())
   425  					}
   426  					err := lcVerify(cmd, a, lcUser, signerID, lcUid, lcAttach, lcAttachForce, lcVerbose, output)
   427  					if err != nil {
   428  						return err
   429  					}
   430  				}
   431  			}
   432  		}
   433  
   434  		return nil
   435  	}
   436  
   437  	if output == "attachments" {
   438  		return fmt.Errorf("in order to download attachments, you need to be logged in on Codenotary Cloud®\nProceed by authenticating yourself using <vcn login>")
   439  	}
   440  	// blockchain context
   441  	org := viper.GetString("org")
   442  	var keys []string
   443  	if org != "" {
   444  		bo, err := api.GetBlockChainOrganisation(org)
   445  		if err != nil {
   446  			return err
   447  		}
   448  		keys = bo.MembersIDs()
   449  	} else {
   450  		keys = getSignerIDs()
   451  		// add 0x if missing, lower case, and check if format is correct
   452  		for i, k := range keys {
   453  			if !strings.HasPrefix(k, "0x") {
   454  				keys[i] = "0x" + k
   455  			}
   456  			keys[i] = strings.ToLower(keys[i])
   457  			if !keyRegExp.MatchString(keys[i]) {
   458  				return fmt.Errorf("invalid public address format: %s", k)
   459  			}
   460  		}
   461  	}
   462  
   463  	user := api.NewUser(store.Config().CurrentContext.Email)
   464  
   465  	// by alerts
   466  	if useAlerts {
   467  		if hasAuth, _ := user.IsAuthenticated(); !hasAuth {
   468  			return fmt.Errorf("in order to use --alerts, you need to be logged in\nProceed by authenticating yourself using <vcn login>")
   469  		}
   470  
   471  		alertConfigPath, err := store.AlertFilepath(user.Email())
   472  		if err != nil {
   473  			return err
   474  		}
   475  		if output == "" {
   476  			fmt.Printf("Using alert configuration: %s\n\n", alertConfigPath)
   477  		}
   478  
   479  		alerts, err := store.ReadAlerts(user.Email())
   480  		if err != nil {
   481  			return err
   482  		}
   483  
   484  		if len(alerts) == 0 {
   485  			return fmt.Errorf("no configured alerts")
   486  		}
   487  
   488  		for _, alert := range alerts {
   489  			var alertConfig api.AlertConfig
   490  			if err := alert.ExportConfig(&alertConfig); err != nil {
   491  				cli.PrintWarning(output, fmt.Sprintf(
   492  					`invalid alert config (name="%s") for %s: %s`,
   493  					alert.Name,
   494  					alert.Arg,
   495  					err,
   496  				))
   497  				continue
   498  			}
   499  			alertConfig.Metadata["arg"] = alert.Arg
   500  
   501  			artifacts, err := extractor.Extract([]string{alert.Arg})
   502  			if err != nil {
   503  				cli.PrintWarning(output, err.Error())
   504  				alertConfig.Metadata["error"] = err.Error()
   505  				user.TriggerAlert(alertConfig)
   506  				continue
   507  			}
   508  			if artifacts == nil {
   509  				cli.PrintWarning(output, fmt.Sprintf("unable to process the input asset provided: %s", alert.Arg))
   510  				alertConfig.Metadata["error"] = err.Error()
   511  				user.TriggerAlert(alertConfig)
   512  				continue
   513  			}
   514  			for _, a := range artifacts {
   515  				if err := verify(cmd, a, keys, org, user, &alertConfig, output); err != nil {
   516  					cli.PrintWarning(output, fmt.Sprintf("%s: %s", alert.Arg, err))
   517  				}
   518  				if output == "" {
   519  					fmt.Println()
   520  				}
   521  			}
   522  		}
   523  		return nil
   524  	}
   525  
   526  	// by hash
   527  	if hash != "" {
   528  		a := &api.Artifact{
   529  			Hash: strings.ToLower(hash),
   530  		}
   531  		if err := verify(cmd, a, keys, org, user, nil, output); err != nil {
   532  			return err
   533  		}
   534  		return nil
   535  	}
   536  
   537  	// by args
   538  	for _, arg := range args {
   539  		artifacts, err := extractor.Extract([]string{arg})
   540  		if err != nil {
   541  			return err
   542  		}
   543  		if artifacts == nil {
   544  			return fmt.Errorf("unable to process the input asset provided: %s", arg)
   545  		}
   546  		for _, a := range artifacts {
   547  			if err := verify(cmd, a, keys, org, user, nil, output); err != nil {
   548  				return err
   549  			}
   550  		}
   551  	}
   552  
   553  	return nil
   554  }
   555  
   556  func verify(cmd *cobra.Command, a *api.Artifact, keys []string, org string, user *api.User, alertConfig *api.AlertConfig, output string) (err error) {
   557  	hook := newHook(cmd, a)
   558  	var verification *api.BlockchainVerification
   559  	if output == "" {
   560  		fmt.Println()
   561  		color.Set(meta.StyleAffordance())
   562  		fmt.Println("Your assets will not be uploaded. They will be processed locally.")
   563  		color.Unset()
   564  		fmt.Println()
   565  	}
   566  	// if keys have been passed, check for a verification matching them
   567  	if len(keys) > 0 {
   568  		if output == "" {
   569  			if org == "" {
   570  				fmt.Printf("Looking for blockchain entry matching the passed SignerIDs...\n")
   571  			} else {
   572  				fmt.Printf("Looking for blockchain entry matching the organization (%s)...\n", org)
   573  			}
   574  		}
   575  		verification, err = api.VerifyMatchingSignerIDs(a.Hash, keys)
   576  
   577  	} else {
   578  		// if we have an user, check for verification matching user's key first
   579  		userKey := ""
   580  		if hasAuth, _ := user.IsAuthenticated(); hasAuth {
   581  			userKey, _ = user.SignerID() // todo(leogr): double check this
   582  		}
   583  		if userKey != "" {
   584  			if output == "" {
   585  				fmt.Printf("Looking for blockchain entry matching the current user (%s)...\n", user.Email())
   586  			}
   587  			verification, err = api.VerifyMatchingSignerIDWithFallback(a.Hash, userKey)
   588  			if output == "" {
   589  				if verification.SignerID() != userKey {
   590  					fmt.Printf("No blockchain entry matching the current user found.\n")
   591  					if !verification.Unknown() {
   592  						fmt.Printf("Showing the last blockchain entry with highest level available.\n")
   593  					}
   594  				}
   595  			}
   596  		} else {
   597  			// if no passed keys nor user,
   598  			// just get the last with highest level available verification
   599  			if output == "" {
   600  				fmt.Printf("Looking for the last blockchain entry with highest level available...\n")
   601  			}
   602  			verification, err = api.Verify(a.Hash)
   603  		}
   604  	}
   605  
   606  	if output == "" {
   607  		fmt.Println()
   608  	}
   609  
   610  	if err != nil {
   611  		return fmt.Errorf("unable to authenticate the hash: %s", err)
   612  	}
   613  
   614  	err = hook.finalize(alertConfig, output)
   615  	if err != nil {
   616  		return err
   617  	}
   618  
   619  	var ar *api.ArtifactResponse
   620  	if !verification.Unknown() {
   621  		ar, _ = api.LoadArtifact(user, a.Hash, verification.MetaHash())
   622  	}
   623  
   624  	if err = cli.Print(output, types.NewResult(a, ar, verification)); err != nil {
   625  		return err
   626  	}
   627  
   628  	if output != "" {
   629  		cmd.SilenceErrors = true
   630  	}
   631  
   632  	// todo(ameingast/leogr): remove reduntat event - need backend improvement
   633  	if verification.Trusted() {
   634  		api.TrackVerify(user, a.Hash, a.Name)
   635  	}
   636  
   637  	if alertConfig != nil {
   638  		var err error
   639  		if verification.Trusted() {
   640  			err = user.PingAlert(*alertConfig)
   641  		} else {
   642  			err = user.TriggerAlert(*alertConfig)
   643  		}
   644  		if err != nil {
   645  			return err
   646  		}
   647  
   648  		if output == "" {
   649  			fmt.Printf("\nPing for alert %s sent.\n", alertConfig.AlertUUID)
   650  		}
   651  		api.TrackPublisher(user, meta.VcnAlertVerifyEvent)
   652  	} else {
   653  		api.TrackPublisher(user, meta.VcnVerifyEvent)
   654  	}
   655  
   656  	if !verification.Trusted() {
   657  		errLabels := map[meta.Status]string{
   658  			meta.StatusUnknown:     "was not notarized",
   659  			meta.StatusUntrusted:   "is untrusted",
   660  			meta.StatusUnsupported: "is unsupported",
   661  		}
   662  
   663  		viper.Set("exit-code", strconv.Itoa(verification.Status.Int()))
   664  
   665  		switch true {
   666  		case org != "":
   667  			return fmt.Errorf(`%s %s by "%s"`, a.Hash, errLabels[verification.Status], org)
   668  		case len(keys) == 1:
   669  			return fmt.Errorf("%s %s by %s", a.Hash, errLabels[verification.Status], keys[0])
   670  		case len(keys) > 1:
   671  			return fmt.Errorf("%s %s by any of %s", a.Hash, errLabels[verification.Status], strings.Join(keys, ", "))
   672  		default:
   673  			return fmt.Errorf("%s %s", a.Hash, errLabels[verification.Status])
   674  		}
   675  	}
   676  
   677  	return
   678  }