github.com/vchain-us/vcn@v0.9.11-0.20210921212052-a2484d23c0b3/pkg/cmd/sign/sign.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 sign
    10  
    11  import (
    12  	"bufio"
    13  	"errors"
    14  	"fmt"
    15  	"os"
    16  	"path/filepath"
    17  	"strings"
    18  
    19  	"github.com/vchain-us/vcn/pkg/signature"
    20  
    21  	"github.com/caarlos0/spin"
    22  	"github.com/fatih/color"
    23  	"github.com/schollz/progressbar/v3"
    24  	"github.com/spf13/cobra"
    25  	"github.com/spf13/viper"
    26  	"github.com/vchain-us/vcn/pkg/extractor/wildcard"
    27  	"golang.org/x/crypto/ssh/terminal"
    28  
    29  	"github.com/vchain-us/ledger-compliance-go/schema"
    30  	"github.com/vchain-us/vcn/internal/assert"
    31  	vcnerr "github.com/vchain-us/vcn/internal/errors"
    32  	"github.com/vchain-us/vcn/pkg/api"
    33  	"github.com/vchain-us/vcn/pkg/bom"
    34  	"github.com/vchain-us/vcn/pkg/bom/artifact"
    35  	"github.com/vchain-us/vcn/pkg/bom/docker"
    36  	"github.com/vchain-us/vcn/pkg/cicontext"
    37  	"github.com/vchain-us/vcn/pkg/cmd/internal/cli"
    38  	"github.com/vchain-us/vcn/pkg/cmd/internal/types"
    39  	"github.com/vchain-us/vcn/pkg/cmd/verify"
    40  	"github.com/vchain-us/vcn/pkg/extractor"
    41  	"github.com/vchain-us/vcn/pkg/extractor/dir"
    42  	"github.com/vchain-us/vcn/pkg/meta"
    43  	"github.com/vchain-us/vcn/pkg/store"
    44  	"github.com/vchain-us/vcn/pkg/uri"
    45  )
    46  
    47  const longDescFooter = `
    48  
    49  VCN_NOTARIZATION_PASSWORD env var can be used to pass the
    50  required notarization password in a non-interactive environment.
    51  `
    52  
    53  const helpMsgFooter = `
    54  ARG must be one of:
    55    wildcard
    56    file
    57    directory
    58    file://<file>
    59    dir://<directory>
    60    git://<repository>
    61    docker://<image>
    62    podman://<image>
    63    wildcard://"*"
    64    javacom://<java project component>
    65    nodecom://<node component>
    66    gocom://<Go module in name@version format>
    67    pythoncom://<Python module in name@version format>
    68    dotnetcom://<.Net module in name@version format>
    69  `
    70  
    71  // NewCommand returns the cobra command for `vcn sign`
    72  func NewCommand() *cobra.Command {
    73  	cmd := makeCommand()
    74  	cmd.Flags().Bool("create-alert", false, "if set, an alert will be created (config will be stored into the .vcn dir)")
    75  	cmd.Flags().String("alert-name", "", "set the alert name (ignored if --create-alert is not set)")
    76  	cmd.Flags().String("alert-email", "", "set the alert email recipient (ignored if --create-alert is not set)")
    77  	cmd.Flags().Bool("bom", false, "auto-notarize asset dependencies and link dependencies to the asset")
    78  	cmd.Flags().String("bom-file", "", "use specified BoM file rather than resolve dependencies")
    79  	cmd.Flags().String("bom-signerID", "", "signerID to use for authenticating dependencies")
    80  	cmd.Flags().Bool("bom-deps-only", false, "notarize only the dependencies, not the asset")
    81  	cmd.Flags().StringSlice("bom-container-binary", []string{}, "list of binaries to be executed inside the container - only the relevant dependencies will be processed")
    82  	cmd.Flags().StringSlice("bom-hashes", []string{}, "hashes of the dependencies (disables automatic dependency resolution)")
    83  	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).")
    84  	// BoM output options
    85  	cmd.Flags().Bool("bom-debug", false, "show extra debug info for BoM processing, also disable progress indicators")
    86  	cmd.Flags().String("bom-spdx", "", "name of the file to output BoM in SPDX format")
    87  	cmd.Flags().String("bom-cyclonedx-json", "", "name of the file to output BoM in CycloneDX JSON format")
    88  	cmd.Flags().String("bom-cyclonedx-xml", "", "name of the file to output BoM in CycloneDX XML format")
    89  	return cmd
    90  }
    91  
    92  func makeCommand() *cobra.Command {
    93  	cmd := &cobra.Command{
    94  		Use:     "notarize",
    95  		Aliases: []string{"n", "sign", "s"},
    96  		Short:   "Notarize an asset onto the blockchain",
    97  		Long: `
    98  Notarize an asset onto the blockchain.
    99  
   100  Notarization calculates the SHA-256 hash of a digital asset
   101  (file, directory, container's image).
   102  The hash (not the asset) and the desired status of TRUSTED are then
   103  cryptographically signed by the signer's secret (private key).
   104  Next, these signed objects are sent to the blockchain where the signer’s
   105  trust level and a timestamp are added.
   106  When complete, a new blockchain entry is created that binds the asset’s
   107  signed hash, signed status, level, and timestamp together.
   108  
   109  Note that your assets will not be uploaded. They will be processed locally.
   110  
   111  Assets are referenced by passed ARG with notarization only accepting
   112  1 ARG at a time.
   113  
   114  Pipe mode:
   115  If '-' is provided (echo my-file | vcn n -) stdin is read and parsed. Only pipe ARGs are processed.
   116  
   117  Environment variables:
   118  VCN_USER=
   119  VCN_PASSWORD=
   120  VCN_NOTARIZATION_PASSWORD=
   121  VCN_NOTARIZATION_PASSWORD_EMPTY=
   122  VCN_OTP=
   123  VCN_OTP_EMPTY=
   124  VCN_LC_HOST=
   125  VCN_LC_PORT=
   126  VCN_LC_CERT=
   127  VCN_LC_SKIP_TLS_VERIFY=false
   128  VCN_LC_NO_TLS=false
   129  VCN_LC_API_KEY=
   130  VCN_SIGNING_PUB_KEY_FILE=
   131  VCN_SIGNING_PUB_KEY=
   132  VCN_ENFORCE_SIGNATURE_VERIFY=
   133  ` + helpMsgFooter,
   134  		PreRunE: func(cmd *cobra.Command, args []string) error {
   135  			return viper.BindPFlags(cmd.Flags())
   136  		},
   137  		RunE: func(cmd *cobra.Command, args []string) error {
   138  			if pipeMode() && len(args) > 0 && args[0] == "-" {
   139  				args = make([]string, 0)
   140  				scanner := bufio.NewScanner(os.Stdin)
   141  				scanner.Split(bufio.ScanWords)
   142  				for scanner.Scan() {
   143  					token := scanner.Bytes()
   144  					args = append(args, string(token))
   145  				}
   146  				if err := scanner.Err(); err != nil {
   147  					return fmt.Errorf("error parsing stdin input: %s", err)
   148  				}
   149  			}
   150  			return runSignWithState(cmd, args, meta.StatusTrusted)
   151  		},
   152  		Args: noArgsWhenHashOrPipe,
   153  		Example: `vcn notarize my-file"
   154  vcn notarize -r "*.md"
   155  echo my-file | vcn n -`,
   156  	}
   157  
   158  	cmd.Flags().VarP(make(mapOpts), "attr", "a", "add user defined attributes (repeat --attr for multiple entries). Special attributes: allowdownload=user1,user2 forbids attachments downloads for the CNIL specified user(name)s")
   159  	cmd.Flags().Bool("ci-attr", false, meta.VcnLcCIAttribDesc)
   160  	cmd.Flags().StringP("name", "n", "", "set the asset name")
   161  	cmd.Flags().BoolP("public", "p", false, "when notarized as public, the asset name and metadata will be visible to everyone")
   162  	cmd.Flags().String("hash", "", "specify the hash instead of using an asset, if set no ARG(s) can be used")
   163  	cmd.Flags().Bool("no-ignore-file", false, "if set, .vcnignore will be not written inside the targeted dir (affects dir:// only)")
   164  	cmd.Flags().Bool("read-only", false, "if set, no files will be written into the targeted dir (affects dir:// only)")
   165  	cmd.Flags().BoolP("recursive", "r", false, "if set, wildcard usage will walk inside subdirectories of provided path")
   166  	cmd.Flags().String("lc-host", "", meta.VcnLcHostFlagDesc)
   167  	cmd.Flags().String("lc-port", "443", meta.VcnLcPortFlagDesc)
   168  	cmd.Flags().String("lc-cert", "", meta.VcnLcCertPathDesc)
   169  	cmd.Flags().Bool("lc-skip-tls-verify", false, meta.VcnLcSkipTlsVerifyDesc)
   170  	cmd.Flags().Bool("lc-no-tls", false, meta.VcnLcNoTlsDesc)
   171  	cmd.Flags().String("lc-api-key", "", meta.VcnLcApiKeyDesc)
   172  	cmd.Flags().StringArray("attach", nil, meta.VcnLcAttachDesc)
   173  	cmd.Flags().Bool("bom-cascade", false, "cascade the operation to all assets that include the asset being processed")
   174  	cmd.Flags().Bool("bom-force", false, "force notarization of untrusted dependencies, force cascade operation")
   175  	cmd.Flags().Bool("bom-container-hash", false, "when --bom-container-binary is specified, use container image hash, not binary hash")
   176  	cmd.Flags().String("github-token", "", "Github OAuth token for querying BoM Github package details. Either authenticated or not, requests are subject to Github limits")
   177  
   178  	cmd.SetUsageTemplate(
   179  		strings.Replace(cmd.UsageTemplate(), "{{.UseLine}}", "{{.UseLine}} ARG", 1),
   180  	)
   181  	cmd.Flags().String("signing-pub-key-file", "", meta.VcnSigningPubKeyFileNameDesc)
   182  	cmd.Flags().String("signing-pub-key", "", meta.VcnSigningPubKeyDesc)
   183  	cmd.Flags().Bool("enforce-signature-verify", false, meta.VcnEnforceSignatureVerifyDesc)
   184  	cmd.Flags().Bool("compress", false, "store all specified attachments as a single ZIP archive")
   185  
   186  	return cmd
   187  }
   188  
   189  func runSignWithState(cmd *cobra.Command, args []string, state meta.Status) error {
   190  	// default extractors options
   191  	extractorOptions := []extractor.Option{}
   192  
   193  	noIgnoreFile, err := cmd.Flags().GetBool("no-ignore-file")
   194  	if err != nil {
   195  		return err
   196  	}
   197  	readOnly, err := cmd.Flags().GetBool("read-only")
   198  	if err != nil {
   199  		return err
   200  	}
   201  	if readOnly {
   202  		noIgnoreFile = true
   203  	}
   204  	if !noIgnoreFile {
   205  		extractorOptions = append(extractorOptions, dir.WithIgnoreFileInit())
   206  		extractorOptions = append(extractorOptions, dir.WithSkipIgnoreFileErr())
   207  	}
   208  
   209  	recursive, err := cmd.Flags().GetBool("recursive")
   210  	if err != nil {
   211  		return err
   212  	}
   213  	if recursive {
   214  		extractorOptions = append(extractorOptions, wildcard.WithRecursive())
   215  	}
   216  	var alert *alertOptions
   217  	if hasCreateAlert := cmd.Flags().Lookup("create-alert"); hasCreateAlert != nil {
   218  		createAlert, err := cmd.Flags().GetBool("create-alert")
   219  		if err != nil {
   220  			return err
   221  		}
   222  		if createAlert {
   223  			alert = &alertOptions{
   224  				arg: args[0],
   225  			}
   226  			alert.name, _ = cmd.Flags().GetString("alert-name")
   227  			if err != nil {
   228  				return err
   229  			}
   230  			alert.email, _ = cmd.Flags().GetString("alert-email")
   231  			if err != nil {
   232  				return err
   233  			}
   234  		}
   235  	}
   236  
   237  	var hash string
   238  	if hashFlag := cmd.Flags().Lookup("hash"); hashFlag != nil {
   239  		var err error
   240  		hash, err = cmd.Flags().GetString("hash")
   241  		if err != nil {
   242  			return err
   243  		}
   244  	}
   245  
   246  	public, err := cmd.Flags().GetBool("public")
   247  	if err != nil {
   248  		return err
   249  	}
   250  
   251  	output, err := cmd.Flags().GetString("output")
   252  	if err != nil {
   253  		return err
   254  	}
   255  
   256  	silentMode, err := cmd.Flags().GetBool("silent")
   257  	if err != nil {
   258  		return err
   259  	}
   260  
   261  	name, err := cmd.Flags().GetString("name")
   262  	if err != nil {
   263  		return err
   264  	}
   265  
   266  	metadata := cmd.Flags().Lookup("attr").Value.(mapOpts).StringToInterface()
   267  
   268  	// @todo use dependency injection
   269  	cs := cicontext.NewContextSaver()
   270  
   271  	if viper.GetBool("ci-attr") {
   272  		cicontext.ExtendMetadata(metadata, cs.GetCIContextMetadata())
   273  	}
   274  
   275  	cmd.SilenceUsage = true
   276  
   277  	lcHost := viper.GetString("lc-host")
   278  	lcPort := viper.GetString("lc-port")
   279  	lcCert := viper.GetString("lc-cert")
   280  	skipTlsVerify := viper.GetBool("lc-skip-tls-verify")
   281  	noTls := viper.GetBool("lc-no-tls")
   282  	lcApiKey := viper.GetString("lc-api-key")
   283  
   284  	lcVerbose := viper.GetBool("verbose")
   285  
   286  	signingPubKey, skipLocalPubKeyComp, err := signature.PrepareSignatureParams(
   287  		viper.GetString("signing-pub-key"),
   288  		viper.GetString("signing-pub-key-file"))
   289  	if err != nil {
   290  		return err
   291  	}
   292  	enforceSignatureVerify := viper.GetBool("enforce-signature-verify")
   293  
   294  	// todo add attachment validator. Deny ":" misuses in input string
   295  	attachments, err := cmd.Flags().GetStringArray("attach")
   296  	if err != nil {
   297  		return err
   298  	}
   299  
   300  	if lcApiKey == "" && lcHost != "" {
   301  		return vcnerr.ErrNoLcApiKeyEnv
   302  	}
   303  
   304  	//check if an lcUser is present inside the context
   305  	var lcUser *api.LcUser
   306  	uif, err := api.GetUserFromContext(store.Config().CurrentContext, lcApiKey, "", signingPubKey)
   307  	if err != nil {
   308  		return err
   309  	}
   310  	if lctmp, ok := uif.(*api.LcUser); ok {
   311  		lcUser = lctmp
   312  	}
   313  
   314  	// It uses flags for CNC context client constructor if at least host and apikey are provided
   315  	if lcHost != "" && lcApiKey != "" {
   316  		// client from context could be override by the one created from local flags
   317  		lcUser, err = api.NewLcUser(lcApiKey, "", lcHost, lcPort, lcCert, skipTlsVerify, noTls, signingPubKey)
   318  		if err != nil {
   319  			return err
   320  		} // Store the new config
   321  		if err := store.SaveConfig(); err != nil {
   322  			return err
   323  		}
   324  	}
   325  
   326  	// any set `--bom-xxx` option implies bom mode
   327  	bomFlag := viper.GetBool("bom") ||
   328  		viper.IsSet("bom-file") ||
   329  		viper.IsSet("bom-signerID") ||
   330  		viper.IsSet("bom-deps-only") ||
   331  		viper.IsSet("bom-spdx") ||
   332  		viper.IsSet("bom-cyclonedx-json") ||
   333  		viper.IsSet("bom-cyclonedx-xml") ||
   334  		viper.IsSet("bom-container-binary") ||
   335  		viper.IsSet("bom-container-hash") ||
   336  		viper.IsSet("bom-batch-size")
   337  
   338  	artifacts := make([]*api.Artifact, 0, 1)
   339  	if lcUser != nil {
   340  		err = lcUser.Client.Connect()
   341  		if err != nil {
   342  			return err
   343  		}
   344  
   345  		for attr := range metadata {
   346  			if attr == "allowdownload" {
   347  				err := lcUser.RequireFeatOrErr(schema.FeatAllowDownload)
   348  				if err != nil {
   349  					return err
   350  				}
   351  				break
   352  			}
   353  		}
   354  
   355  		if !skipLocalPubKeyComp {
   356  			err = lcUser.CheckConnectionPublicKey(enforceSignatureVerify)
   357  			if err != nil {
   358  				return err
   359  			}
   360  		}
   361  
   362  		var bomText string
   363  		bomFile := viper.GetString("bom-file")
   364  
   365  		if bomFlag {
   366  			err := lcUser.RequireFeatOrErr(schema.FeatBoM)
   367  			if err != nil {
   368  				return err
   369  			}
   370  		}
   371  		outputOpts := artifact.Progress
   372  		if viper.GetBool("silent") || output != "" {
   373  			outputOpts = artifact.Silent
   374  		} else if viper.GetBool("bom-debug") {
   375  			outputOpts = artifact.Debug
   376  		}
   377  
   378  		var bomArtifact artifact.Artifact
   379  		if bomFlag && !viper.IsSet("bom-hashes") {
   380  			// if bom-file specified, use BoM data from file, otherwise resolve dependencies
   381  			if bomFile == "" {
   382  				if len(args) != 1 {
   383  					return fmt.Errorf("--bom option can be used only with single asset")
   384  				}
   385  				path := args[0]
   386  				u, err := uri.Parse(path)
   387  				if err != nil {
   388  					return err
   389  				}
   390  				if _, ok := bom.BomSchemes[u.Scheme]; !ok {
   391  					return fmt.Errorf("unsupported URI %s for --bom option", path)
   392  				}
   393  				if u.Scheme != "" {
   394  					path = strings.TrimPrefix(u.Opaque, "//")
   395  				}
   396  				if u.Scheme == "docker" {
   397  					binaries := viper.GetStringSlice("bom-container-binary")
   398  					dockerArtifact, err := docker.New(path, binaries)
   399  					if err != nil {
   400  						return err
   401  					}
   402  					if !viper.GetBool("bom-container-hash") && len(binaries) > 0 {
   403  						// use binary hash rather than container hash
   404  						if len(binaries) != 1 {
   405  							return errors.New("cannot use binary hash when several binaries are specified. Use --bom-container-hash option to use container hash")
   406  						}
   407  						// setting hash disables hash calculation from container image
   408  						hash, err = dockerArtifact.FileHash(binaries[0])
   409  						if err != nil {
   410  							return err
   411  						}
   412  						name = filepath.Base(binaries[0])
   413  					}
   414  					bomArtifact = dockerArtifact
   415  				} else {
   416  					path, err = filepath.Abs(path)
   417  					if err != nil {
   418  						return err
   419  					}
   420  					bomArtifact = bom.New(path)
   421  				}
   422  				if bomArtifact == nil {
   423  					return fmt.Errorf("unsupported asset format/language")
   424  				}
   425  			} else {
   426  				bomArtifact, err = artifact.Load(bomFile)
   427  				if err != nil {
   428  					return fmt.Errorf("cannot load BoM from file: %w", err)
   429  				}
   430  			}
   431  
   432  			if outputOpts != artifact.Silent {
   433  				fmt.Printf("Resolving dependencies...\n")
   434  			}
   435  			deps, err := bomArtifact.ResolveDependencies(outputOpts)
   436  			if err != nil {
   437  				return fmt.Errorf("cannot get dependencies: %w", err)
   438  			}
   439  
   440  			bomBatchSize := int(viper.GetUint("bom-batch-size"))
   441  
   442  			bomText, err = notarizeDeps(lcUser, deps, outputOpts, bomArtifact.Type(), bomBatchSize)
   443  			if err != nil {
   444  				return err
   445  			}
   446  
   447  			if bomFile != "" {
   448  				// just to keep BoM file current
   449  				err = artifact.Store(bomArtifact, bomFile)
   450  				if err != nil {
   451  					// show warning, but not error, because notarization succeeded
   452  					fmt.Printf("Cannot store actual BoM: %v", err)
   453  				}
   454  			}
   455  
   456  			err = bom.Output(bomArtifact) // process all possible BoM output options
   457  			if err != nil {
   458  				// show warning, but not error, because authentication finished
   459  				fmt.Println(err)
   460  			}
   461  			if outputOpts != artifact.Silent {
   462  				artifact.Display(bomArtifact, artifact.ColNameVersion|artifact.ColHash|artifact.ColTrustLevel)
   463  			}
   464  		}
   465  
   466  		// Dependencies specified as hashes, not resolved
   467  		if viper.IsSet("bom-hashes") {
   468  			// TODO: Is it correct that BoM output is not allowed for hashes?
   469  			if viper.IsSet("bom-deps-only") ||
   470  				viper.IsSet("bom-spdx") ||
   471  				viper.IsSet("bom-cyclonedx-json") ||
   472  				viper.IsSet("bom-cyclonedx-xml") {
   473  				return fmt.Errorf("bom-hashes option cannot be combined with bom-deps-only or any BoM output option")
   474  			}
   475  			hashes := viper.GetStringSlice("bom-hashes")
   476  
   477  			signerID := viper.GetString("bom-signerID")
   478  			if signerID == "" {
   479  				signerID = api.GetSignerIDByApiKey(lcUser.Client.ApiKey)
   480  			}
   481  
   482  			bomText, err = bomHashes(lcUser, signerID, output, hashes)
   483  			if err != nil {
   484  				return err
   485  			}
   486  		}
   487  
   488  		// notarize the asset if not instructed otherwise
   489  		if !viper.GetBool("bom-deps-only") {
   490  			if hash != "" {
   491  				hash = strings.ToLower(hash)
   492  				// Load existing artifact, if any, otherwise use an empty artifact
   493  				if ar, _, err := lcUser.LoadArtifact(hash, "", "", 0, nil); err == nil && ar != nil {
   494  					artifacts = append(artifacts, &api.Artifact{
   495  						Kind:        ar.Kind,
   496  						Name:        ar.Name,
   497  						Hash:        ar.Hash,
   498  						Size:        ar.Size,
   499  						ContentType: ar.ContentType,
   500  						Metadata:    ar.Metadata,
   501  					})
   502  				} else {
   503  					if name == "" {
   504  						return fmt.Errorf("please set an asset name, by using --name")
   505  					}
   506  					artifacts = append(artifacts, &api.Artifact{Hash: hash})
   507  				}
   508  			} else {
   509  				artifacts, err = extractor.Extract(args, extractorOptions...)
   510  				if err != nil {
   511  					return err
   512  				}
   513  			}
   514  			err = LcSign(lcUser, artifacts, state, output, name, metadata, attachments, lcVerbose, bomText)
   515  			if err != nil {
   516  				return err
   517  			}
   518  		}
   519  
   520  		// cascade processing
   521  		if viper.GetBool("bom-cascade") && (state == meta.StatusUntrusted || state == meta.StatusUnsupported || state == meta.StatusTrusted) {
   522  			err = bomCascade(lcUser, artifacts, output, state, lcVerbose)
   523  			if err != nil {
   524  				return err
   525  			}
   526  		}
   527  
   528  		return nil
   529  	}
   530  
   531  	// User
   532  	if err := assert.UserLogin(); err != nil {
   533  		return err
   534  	}
   535  	u, ok := uif.(*api.User)
   536  	if !ok {
   537  		return fmt.Errorf("cannot load the current user")
   538  	}
   539  
   540  	// Make the artifact to be signed
   541  
   542  	if hash != "" {
   543  		if alert != nil {
   544  			return fmt.Errorf("cannot use --create-alert with --hash")
   545  		}
   546  		hash = strings.ToLower(hash)
   547  		// Load existing artifact, if any, otherwise use an empty artifact
   548  		if ar, err := u.LoadArtifact(hash); err == nil && ar != nil {
   549  			artifacts = []*api.Artifact{ar.Artifact()}
   550  		} else {
   551  			if name == "" {
   552  				return fmt.Errorf("please set an asset name, by using --name")
   553  			}
   554  			artifacts = []*api.Artifact{{Hash: hash}}
   555  		}
   556  	} else {
   557  		// Extract artifact from arg
   558  		artifacts, err = extractor.Extract(args, extractorOptions...)
   559  		if err != nil {
   560  			return err
   561  		}
   562  	}
   563  
   564  	if artifacts == nil {
   565  		return fmt.Errorf("unable to process the input asset provided")
   566  	}
   567  
   568  	if len(artifacts) == 1 {
   569  		// Override the asset's name, if provided by --name
   570  		if name != "" {
   571  			artifacts[0].Name = name
   572  		}
   573  	}
   574  
   575  	for _, a := range artifacts {
   576  		// Copy user provided custom attributes
   577  		a.Metadata.SetValues(metadata)
   578  
   579  		err := sign(*u, *a, state, meta.VisibilityForFlag(public), output, silentMode, readOnly, alert)
   580  		if err != nil {
   581  			return err
   582  		}
   583  	}
   584  
   585  	return nil
   586  }
   587  
   588  func sign(u api.User, a api.Artifact, state meta.Status, visibility meta.Visibility, output string, silent bool, readOnly bool, alert *alertOptions) error {
   589  
   590  	if output == "" {
   591  		color.Set(meta.StyleAffordance())
   592  		fmt.Print("Your assets will not be uploaded. They will be processed locally.")
   593  		color.Unset()
   594  		fmt.Println()
   595  		fmt.Println()
   596  		fmt.Println("Signer:\t" + u.Email())
   597  	}
   598  
   599  	hook := newHook(&a)
   600  
   601  	s := spin.New("%s Notarization in progress...")
   602  	s.Set(spin.Spin1)
   603  
   604  	var verification *api.BlockchainVerification
   605  	var err error
   606  
   607  	for i := 1; true; i++ {
   608  		var passphrase string
   609  		var interactive bool
   610  		passphrase, interactive, err = cli.ProvidePassphrase()
   611  		if err != nil {
   612  			return err
   613  		}
   614  
   615  		if output == "" && !silent {
   616  			s.Start()
   617  		}
   618  
   619  		var keyin string
   620  		var offline bool
   621  		keyin, _, offline, err = u.Secret()
   622  		if err != nil {
   623  			return err
   624  		}
   625  		if offline {
   626  			return fmt.Errorf("offline secret is not supported by the current vcn version")
   627  		}
   628  
   629  		verification, err = u.Sign(
   630  			a,
   631  			api.SignWithStatus(state),
   632  			api.SignWithVisibility(visibility),
   633  			api.SignWithKey(keyin, passphrase),
   634  		)
   635  
   636  		if err != nil && i >= 3 {
   637  			s.Stop()
   638  			return fmt.Errorf("too many failed attempts: %s", err)
   639  		}
   640  
   641  		if interactive && err == api.WrongPassphraseErr {
   642  			s.Stop()
   643  			fmt.Printf("\nError: %s, please try again\n\n", err.Error())
   644  			continue
   645  		}
   646  		break
   647  	}
   648  
   649  	s.Stop()
   650  
   651  	if err != nil {
   652  		return err
   653  	}
   654  
   655  	// once transaction is confirmed we don't want to show errors, just print warnings instead.
   656  
   657  	// todo(ameingast/leogr): remove redundant event - need backend improvement
   658  	api.TrackPublisher(&u, meta.VcnSignEvent)
   659  	api.TrackSign(&u, a.Hash, a.Name, state)
   660  
   661  	err = hook.finalize(verification, readOnly)
   662  	if err != nil {
   663  		return cli.PrintWarning(output, err.Error())
   664  	}
   665  
   666  	if output == "" {
   667  		fmt.Println()
   668  	}
   669  
   670  	artifact, err := api.LoadArtifact(&u, a.Hash, verification.MetaHash())
   671  	if err != nil {
   672  		return cli.PrintWarning(output, err.Error())
   673  	}
   674  
   675  	cli.Print(output, types.NewResult(&a, artifact, verification))
   676  
   677  	if alert != nil {
   678  		if err := handleAlert(alert, u, a, *verification, output); err != nil {
   679  			return cli.PrintWarning(output, err.Error())
   680  		}
   681  	}
   682  
   683  	return nil
   684  }
   685  
   686  func pipeMode() bool {
   687  	fileInfo, _ := os.Stdin.Stat()
   688  	return fileInfo.Mode()&os.ModeCharDevice == 0
   689  }
   690  
   691  func notarizeDeps(lcUser *api.LcUser, deps []artifact.Dependency, outputOpts artifact.OutputOptions, artType string, batchSize int) (string, error) {
   692  	if outputOpts != artifact.Silent {
   693  		fmt.Printf("Authenticating dependencies...\n")
   694  	}
   695  
   696  	signerID := viper.GetString("bom-signerID")
   697  	if signerID == "" {
   698  		signerID = api.GetSignerIDByApiKey(lcUser.Client.ApiKey)
   699  	}
   700  
   701  	force := viper.GetBool("bom-force")
   702  
   703  	var bar *progressbar.ProgressBar
   704  	if len(deps) > 1 && outputOpts == artifact.Progress {
   705  		bar = progressbar.Default(int64(len(deps)))
   706  	}
   707  
   708  	progressCallback := func(processedDeps []artifact.Dependency) {
   709  		switch outputOpts {
   710  		case artifact.Progress:
   711  			if bar != nil {
   712  				bar.Add(len(processedDeps))
   713  			}
   714  		case artifact.Debug:
   715  			for _, d := range processedDeps {
   716  				fmt.Printf("%s@%s (%s) - %s\n", d.Name, d.Version, d.Hash, artifact.TrustLevelName(d.TrustLevel))
   717  			}
   718  		}
   719  	}
   720  
   721  	errs, err := artifact.AuthenticateDependencies(lcUser, signerID, deps, batchSize, progressCallback)
   722  	if err != nil {
   723  		return "", fmt.Errorf("error authenticating dependencies: %w", err)
   724  	}
   725  
   726  	var msgs []string
   727  	var depsToNotarize []*artifact.Dependency
   728  	var kinds []string
   729  
   730  	for i := range deps { // Authenticate mutates the dependency, so use the index
   731  		if errs[i] != nil {
   732  			return "", fmt.Errorf("cannot authenticate %s@%s dependency: %w",
   733  				deps[i].SignerID, deps[i].Version, errs[i])
   734  		}
   735  		if deps[i].TrustLevel < artifact.Unknown {
   736  			msgs = append(msgs, fmt.Sprintf("Dependency %s@%s trust level is %s",
   737  				deps[i].Name, deps[i].Version, artifact.TrustLevelName(deps[i].TrustLevel)))
   738  		}
   739  		if deps[i].TrustLevel < artifact.Trusted {
   740  			depsToNotarize = append(depsToNotarize, &deps[i])
   741  			kinds = append(kinds, artType)
   742  		}
   743  	}
   744  
   745  	if len(msgs) > 0 && !force {
   746  		for _, msg := range msgs {
   747  			fmt.Println(msg)
   748  		}
   749  		return "", fmt.Errorf("some dependencies have insufficient trust level and cannot be automatically notarized. You can override it with --bom-force option")
   750  	}
   751  
   752  	// notarize only the dependencies first to make sure all needed keys are present into DB before
   753  	// adding key references to the index
   754  	// we don't get here if 'force' flag isn't set and some dependencies are untrusted
   755  	if len(depsToNotarize) > 0 {
   756  		var bar *progressbar.ProgressBar
   757  		if outputOpts != artifact.Silent {
   758  			ds := "dependencies"
   759  			if len(depsToNotarize) == 1 {
   760  				ds = "dependency"
   761  			}
   762  			fmt.Printf("Notarizing %d %s ...\n", len(depsToNotarize), ds)
   763  			if outputOpts == artifact.Progress {
   764  				bar = progressbar.Default(int64(len(depsToNotarize)))
   765  			}
   766  		}
   767  
   768  		progressCallbackN := func(processedDeps []*artifact.Dependency) {
   769  			switch outputOpts {
   770  			case artifact.Progress:
   771  				if bar != nil {
   772  					bar.Add(len(processedDeps))
   773  				}
   774  			case artifact.Debug:
   775  				for _, d := range processedDeps {
   776  					fmt.Printf("%s@%s (%s)\n", d.Name, d.Version, d.Hash)
   777  				}
   778  			}
   779  		}
   780  
   781  		errs, err = artifact.NotarizeDependencies(lcUser, kinds, depsToNotarize, batchSize, progressCallbackN)
   782  		if err != nil {
   783  			return "", fmt.Errorf("error notarizing dependencies: %w", err)
   784  		}
   785  		var errMsgs []string
   786  		for _, e := range errs {
   787  			if e != nil {
   788  				errMsgs = append(errMsgs, e.Error())
   789  			}
   790  		}
   791  		if len(errMsgs) > 0 {
   792  			return "", fmt.Errorf("error notarizing (some) dependencies: %s", strings.Join(errMsgs, ", "))
   793  		}
   794  	} else {
   795  		fmt.Printf("No dependencies require notarization\n")
   796  	}
   797  
   798  	var builder strings.Builder
   799  	for i := range deps {
   800  		// add dep key to BoM list for attaching
   801  		fmt.Fprintf(&builder, "vcn.%s.%s\n", deps[i].SignerID, deps[i].Hash)
   802  	}
   803  
   804  	return builder.String(), nil
   805  }
   806  
   807  func bomHashes(lcUser *api.LcUser, signerID string, output string, hashes []string) (string, error) {
   808  	var builder strings.Builder
   809  	if output == "" {
   810  		fmt.Println("Resolving hashes...")
   811  	}
   812  	for _, hash := range hashes {
   813  		ar, verified, err := lcUser.LoadArtifact(hash, signerID, "", 0, nil)
   814  		if err == api.ErrNotFound || ar.Status != meta.StatusTrusted {
   815  			return "", fmt.Errorf("you can only add BoM hashes of known trusted artifacts (please notarize the artifact using vcn notarize) before adding it to --bom-hashes")
   816  		}
   817  		if err != nil {
   818  			return "", err
   819  		}
   820  		if !verified {
   821  			return "", fmt.Errorf("the ledger is compromised")
   822  		}
   823  		if output == "" {
   824  			fmt.Printf("%s ==> %s", hash, ar.Name)
   825  			ver, ok := ar.Metadata.Get("version", "").(string)
   826  			if ok && ver != "" {
   827  				fmt.Printf("@%s", ver)
   828  			}
   829  			fmt.Println()
   830  		}
   831  
   832  		fmt.Fprintf(&builder, "vcn.%s.%s\n", signerID, hash)
   833  	}
   834  	return builder.String(), nil
   835  }
   836  
   837  func bomCascade(lcUser *api.LcUser, artifacts []*api.Artifact, output string, state meta.Status, lcVerbose bool) error {
   838  	signerID := api.GetSignerIDByApiKey(lcUser.Client.ApiKey)
   839  	toProcess := make([]api.PackageDetails, 0)
   840  	for _, ar := range artifacts {
   841  		included, err := verify.GetIncluded(ar.Hash, signerID, lcUser)
   842  		if err != nil {
   843  			return fmt.Errorf("error finding assets for %s: %w", ar.Name, err)
   844  		}
   845  		toProcess = append(toProcess, included...)
   846  	}
   847  	cascaded := make([]*api.Artifact, 0)
   848  	for _, asset := range toProcess {
   849  		if asset.Status == state {
   850  			continue
   851  		}
   852  		ar, _, err := lcUser.LoadArtifact(asset.Hash, "", "", 0, nil)
   853  		if err != nil {
   854  			return err
   855  		}
   856  		if ar == nil {
   857  			return fmt.Errorf("cannot resolve artifact by hash %s", asset.Hash)
   858  		}
   859  		cascaded = append(cascaded, &api.Artifact{
   860  			Kind:        ar.Kind,
   861  			Name:        ar.Name,
   862  			Hash:        ar.Hash,
   863  			Size:        ar.Size,
   864  			ContentType: ar.ContentType,
   865  			Metadata:    ar.Metadata,
   866  		})
   867  	}
   868  	if len(cascaded) > 0 && !viper.GetBool("bom-force") {
   869  		if !terminal.IsTerminal(int(os.Stdout.Fd())) {
   870  			return errors.New("run vcn interactively or specify --bom-force for cascade operations")
   871  		}
   872  		fmt.Printf("Following assets depend on assets being processed:\n")
   873  		for _, asset := range cascaded {
   874  			fmt.Printf("%s", asset.Name)
   875  			ver, ok := asset.Metadata.Get("version", "").(string)
   876  			if ok && ver != "" {
   877  				fmt.Printf("@%s", ver)
   878  			}
   879  			fmt.Printf(" (%s)\n", asset.Hash)
   880  		}
   881  		actionMap := map[meta.Status]string{
   882  			meta.StatusUntrusted:   "untrust",
   883  			meta.StatusUnsupported: "unsupport",
   884  			meta.StatusTrusted:     "notarize",
   885  		}
   886  		for {
   887  			fmt.Printf("Are you sure you want to %s these assets? (y/N)", actionMap[state])
   888  			var confirm string
   889  			_, err := fmt.Scanln(&confirm)
   890  			if err != nil {
   891  				return err
   892  			}
   893  			confirm = strings.ToLower(strings.TrimSuffix(confirm, "\n"))
   894  			if confirm == "y" {
   895  				break
   896  			}
   897  			if confirm == "n" || confirm == "" {
   898  				return errors.New("cascade operation aborted")
   899  			}
   900  			fmt.Println("please enter y or n")
   901  		}
   902  	}
   903  	err := LcSign(lcUser, cascaded, state, output, "", nil, nil, lcVerbose, "")
   904  	if err != nil {
   905  		return err
   906  	}
   907  	return nil
   908  }