From b8e9f5196a82f98c45d8451dab3c33d15b9b05ca Mon Sep 17 00:00:00 2001 From: Tom Meadows Date: Sun, 7 Apr 2024 15:06:28 +0100 Subject: [PATCH] Improvements / Changes to Link Attestor (#428) * adding flag logic for outfile --------- Signed-off-by: chaosinthecrd Signed-off-by: John Kjell Co-authored-by: John Kjell --- cmd/root_test.go | 5 +- cmd/run.go | 56 ++++- cmd/run_test.go | 504 ++++++++++++++++++++++++++++++++------------- cmd/verify_test.go | 8 +- docs/commands.md | 1 + options/run.go | 6 +- 6 files changed, 422 insertions(+), 158 deletions(-) diff --git a/cmd/root_test.go b/cmd/root_test.go index f458914d..0e971767 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -148,7 +148,6 @@ func rsakeypair(t *testing.T) (privatePem *os.File, publicPem *os.File) { } return privatePem, publicPem - } // ref: https://jamielinux.com/docs/openssl-certificate-authority/appendix/intermediate-configuration-file.html @@ -189,7 +188,7 @@ func fullChain(t *testing.T) (caPem *os.File, intermediatePems []*os.File, leafP t.Fatal(err) } - //common name must be different than the CA name + // common name must be different than the CA name intermediate := &x509.Certificate{ SerialNumber: big.NewInt(43), Subject: pkix.Name{ @@ -261,7 +260,6 @@ func fullChain(t *testing.T) (caPem *os.File, intermediatePems []*os.File, leafP } leafkeyPem, err = os.CreateTemp(workingDir, "leaf.key") - if err != nil { t.Fatal(err) } @@ -272,5 +270,4 @@ func fullChain(t *testing.T) (caPem *os.File, intermediatePems []*os.File, leafP } return caPem, intermediatePems, leafPem, leafkeyPem - } diff --git a/cmd/run.go b/cmd/run.go index ea33f8a1..62b1cafa 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -18,6 +18,7 @@ import ( "context" "encoding/json" "fmt" + "path/filepath" witness "github.com/in-toto/go-witness" "github.com/in-toto/go-witness/archivista" @@ -138,26 +139,59 @@ func runRun(ctx context.Context, ro options.RunOptions, args []string, signers . return err } - for _, result := range results { + for i, result := range results { signedBytes, err := json.Marshal(&result.SignedEnvelope) if err != nil { return fmt.Errorf("failed to marshal envelope: %w", err) } - // TODO: Find out explicit way to describe "prefix" in CLI options - outfile := ro.OutFilePath - if result.AttestorName != "" { - outfile += "-" + result.AttestorName + ".json" + var outfile string + // NOTE: This is a temporary fix until https://github.com/in-toto/witness/pull/350 is merged + if ro.OutFile != "" && ro.OutFilePath != "" { + return fmt.Errorf("cannot use both --outfile and --output") } + if ro.OutFile != "" { + log.Warn("--outfile is deprecated, please use --output instead") + if len(results) > 1 { + atts := "collection" + for _, r := range results { + if r.AttestorName != "" { + atts = fmt.Sprintf("%s, %s", atts, r.AttestorName) + } + } + return fmt.Errorf("multiple attestations (%s) were created but only one output file was specified", atts) + } + outfile = ro.OutFile + } else if ro.OutFilePath != "" { + var prefix string + if ro.OutFilePrefix != "" { + prefix = ro.OutFilePrefix + } else { + prefix = ro.StepName + } - out, err := loadOutfile(outfile) - if err != nil { - return fmt.Errorf("failed to open out file: %w", err) + if result.AttestorName != "" { + outfile = filepath.Join(ro.OutFilePath, fmt.Sprintf("%s.%s.json", prefix, result.AttestorName)) + } else if result.Collection.Name != "" { + outfile = filepath.Join(ro.OutFilePath, fmt.Sprintf("%s.collection.json", prefix)) + } + // We only want to warn the user wants so logging on the first iteration + } else if ro.OutFilePrefix != "" && i == 0 { + log.Warn("--output-prefix is ignored unless --output is set") } - defer out.Close() - if _, err := out.Write(signedBytes); err != nil { - return fmt.Errorf("failed to write envelope to out file: %w", err) + if outfile != "" { + out, err := loadOutfile(outfile) + if err != nil { + return fmt.Errorf("failed to open out file: %w", err) + } + defer out.Close() + + if _, err := out.Write(signedBytes); err != nil { + return fmt.Errorf("failed to write envelope to out file: %w", err) + } + + log.Info("attestation written to ", outfile) } if ro.ArchivistaOptions.Enable { diff --git a/cmd/run_test.go b/cmd/run_test.go index 01a8cb0e..dbe3f7ea 100644 --- a/cmd/run_test.go +++ b/cmd/run_test.go @@ -21,6 +21,7 @@ import ( "crypto/rsa" "encoding/json" "fmt" + logg "log" "os" "path/filepath" "strings" @@ -39,46 +40,37 @@ import ( ) func TestRunRSAKeyPair(t *testing.T) { - privatekey, err := rsa.GenerateKey(rand.Reader, keybits) - require.NoError(t, err) - signer := cryptoutil.NewRSASigner(privatekey, crypto.SHA256) - - workingDir := t.TempDir() - attestationPath := filepath.Join(workingDir, "outfile.txt") - runOptions := options.RunOptions{ - WorkingDir: workingDir, - Attestations: []string{}, - OutFilePath: attestationPath, - StepName: "teststep", - Tracing: false, - } - - args := []string{ - "bash", - "-c", - "echo 'test' > test.txt", + tests := []runTest{ + { + name: "Normal RSA Keypair Signing", + options: options.RunOptions{ + Attestations: []string{"environment"}, + StepName: "teststep", + Tracing: false, + }, + args: []string{}, + signers: []cryptoutil.Signer{}, + expectLogs: nil, + requireErr: "", + }, } - require.NoError(t, runRun(context.Background(), runOptions, args, signer)) - attestationBytes, err := os.ReadFile(attestationPath) - require.NoError(t, err) - env := dsse.Envelope{} - require.NoError(t, json.Unmarshal(attestationBytes, &env)) + testRun(t, tests) } func Test_runRunRSACA(t *testing.T) { - _, intermediates, leafcert, leafkey := fullChain(t) + _, intermediates, leafCert, leafKey := fullChain(t) signerOptions := options.SignerOptions{} signerOptions["file"] = []func(signer.SignerProvider) (signer.SignerProvider, error){ func(sp signer.SignerProvider) (signer.SignerProvider, error) { fsp := sp.(file.FileSignerProvider) - fsp.KeyPath = leafkey.Name() + fsp.KeyPath = leafKey.Name() fsp.IntermediatePaths = []string{intermediates[0].Name()} for _, intermediate := range intermediates { fsp.IntermediatePaths = append(fsp.IntermediatePaths, intermediate.Name()) } - fsp.CertPath = leafcert.Name() + fsp.CertPath = leafCert.Name() return fsp, nil }, } @@ -86,162 +78,398 @@ func Test_runRunRSACA(t *testing.T) { signers, err := loadSigners(context.Background(), signerOptions, options.KMSSignerProviderOptions{}, map[string]struct{}{"file": {}}) require.NoError(t, err) - workingDir := t.TempDir() - attestationPath := filepath.Join(workingDir, "outfile.txt") - runOptions := options.RunOptions{ - SignerOptions: signerOptions, - WorkingDir: workingDir, - Attestations: []string{}, - OutFilePath: attestationPath, - StepName: "teststep", - Tracing: false, - } - - args := []string{ - "bash", - "-c", - "echo 'test' > test.txt", - } - - require.NoError(t, runRun(context.Background(), runOptions, args, signers...)) - attestationBytes, err := os.ReadFile(attestationPath) - require.NoError(t, err) - assert.True(t, len(attestationBytes) > 0) - - env := dsse.Envelope{} - if err := json.Unmarshal(attestationBytes, &env); err != nil { - t.Errorf("Error reading envelope: %v", err) + tests := []runTest{ + { + name: "Valid hashes option", + options: options.RunOptions{ + SignerOptions: signerOptions, + Attestations: []string{}, + StepName: "teststep", + Tracing: false, + }, + args: []string{}, + signers: signers, + expectLogs: nil, + requireErr: "", + intermediate: intermediates[0], + leafCert: leafCert, + }, } - b, err := os.ReadFile(intermediates[0].Name()) - require.NoError(t, err) - assert.Equal(t, b, env.Signatures[0].Intermediates[0]) - - b, err = os.ReadFile(leafcert.Name()) - require.NoError(t, err) - assert.Equal(t, b, env.Signatures[0].Certificate) + testRun(t, tests) } func TestRunHashesOptions(t *testing.T) { - tests := []struct { - name string - hashesOption []string - expectErr bool - }{ + workingDir := t.TempDir() + tests := []runTest{ { - name: "Valid RSA key pair", - hashesOption: []string{"sha256"}, - expectErr: false, + name: "Valid hashes option", + options: options.RunOptions{ + Attestations: []string{"environment"}, + StepName: "teststep", + Hashes: []string{"sha256"}, + Tracing: false, + }, + args: []string{}, + signers: []cryptoutil.Signer{}, + expectLogs: nil, + requireErr: "", }, { - name: "Invalid hashes option", - hashesOption: []string{"invalidHash"}, - expectErr: true, + name: "Invalid hashes option", + options: options.RunOptions{ + Attestations: []string{"environment"}, + StepName: "teststep", + WorkingDir: workingDir, + OutFile: filepath.Join(workingDir, "outfile.txt"), + Hashes: []string{"invalidHash"}, + Tracing: false, + }, + args: []string{}, + signers: []cryptoutil.Signer{}, + requireErr: "failed to parse hash: unsupported hash function: invalidHash", }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - privatekey, err := rsa.GenerateKey(rand.Reader, keybits) - require.NoError(t, err) - signer := cryptoutil.NewRSASigner(privatekey, crypto.SHA256) + testRun(t, tests) +} - workingDir := t.TempDir() - attestationPath := filepath.Join(workingDir, "outfile.txt") - runOptions := options.RunOptions{ - WorkingDir: workingDir, - Attestations: []string{}, - Hashes: tt.hashesOption, - OutFilePath: attestationPath, +func TestRunOutputFileHandling(t *testing.T) { + tempDir := t.TempDir() + tests := []runTest{ + { + name: "OutFile specified", + options: options.RunOptions{ + WorkingDir: tempDir, + OutFile: filepath.Join(tempDir, "outfile.txt"), + Attestations: []string{"environment"}, StepName: "teststep", Tracing: false, - } - - args := []string{ - "bash", - "-c", - "echo 'test' > test.txt", - } - - err = runRun(context.Background(), runOptions, args, signer) - if tt.expectErr { - require.Error(t, err) - } else { - require.NoError(t, err) - attestationBytes, err := os.ReadFile(attestationPath) - require.NoError(t, err) - env := dsse.Envelope{} - require.NoError(t, json.Unmarshal(attestationBytes, &env)) - } - }) + }, + expectLogs: []tlog{ + { + level: logrus.WarnLevel, + message: "--outfile is deprecated, please use --output instead", + }, + }, + requireErr: "", + }, + { + name: "OutFilePath specified with default prefixes", + options: options.RunOptions{ + OutFilePath: tempDir, + Attestations: []string{"environment"}, + StepName: "teststep", + Tracing: false, + }, + expectLogs: []tlog{ + { + level: logrus.InfoLevel, + message: fmt.Sprintf("attestation written to %s/teststep.collection.json", tempDir), + }, + }, + unexpectLogs: []tlog{ + { + level: logrus.InfoLevel, + message: "--outfile is deprecated, please use --output instead", + }, + }, + requireErr: "", + }, + { + name: "OutFilePath specified with set prefix", + options: options.RunOptions{ + OutFilePath: tempDir, + OutFilePrefix: "super-secret-prefix", + Attestations: []string{"environment"}, + StepName: "teststep", + Tracing: false, + }, + expectLogs: []tlog{ + { + level: logrus.InfoLevel, + message: fmt.Sprintf("attestation written to %s/super-secret-prefix.collection.json", tempDir), + }, + }, + unexpectLogs: []tlog{ + { + level: logrus.InfoLevel, + message: "--outfile is deprecated, please use --output instead", + }, + }, + requireErr: "", + }, + { + name: "OutFilePath specified with set prefix and exported attestations", + options: options.RunOptions{ + OutFilePath: tempDir, + Attestations: []string{"environment", "slsa", "link"}, + StepName: "teststep", + Tracing: false, + }, + exportedAtts: []string{"slsa", "link"}, + expectLogs: []tlog{ + { + level: logrus.InfoLevel, + message: fmt.Sprintf("attestation written to %s/teststep.collection.json", tempDir), + }, + { + level: logrus.InfoLevel, + message: fmt.Sprintf("attestation written to %s/teststep.slsa.json", tempDir), + }, + { + level: logrus.InfoLevel, + message: fmt.Sprintf("attestation written to %s/teststep.link.json", tempDir), + }, + }, + unexpectLogs: []tlog{ + { + level: logrus.InfoLevel, + message: "--outfile is deprecated, please use --output instead", + }, + }, + requireErr: "", + }, + { + name: "OutFilePath specified with set prefix and exported attestations", + options: options.RunOptions{ + OutFilePath: tempDir, + OutFilePrefix: "super-secret-prefix", + Attestations: []string{"environment", "slsa", "link"}, + StepName: "teststep", + Tracing: false, + }, + exportedAtts: []string{"slsa", "link"}, + expectLogs: []tlog{ + { + level: logrus.InfoLevel, + message: fmt.Sprintf("attestation written to %s/super-secret-prefix.collection.json", tempDir), + }, + { + level: logrus.InfoLevel, + message: fmt.Sprintf("attestation written to %s/super-secret-prefix.slsa.json", tempDir), + }, + { + level: logrus.InfoLevel, + message: fmt.Sprintf("attestation written to %s/super-secret-prefix.link.json", tempDir), + }, + }, + unexpectLogs: []tlog{ + { + level: logrus.InfoLevel, + message: "--outfile is deprecated, please use --output instead", + }, + }, + requireErr: "", + }, + { + name: "OutFilePath specified with invalid prefix", + options: options.RunOptions{ + OutFilePath: tempDir, + OutFilePrefix: "(&*^*&TYUTY)///$$$#", + Attestations: []string{"environment"}, + StepName: "teststep", + Tracing: false, + }, + requireErr: fmt.Sprintf("failed to open out file: failed to create output file: open %s/(&*^*&TYUTY)/$$$#.collection.json: no such file or directory", tempDir), + }, + { + name: "Both old and new options specified", + options: options.RunOptions{ + WorkingDir: tempDir, + OutFile: filepath.Join(tempDir, "outfile.txt"), + OutFilePath: tempDir, + Attestations: []string{"environment"}, + StepName: "teststep", + Tracing: false, + }, + expectLogs: nil, + requireErr: "cannot use both --outfile and --output", + }, } + + testRun(t, tests) } func TestRunDuplicateAttestors(t *testing.T) { - tests := []struct { - name string - attestors []string - expectWarn int - }{ + tests := []runTest{ { - name: "No duplicate attestors", - attestors: []string{"environment"}, - expectWarn: 0, + name: "No duplicate attestors", + options: options.RunOptions{ + Attestations: []string{"environment"}, + StepName: "teststep", + Tracing: false, + }, + args: []string{}, + signers: []cryptoutil.Signer{}, + expectLogs: nil, + requireErr: "", }, { - name: "duplicate attestors", - attestors: []string{"environment", "environment"}, - expectWarn: 1, + name: "Duplicate attestors", + options: options.RunOptions{ + Attestations: []string{"environment", "environment"}, + StepName: "teststep", + Tracing: false, + }, + args: []string{}, + signers: []cryptoutil.Signer{}, + expectLogs: []tlog{ + { + level: logrus.WarnLevel, + message: "Attestor environment already declared, skipping", + }, + }, + requireErr: "", }, { - name: "duplicate attestor due to default", - attestors: []string{"product"}, - expectWarn: 1, + name: "Duplicate attestor due to default", + options: options.RunOptions{ + Attestations: []string{"product"}, + StepName: "teststep", + Tracing: false, + }, + args: []string{}, + signers: []cryptoutil.Signer{}, + expectLogs: []tlog{ + { + level: logrus.WarnLevel, + message: "Attestor product already declared, skipping", + }, + }, + requireErr: "", }, } + testRun(t, tests) +} + +type runTest struct { + name string + options options.RunOptions + args []string + signers []cryptoutil.Signer + intermediate *os.File + leafCert *os.File + expectLogs []tlog + unexpectLogs []tlog + requireErr string + exportedAtts []string +} + +type tlog struct { + level logrus.Level + message string +} + +func testRun(t *testing.T, tests []runTest) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { fmt.Println(tt.name) testLogger, hook := test.NewNullLogger() log.SetLogger(testLogger) - privatekey, err := rsa.GenerateKey(rand.Reader, keybits) - require.NoError(t, err) - signer := cryptoutil.NewRSASigner(privatekey, crypto.SHA256) + ss := tt.signers + if len(ss) == 0 { + privatekey, err := rsa.GenerateKey(rand.Reader, keybits) + require.NoError(t, err) + ss = []cryptoutil.Signer{cryptoutil.NewRSASigner(privatekey, crypto.SHA256)} + } - workingDir := t.TempDir() - attestationPath := filepath.Join(workingDir, "outfile.txt") - runOptions := options.RunOptions{ - WorkingDir: workingDir, - Attestations: tt.attestors, - OutFilePath: attestationPath, - StepName: "teststep", - Tracing: false, + args := tt.args + if len(args) == 0 { + args = []string{ + "bash", + "-c", + "echo 'test' > test.txt", + } } - args := []string{ - "bash", - "-c", - "echo 'test' > test.txt", + var attestationPaths []string + if tt.options.OutFile != "" && tt.options.WorkingDir == "" { + logg.Fatal("test error: WorkingDir must be set if OutFile is set for tests") + } else if tt.options.OutFile != "" { + if !strings.Contains(tt.options.OutFile, tt.options.WorkingDir) { + logg.Fatal("test error: OutFile must be a full path inside the working directory") + } + attestationPaths = []string{tt.options.OutFile} + } else { + if tt.options.WorkingDir == "" { + tt.options.WorkingDir = t.TempDir() + } + + if tt.options.OutFilePath == "" { + tt.options.OutFilePath = tt.options.WorkingDir + } + + var prefix string + if tt.options.OutFilePrefix != "" { + prefix = tt.options.OutFilePrefix + } else { + prefix = tt.options.StepName + } + attestationPaths = []string{filepath.Join(tt.options.OutFilePath, fmt.Sprintf("%s.collection.json", prefix))} + for _, att := range tt.exportedAtts { + attestationPaths = append(attestationPaths, filepath.Join(tt.options.OutFilePath, fmt.Sprintf("%s.%s.json", prefix, att))) + } } - err = runRun(context.Background(), runOptions, args, signer) - if tt.expectWarn > 0 { - c := 0 + err := runRun(context.Background(), tt.options, args, ss...) + var logs []tlog + if len(tt.expectLogs) > 0 { for _, entry := range hook.AllEntries() { - fmt.Println(tt.name, "log:", entry.Message) - if entry.Level == logrus.WarnLevel && strings.Contains(entry.Message, "already declared, skipping") { - c++ - } + logs = append(logs, tlog{level: entry.Level, message: entry.Message}) + } + + for _, l := range tt.expectLogs { + assert.Contains(t, logs, l) + } + } + if len(tt.unexpectLogs) > 0 { + for _, l := range tt.unexpectLogs { + assert.NotContains(t, logs, l) } - assert.Equal(t, tt.expectWarn, c) + } + + if tt.requireErr != "" && err != nil { + assert.Equal(t, tt.requireErr, err.Error()) + return } else { require.NoError(t, err) + } + // NOTE: For tests, make sure to set the OutFile to the entire path of the file + for _, attestationPath := range attestationPaths { attestationBytes, err := os.ReadFile(attestationPath) - require.NoError(t, err) + if tt.requireErr != "" && err != nil { + assert.Equal(t, tt.requireErr, err.Error()) + return + } else { + require.NoError(t, err) + } env := dsse.Envelope{} require.NoError(t, json.Unmarshal(attestationBytes, &env)) + if tt.intermediate != nil && tt.leafCert != nil { + b, err := os.ReadFile(tt.intermediate.Name()) + if tt.requireErr != "" && err != nil { + assert.Equal(t, tt.requireErr, err.Error()) + return + } else { + require.NoError(t, err) + } + assert.Equal(t, b, env.Signatures[0].Intermediates[0]) + + b, err = os.ReadFile(tt.leafCert.Name()) + if tt.requireErr != "" && err != nil { + assert.Equal(t, tt.requireErr, err.Error()) + return + } else { + require.NoError(t, err) + } + assert.Equal(t, b, env.Signatures[0].Certificate) + } + } + if tt.requireErr != "" { + logg.Fatalf("expected error in test: %s", tt.requireErr) } }) } diff --git a/cmd/verify_test.go b/cmd/verify_test.go index 461f8b8a..0f88d4bf 100644 --- a/cmd/verify_test.go +++ b/cmd/verify_test.go @@ -84,7 +84,7 @@ func TestRunVerifyCA(t *testing.T) { SignerOptions: so, WorkingDir: workingDir, Attestations: []string{}, - OutFilePath: s1FilePath, + OutFile: s1FilePath, StepName: "step01", Tracing: false, } @@ -110,7 +110,7 @@ func TestRunVerifyCA(t *testing.T) { SignerOptions: so, WorkingDir: workingDir, Attestations: []string{}, - OutFilePath: s2FilePath, + OutFile: s2FilePath, StepName: "step02", Tracing: false, } @@ -182,7 +182,7 @@ func TestRunVerifyKeyPair(t *testing.T) { SignerOptions: so, WorkingDir: workingDir, Attestations: []string{}, - OutFilePath: s1FilePath, + OutFile: s1FilePath, StepName: "step01", Tracing: false, } @@ -208,7 +208,7 @@ func TestRunVerifyKeyPair(t *testing.T) { SignerOptions: so, WorkingDir: workingDir, Attestations: []string{}, - OutFilePath: s2FilePath, + OutFile: s2FilePath, StepName: "step02", Tracing: false, } diff --git a/docs/commands.md b/docs/commands.md index fd41b6bf..1d974528 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -44,6 +44,7 @@ witness run [cmd] [flags] ``` --archivista-server string URL of the Archivista server to store or retrieve attestations (default "https://archivista.testifysec.io") -a, --attestations strings Attestations to record ('product' and 'material' are always recorded) (default [environment,git]) + --attestor-link-export Export the link attestation to its own file --attestor-maven-pom-path string The path to the Project Object Model (POM) XML file used for task being attested (default "pom.xml"). (default "pom.xml") --attestor-product-exclude-glob string Pattern to use when recording products. Files that match this pattern will be excluded as subjects on the attestation. --attestor-product-include-glob string Pattern to use when recording products. Files that match this pattern will be included as subjects on the attestation. (default "*") diff --git a/options/run.go b/options/run.go index 00f38ace..121d7511 100644 --- a/options/run.go +++ b/options/run.go @@ -29,7 +29,9 @@ type RunOptions struct { WorkingDir string Attestations []string Hashes []string + OutFile string OutFilePath string + OutFilePrefix string StepName string Tracing bool TimestampServers []string @@ -42,7 +44,9 @@ func (ro *RunOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().StringVarP(&ro.WorkingDir, "workingdir", "d", "", "Directory from which commands will run") cmd.Flags().StringSliceVarP(&ro.Attestations, "attestations", "a", DefaultAttestors, "Attestations to record ('product' and 'material' are always recorded)") cmd.Flags().StringSliceVar(&ro.Hashes, "hashes", []string{"sha256"}, "Hashes selected for digest calculation. Defaults to SHA256") - cmd.Flags().StringVarP(&ro.OutFilePath, "outfile", "o", "", "File to which to write signed data. Defaults to stdout") + cmd.Flags().StringVarP(&ro.OutFile, "outfile", "o", "", "(Deprecated for --output) File to which to write signed data. Defaults to stdout") + cmd.Flags().StringVar(&ro.OutFilePath, "output", "", "The filepath to output the attestations to. The names of the files will follow a standard scheme unless --output-prefix is specified") + cmd.Flags().StringVar(&ro.OutFilePrefix, "output-prefix", "", "The prefix to use for the output file names. If not specified, the step name will be used") cmd.Flags().StringVarP(&ro.StepName, "step", "s", "", "Name of the step being run") cmd.Flags().BoolVar(&ro.Tracing, "trace", false, "Enable tracing for the command") cmd.Flags().StringSliceVar(&ro.TimestampServers, "timestamp-servers", []string{}, "Timestamp Authority Servers to use when signing envelope")