diff --git a/.goreleaser.yml b/.goreleaser.yml index 08a0359a..f54dbabb 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -9,7 +9,7 @@ before: # - go generate ./... builds: - - + - id: default env: - CGO_ENABLED=0 @@ -20,6 +20,7 @@ builds: - linux_386 - linux_amd64 - linux_arm64 + - linux_arm_5 - linux_arm_6 - linux_arm_7 - linux_mips @@ -78,7 +79,7 @@ nfpms: # Useful tools for debugging .debs: # List file contents: dpkg -c dist/step_...deb # Package metadata: dpkg --info dist/step_....deb - # + # - builds: - nfpm @@ -89,7 +90,7 @@ nfpms: maintainer: Smallstep description: > step-cli lets you build, operate, and automate Public Key Infrastructure (PKI) systems and workflows. - + It's a swiss army knife for authenticated encryption (X.509, TLS), single sign-on (OAuth OIDC, SAML), multi-factor authentication (OATH OTP, FIDO U2F), encryption mechanisms (JSON Web Encryption, NaCl), and verifiable claims (JWT, SAML assertions). license: Apache 2.0 section: utils @@ -104,20 +105,14 @@ nfpms: dst: /usr/share/bash-completion/completions/step-cli - src: debian/copyright dst: /usr/share/doc/step-cli/copyright - overrides: - deb: - scripts: - postinstall: "debian/postinstall.sh" - preremove: "debian/preremove.sh" - rpm: - contents: - - src: autocomplete/bash_autocomplete - dst: /usr/share/bash-completion/completions/step-cli - - src: debian/copyright - dst: /usr/share/doc/step-cli/copyright - - src: /usr/bin/step-cli - dst: /usr/bin/step - type: "symlink" + # Ghost files are used for RPM and ignored elsewhere + - dst: /usr/bin/step + type: ghost + - dst: /usr/share/bash-completion/completions/step + type: ghost + scripts: + postinstall: scripts/postinstall.sh + postremove: scripts/postremove.sh source: diff --git a/CHANGELOG.md b/CHANGELOG.md index ac0a0596..433623bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Add `--format` flag to `step crypto key fingerprint`. - Add `--format` flag to `step ssh fingerprint`. - Add FreeBSD support to `step certificate install`. +- Add `step crl inspect` to inspect a certificate revocation list (CRL). +- Add `--auth-param` flag to `step oauth` for adding args to query +- Add `--no-agent` flag to `step ssh certificate` to skip ssh-add ### Changed ### Deprecated ### Removed diff --git a/cmd/step/main.go b/cmd/step/main.go index 6fb2c820..0f9da0df 100644 --- a/cmd/step/main.go +++ b/cmd/step/main.go @@ -25,6 +25,7 @@ import ( _ "github.com/smallstep/cli/command/ca" _ "github.com/smallstep/cli/command/certificate" _ "github.com/smallstep/cli/command/context" + _ "github.com/smallstep/cli/command/crl" _ "github.com/smallstep/cli/command/crypto" _ "github.com/smallstep/cli/command/fileserver" _ "github.com/smallstep/cli/command/oauth" diff --git a/command/ca/acme/acme.go b/command/ca/acme/acme.go new file mode 100644 index 00000000..2d02cd23 --- /dev/null +++ b/command/ca/acme/acme.go @@ -0,0 +1,19 @@ +package acme + +import ( + "github.com/smallstep/cli/command/ca/acme/eab" + "github.com/urfave/cli" +) + +// Command returns the acme subcommand. +func Command() cli.Command { + return cli.Command{ + Name: "acme", + Usage: "manage ACME", + UsageText: "**step beta ca acme** [arguments] [global-flags] [subcommand-flags]", + Description: `**step beta ca acme** command group provides facilities for managing ACME.`, + Subcommands: cli.Commands{ + eab.Command(), + }, + } +} diff --git a/command/ca/acme/eab/add.go b/command/ca/acme/eab/add.go new file mode 100644 index 00000000..5275ffe7 --- /dev/null +++ b/command/ca/acme/eab/add.go @@ -0,0 +1,94 @@ +package eab + +import ( + "fmt" + "os" + + "github.com/pkg/errors" + adminAPI "github.com/smallstep/certificates/authority/admin/api" + "github.com/smallstep/cli/flags" + "github.com/smallstep/cli/utils/cautils" + "github.com/urfave/cli" + "go.step.sm/cli-utils/errs" +) + +func addCommand() cli.Command { + return cli.Command{ + Name: "add", + Action: cli.ActionFunc(addAction), + Usage: "add ACME External Account Binding Key", + UsageText: `**step beta ca acme eab add** [] +[**--admin-cert**=] [**--admin-key**=] +[**--admin-provisioner**=] [**--admin-subject**=] +[**--password-file**=] [**--ca-url**=] [**--root**=] +[**--context**=]`, + Flags: []cli.Flag{ + flags.AdminCert, + flags.AdminKey, + flags.AdminProvisioner, + flags.AdminSubject, + flags.PasswordFile, + flags.CaURL, + flags.Root, + flags.Context, + }, + Description: `**step beta ca acme eab add** adds ACME External Account Binding Key. + +## POSITIONAL ARGUMENTS + + +: Name of the provisioner to which the ACME EAB key will be added + + +: (Optional) reference (from external system) for the key that will be added + +## EXAMPLES + +Add an ACME External Account Binding Key without reference: +''' +$ step beta ca acme eab add my_acme_provisioner +''' + +Add an ACME External Account Binding Key with reference: +''' +$ step beta ca acme eab add my_acme_provisioner my_first_eab_key +'''`, + } +} + +func addAction(ctx *cli.Context) (err error) { + if err := errs.MinMaxNumberOfArguments(ctx, 1, 2); err != nil { + return err + } + + args := ctx.Args() + provisioner := args.Get(0) + + reference := "" + if ctx.NArg() == 2 { + reference = args.Get(1) + } + + client, err := cautils.NewAdminClient(ctx) + if err != nil { + return errors.Wrap(err, "error creating admin client") + } + + eak, err := client.CreateExternalAccountKey(provisioner, &adminAPI.CreateExternalAccountKeyRequest{ + Reference: reference, + }) + if err != nil { + return errors.Wrap(err, "error creating ACME EAB key") + } + + cliEAK := toCLI(ctx, client, eak) + + // TODO(hs): JSON output, so that executing this command can be more easily automated? + + out := os.Stdout + format := "%-36s%-28s%-48s%s\n" + fmt.Fprintf(out, format, "Key ID", "Provisioner", "Key (base64, raw url encoded)", "Reference") + fmt.Fprintf(out, format, cliEAK.id, cliEAK.provisioner, cliEAK.key, cliEAK.reference) + + return nil +} diff --git a/command/ca/acme/eab/eab.go b/command/ca/acme/eab/eab.go new file mode 100644 index 00000000..5fecbcfb --- /dev/null +++ b/command/ca/acme/eab/eab.go @@ -0,0 +1,69 @@ +package eab + +import ( + "encoding/base64" + + "github.com/smallstep/certificates/ca" + "github.com/urfave/cli" + "go.step.sm/linkedca" +) + +type cliEAK struct { + id string + provisioner string + reference string + key string + createdAt string + boundAt string + account string +} + +func toCLI(ctx *cli.Context, client *ca.AdminClient, eak *linkedca.EABKey) *cliEAK { + boundAt := "" + if !eak.BoundAt.AsTime().IsZero() { + boundAt = eak.BoundAt.AsTime().Format("2006-01-02 15:04:05 -07:00") + } + return &cliEAK{ + id: eak.Id, + provisioner: eak.Provisioner, + reference: eak.Reference, + key: base64.RawURLEncoding.Strict().EncodeToString(eak.HmacKey), + createdAt: eak.CreatedAt.AsTime().Format("2006-01-02 15:04:05 -07:00"), + boundAt: boundAt, + account: eak.Account, + } +} + +// Command returns the eab subcommand. +func Command() cli.Command { + return cli.Command{ + Name: "eab", + Usage: "create and manage ACME External Account Binding Keys", + UsageText: "**step beta ca acme eab** [arguments] [global-flags] [subcommand-flags]", + Subcommands: cli.Commands{ + listCommand(), + addCommand(), + removeCommand(), + }, + Description: `**step beta ca acme eab** command group provides facilities for managing ACME + External Account Binding Keys. + +## EXAMPLES + +List the active ACME External Account Binding Keys: +''' +$ step beta ca acme eab list +''' + +Add an ACME External Account Binding Key: +''' +$ step beta ca acme eab add provisioner_name some_name_or_reference +''' + +Remove an ACME External Account Binding Key: +''' +$ step beta ca acme eab remove key_id +''' +`, + } +} diff --git a/command/ca/acme/eab/list.go b/command/ca/acme/eab/list.go new file mode 100644 index 00000000..43ac07ee --- /dev/null +++ b/command/ca/acme/eab/list.go @@ -0,0 +1,178 @@ +package eab + +import ( + "fmt" + "io" + "os" + "os/exec" + "os/signal" + "syscall" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/ca" + "github.com/smallstep/cli/flags" + "github.com/smallstep/cli/utils/cautils" + "github.com/urfave/cli" + "go.step.sm/cli-utils/errs" +) + +func listCommand() cli.Command { + return cli.Command{ + Name: "list", + Action: cli.ActionFunc(listAction), + Usage: "list all ACME External Account Binding Keys", + UsageText: `**step beta ca acme eab list** [] +[**--limit**=] [**--admin-cert**=] [**--admin-key**=] +[**--admin-provisioner**=] [**--admin-subject**=] +[**--password-file**=] [**--ca-url**=] [**--root**=] +[**--context**=]`, + Flags: []cli.Flag{ + flags.Limit, + flags.NoPager, + flags.AdminCert, + flags.AdminKey, + flags.AdminProvisioner, + flags.AdminSubject, + flags.PasswordFile, + flags.CaURL, + flags.Root, + flags.Context, + }, + Description: `**step beta ca acme eab list** lists all ACME External Account Binding (EAB) Keys. + +Output will go to stdout by default. If many EAB keys are stored in the ACME provisioner, output will be sent to $PAGER (when set). + +## POSITIONAL ARGUMENTS + + +: Name of the provisioner to list ACME EAB keys for + + +: (Optional) reference (from external system) for the key to be listed + + +## EXAMPLES + +List all ACME External Account Binding Keys: +''' +$ step beta ca acme eab list my_acme_provisioner +''' + +Show ACME External Account Binding Key with specific reference: +''' +$ step beta ca acme eab list my_acme_provisioner my_reference +''' +`, + } +} + +func listAction(ctx *cli.Context) (err error) { + if err := errs.MinMaxNumberOfArguments(ctx, 1, 2); err != nil { + return err + } + + args := ctx.Args() + provisioner := args.Get(0) + + reference := "" + if ctx.NArg() == 2 { + reference = args.Get(1) + } + + client, err := cautils.NewAdminClient(ctx) + if err != nil { + return errors.Wrap(err, "error creating admin client") + } + + var out io.WriteCloser + var cmd *exec.Cmd + + usePager := true + if ctx.IsSet("no-pager") { + usePager = !ctx.Bool("no-pager") + } + + // the pipeSignalHandler goroutine ensures that the parent process is closed + // whenever one of its children is killed. + go pipeSignalHandler() + + // prepare the $PAGER command to run when not disabled and when available + pager := os.Getenv("PAGER") + if usePager && pager != "" { + cmd = exec.Command(pager) + var err error + out, err = cmd.StdinPipe() + if err != nil { + return errors.Wrap(err, "error setting stdin") + } + defer out.Close() + cmd.Stdout = os.Stdout + } else { + out = os.Stdout + } + + // default to API paging per 100 entities + limit := uint(0) + if ctx.IsSet("limit") { + limit = ctx.Uint("limit") + } + + cursor := "" + format := "%-36s%-28s%-16s%-30s%-30s%-36s%s\n" + firstIteration := true + startedPager := false + + for { + options := []ca.AdminOption{ca.WithAdminCursor(cursor), ca.WithAdminLimit(int(limit))} + eaksResponse, err := client.GetExternalAccountKeysPaginate(provisioner, reference, options...) + if err != nil { + return errors.Wrap(err, "error retrieving ACME EAB keys") + } + if firstIteration && len(eaksResponse.EAKs) == 0 { + fmt.Printf("No ACME EAB keys stored for provisioner %s\n", provisioner) + break + } + if firstIteration && cmd != nil { + if err := cmd.Start(); err != nil { + return errors.Wrap(err, "unable to start $PAGER") + } + startedPager = true + } + if firstIteration { + fmt.Fprintf(out, format, "Key ID", "Provisioner", "Key (masked)", "Created At", "Bound At", "Account", "Reference") + firstIteration = false + } + for _, k := range eaksResponse.EAKs { + cliEAK := toCLI(ctx, client, k) + _, err = fmt.Fprintf(out, format, cliEAK.id, cliEAK.provisioner, "*****", cliEAK.createdAt, cliEAK.boundAt, cliEAK.account, cliEAK.reference) + if err != nil { + return errors.Wrap(err, "error writing ACME EAB key to output") + } + } + if eaksResponse.NextCursor == "" { + break + } + cursor = eaksResponse.NextCursor + } + + // ensure closing the output when at the end of what needs to be output + out.Close() + + if startedPager { + if err := cmd.Wait(); err != nil { + return errors.Wrap(err, "error waiting for $PAGER") + } + } + + return nil +} + +func pipeSignalHandler() { + signals := make(chan os.Signal, 1) + signal.Notify(signals, syscall.SIGCHLD) + + for range signals { + signal.Stop(signals) + os.Exit(0) + } +} diff --git a/command/ca/acme/eab/remove.go b/command/ca/acme/eab/remove.go new file mode 100644 index 00000000..d39583ce --- /dev/null +++ b/command/ca/acme/eab/remove.go @@ -0,0 +1,75 @@ +package eab + +import ( + "fmt" + + "github.com/pkg/errors" + "github.com/smallstep/cli/flags" + "github.com/smallstep/cli/utils/cautils" + "github.com/urfave/cli" + "go.step.sm/cli-utils/errs" +) + +func removeCommand() cli.Command { + return cli.Command{ + Name: "remove", + Action: cli.ActionFunc(removeAction), + Usage: "remove an ACME EAB Key from the CA", + UsageText: `**step beta ca acme eab remove** +[**--admin-cert**=] [**--admin-key**=] +[**--admin-provisioner**=] [**--admin-subject**=] +[**--password-file**=] [**--ca-url**=] [**--root**=] +[**--context**=]`, + Flags: []cli.Flag{ + flags.AdminCert, + flags.AdminKey, + flags.AdminProvisioner, + flags.AdminSubject, + flags.PasswordFile, + flags.CaURL, + flags.Root, + flags.Context, + }, + Description: `**step beta ca acme eab remove** removes an ACME EAB Key from the CA. + +## POSITIONAL ARGUMENTS + + +: Name of the provisioner to remove an ACME EAB key for + + +: The ACME EAB Key ID to remove + +## EXAMPLES + +Remove ACME EAB Key with Key ID "zFGdKC1sHmNf3Wsx3OujY808chxwEdmr" from my_acme_provisioner: +''' +$ step beta ca acme eab remove my_acme_provisioner zFGdKC1sHmNf3Wsx3OujY808chxwEdmr +''' +`, + } +} + +func removeAction(ctx *cli.Context) error { + if err := errs.NumberOfArguments(ctx, 2); err != nil { + return err + } + + args := ctx.Args() + provisioner := args.Get(0) + keyID := args.Get(1) + + client, err := cautils.NewAdminClient(ctx) + if err != nil { + return errors.Wrap(err, "error creating admin client") + } + + err = client.RemoveExternalAccountKey(provisioner, keyID) + if err != nil { + return errors.Wrap(err, "error removing ACME EAB key") + } + + fmt.Println("Key was deleted successfully!") + + return nil +} diff --git a/command/ca/ca.go b/command/ca/ca.go index f14e2445..b4954331 100644 --- a/command/ca/ca.go +++ b/command/ca/ca.go @@ -1,6 +1,8 @@ package ca import ( + "github.com/smallstep/cli/command/ca/acme" + "github.com/smallstep/cli/command/ca/admin" "github.com/smallstep/cli/command/ca/provisioner" "github.com/smallstep/cli/command/ca/provisionerbeta" @@ -165,6 +167,7 @@ commands may change, disappear, or be promoted to a different subcommand in the Subcommands: cli.Commands{ admin.Command(), provisionerbeta.Command(), + acme.Command(), }, } } diff --git a/command/ca/init.go b/command/ca/init.go index 5bcd6ded..958a37b2 100644 --- a/command/ca/init.go +++ b/command/ca/init.go @@ -512,7 +512,11 @@ func initAction(ctx *cli.Context) (err error) { } var address string - ui.Println("What IP and port will your new CA bind to?", ui.WithValue(ctx.String("address"))) + if helm { + ui.Println("What IP and port will your new CA bind to (it should match service.targetPort)?", ui.WithValue(ctx.String("address"))) + } else { + ui.Println("What IP and port will your new CA bind to?", ui.WithValue(ctx.String("address"))) + } address, err = ui.Prompt("(e.g. :443 or 127.0.0.1:443)", ui.WithValidateFunc(ui.Address()), ui.WithValue(ctx.String("address"))) if err != nil { diff --git a/command/ca/provisioner/add.go b/command/ca/provisioner/add.go index 4ac44afa..989d6351 100644 --- a/command/ca/provisioner/add.go +++ b/command/ca/provisioner/add.go @@ -332,7 +332,7 @@ func addAction(ctx *cli.Context) (err error) { provMap := make(map[string]bool) for _, p := range c.AuthorityConfig.Provisioners { - provMap[p.GetID()] = true + provMap[p.GetIDForToken()] = true } var list provisioner.List @@ -408,8 +408,8 @@ func addJWKProvisioner(ctx *cli.Context, name string, provMap map[string]bool) ( Claims: getClaims(ctx), } // Check for duplicates - if _, ok := provMap[p.GetID()]; !ok { - provMap[p.GetID()] = true + if _, ok := provMap[p.GetIDForToken()]; !ok { + provMap[p.GetIDForToken()] = true } else { return nil, errors.Errorf("duplicated provisioner: CA config already contains a provisioner with name=%s and kid=%s", name, jwk.KeyID) } @@ -448,8 +448,8 @@ func addJWKProvisioner(ctx *cli.Context, name string, provMap map[string]bool) ( Key: &key, Claims: getClaims(ctx), } - if _, ok := provMap[p.GetID()]; !ok { - provMap[p.GetID()] = true + if _, ok := provMap[p.GetIDForToken()]; !ok { + provMap[p.GetIDForToken()] = true } else { return nil, errors.Errorf("duplicated provisioner: CA config already contains a provisioner with name=%s and kid=%s", name, jwk.KeyID) } @@ -500,10 +500,10 @@ func addOIDCProvisioner(ctx *cli.Context, name string, provMap map[string]bool) ListenAddress: ctx.String("listen-address"), } // Check for duplicates - if _, ok := provMap[p.GetID()]; !ok { - provMap[p.GetID()] = true + if _, ok := provMap[p.GetIDForToken()]; !ok { + provMap[p.GetIDForToken()] = true } else { - return nil, errors.Errorf("duplicated provisioner: CA config already contains a provisioner with name=%s and client-id=%s", p.GetName(), p.GetID()) + return nil, errors.Errorf("duplicated provisioner: CA config already contains a provisioner with client-id=%s", p.GetID()) } list = append(list, p) return @@ -527,8 +527,8 @@ func addAWSProvisioner(ctx *cli.Context, name string, provMap map[string]bool) ( } // Check for duplicates - if _, ok := provMap[p.GetID()]; !ok { - provMap[p.GetID()] = true + if _, ok := provMap[p.GetIDForToken()]; !ok { + provMap[p.GetIDForToken()] = true } else { return nil, errors.Errorf("duplicated provisioner: CA config already contains a provisioner with type=AWS and name=%s", p.GetName()) } @@ -554,8 +554,8 @@ func addAzureProvisioner(ctx *cli.Context, name string, provMap map[string]bool) } // Check for duplicates - if _, ok := provMap[p.GetID()]; !ok { - provMap[p.GetID()] = true + if _, ok := provMap[p.GetIDForToken()]; !ok { + provMap[p.GetIDForToken()] = true } else { return nil, errors.Errorf("duplicated provisioner: CA config already contains a provisioner with type=Azure and name=%s", p.GetName()) } @@ -582,8 +582,8 @@ func addGCPProvisioner(ctx *cli.Context, name string, provMap map[string]bool) ( } // Check for duplicates - if _, ok := provMap[p.GetID()]; !ok { - provMap[p.GetID()] = true + if _, ok := provMap[p.GetIDForToken()]; !ok { + provMap[p.GetIDForToken()] = true } else { return nil, errors.Errorf("duplicated provisioner: CA config already contains a provisioner with type=GCP and name=%s", p.GetName()) } @@ -600,10 +600,10 @@ func addACMEProvisioner(ctx *cli.Context, name string, provMap map[string]bool) } // Check for duplicates - if _, ok := provMap[p.GetID()]; !ok { - provMap[p.GetID()] = true + if _, ok := provMap[p.GetIDForToken()]; !ok { + provMap[p.GetIDForToken()] = true } else { - return nil, errors.Errorf("duplicated provisioner: CA config already contains a provisioner with ID==%s", p.GetID()) + return nil, errors.Errorf("duplicated provisioner: CA config already contains a provisioner with ID==%s", p.GetIDForToken()) } list = append(list, p) @@ -641,10 +641,10 @@ func addX5CProvisioner(ctx *cli.Context, name string, provMap map[string]bool) ( } // Check for duplicates - if _, ok := provMap[p.GetID()]; !ok { - provMap[p.GetID()] = true + if _, ok := provMap[p.GetIDForToken()]; !ok { + provMap[p.GetIDForToken()] = true } else { - return nil, errors.Errorf("duplicated provisioner: CA config already contains a provisioner with ID=%s", p.GetID()) + return nil, errors.Errorf("duplicated provisioner: CA config already contains a provisioner with ID=%s", p.GetIDForToken()) } list = append(list, p) @@ -706,10 +706,10 @@ func addK8sSAProvisioner(ctx *cli.Context, name string, provMap map[string]bool) } // Check for duplicates - if _, ok := provMap[p.GetID()]; !ok { - provMap[p.GetID()] = true + if _, ok := provMap[p.GetIDForToken()]; !ok { + provMap[p.GetIDForToken()] = true } else { - return nil, errors.Errorf("duplicated provisioner: CA config already contains a provisioner with ID=%s", p.GetID()) + return nil, errors.Errorf("duplicated provisioner: CA config already contains a provisioner with ID=%s", p.GetIDForToken()) } list = append(list, p) @@ -726,10 +726,10 @@ func addSSHPOPProvisioner(ctx *cli.Context, name string, provMap map[string]bool } // Check for duplicates - if _, ok := provMap[p.GetID()]; !ok { - provMap[p.GetID()] = true + if _, ok := provMap[p.GetIDForToken()]; !ok { + provMap[p.GetIDForToken()] = true } else { - return nil, errors.Errorf("duplicated provisioner: CA config already contains a provisioner with ID=%s", p.GetID()) + return nil, errors.Errorf("duplicated provisioner: CA config already contains a provisioner with ID=%s", p.GetIDForToken()) } list = append(list, p) diff --git a/command/ca/provisionerbeta/add.go b/command/ca/provisionerbeta/add.go index afe5dab6..4ae66cc2 100644 --- a/command/ca/provisionerbeta/add.go +++ b/command/ca/provisionerbeta/add.go @@ -76,10 +76,16 @@ func addCommand() cli.Command { [**--admin-subject**=] [**--password-file**=] [**--ca-url**=] [**--root**=] [**--context**=] -**step beta ca provisioner add** **--type**=ACME [**--force-cn**] +**step beta ca provisioner add** **--type**=ACME [**--force-cn**] [**--require-eab**] [**--admin-cert**=] [**--admin-key**=] [**--admin-provisioner**=] [**--admin-subject**=] [**--password-file**=] [**--ca-url**=] -[**--root**=] [**--context**=]`, +[**--root**=] [**--context**=] + +**step beta ca provisioner add** **--type**=SCEP [**--force-cn**] [**--challenge**=] +[**--capabilities**=] [**--include-root**] [**--min-public-key-length**=] +[**--encryption-algorithm-identifier**=] [**--admin-cert**=] [**--admin-key**=] +[**--admin-provisioner**=] [**--admin-subject**=] [**--password-file**=] +[**--ca-url**=] [**--root**=] [**--context**=]`, Flags: []cli.Flag{ cli.StringFlag{ Name: "type", @@ -115,7 +121,10 @@ func addCommand() cli.Command { **SSHPOP** : Uses an SSH certificate / private key pair to sign provisioning tokens. - **Nebula** + **SCEP** + : Uses the SCEP protocol to create certificates. + + **Nebula** : Uses a Nebula certificate / private key pair to sign provisioning tokens. `}, x509TemplateFlag, @@ -195,6 +204,14 @@ provisioning tokens.`, // ACME provisioner flags forceCNFlag, + requireEABFlag, + + // SCEP provisioner flags + scepChallengeFlag, + scepCapabilitiesFlag, + scepIncludeRootFlag, + scepMinimumPublicKeyLengthFlag, + scepEncryptionAlgorithmIdentifierFlag, // Cloud provisioner flags awsAccountFlag, @@ -258,6 +275,11 @@ Create an ACME provisioner: step beta ca provisioner add acme --type ACME ''' +Create an ACME provisioner, forcing a CN and requiring EAB: +''' +step beta ca provisioner add acme --type ACME --force-cn --require-eab +''' + Create an K8SSA provisioner: ''' step beta ca provisioner add kube --type K8SSA --ssh --public-key key.pub @@ -268,6 +290,11 @@ Create an SSHPOP provisioner for renewing SSH host certificates:") step beta ca provisioner add sshpop --type SSHPOP ''' +Create a SCEP provisioner with 'secret' challenge and AES-256-CBC encryption: +''' +step beta ca provisioner add my_scep_provisioner --type SCEP --challenge secret --encryption-algorithm-identifier 2 +''' + Create an Azure provisioner with two service groups: ''' $ step beta ca provisioner add Azure --type Azure \ @@ -404,10 +431,12 @@ func addAction(ctx *cli.Context) (err error) { case linkedca.Provisioner_GCP: p.Type = linkedca.Provisioner_GCP p.Details, err = createGCPDetails(ctx) + case linkedca.Provisioner_SCEP: + p.Type = linkedca.Provisioner_SCEP + p.Details, err = createSCEPDetails(ctx) case linkedca.Provisioner_NEBULA: p.Type = linkedca.Provisioner_NEBULA p.Details, err = createNebulaDetails(ctx) - // TODO add SCEP provisioner support. default: return fmt.Errorf("unsupported provisioner type %s", ctx.String("type")) } @@ -554,7 +583,8 @@ func createACMEDetails(ctx *cli.Context) (*linkedca.ProvisionerDetails, error) { return &linkedca.ProvisionerDetails{ Data: &linkedca.ProvisionerDetails_ACME{ ACME: &linkedca.ACMEProvisioner{ - ForceCn: ctx.Bool("force-cn"), + ForceCn: ctx.Bool("force-cn"), + RequireEab: ctx.Bool("require-eab"), }, }, }, nil @@ -702,7 +732,7 @@ func createOIDCDetails(ctx *cli.Context) (*linkedca.ProvisionerDetails, error) { } func createAWSDetails(ctx *cli.Context) (*linkedca.ProvisionerDetails, error) { - d, err := parseIntaceAge(ctx) + d, err := parseInstanceAge(ctx) if err != nil { return nil, err } @@ -740,7 +770,7 @@ func createAzureDetails(ctx *cli.Context) (*linkedca.ProvisionerDetails, error) } func createGCPDetails(ctx *cli.Context) (*linkedca.ProvisionerDetails, error) { - d, err := parseIntaceAge(ctx) + d, err := parseInstanceAge(ctx) if err != nil { return nil, err } @@ -757,3 +787,18 @@ func createGCPDetails(ctx *cli.Context) (*linkedca.ProvisionerDetails, error) { }, }, nil } + +func createSCEPDetails(ctx *cli.Context) (*linkedca.ProvisionerDetails, error) { + return &linkedca.ProvisionerDetails{ + Data: &linkedca.ProvisionerDetails_SCEP{ + SCEP: &linkedca.SCEPProvisioner{ + ForceCn: ctx.Bool("force-cn"), + Challenge: ctx.String("challenge"), + Capabilities: ctx.StringSlice("capabilities"), + MinimumPublicKeyLength: int32(ctx.Int("min-public-key-length")), + IncludeRoot: ctx.Bool("include-root"), + EncryptionAlgorithmIdentifier: int32(ctx.Int("encryption-algorithm-identifier")), + }, + }, + }, nil +} diff --git a/command/ca/provisionerbeta/provisioner.go b/command/ca/provisionerbeta/provisioner.go index cdf2ad47..5526de37 100644 --- a/command/ca/provisionerbeta/provisioner.go +++ b/command/ca/provisionerbeta/provisioner.go @@ -72,7 +72,7 @@ $ step beta ca provisioner remove max@smallstep.com } } -func parseIntaceAge(ctx *cli.Context) (age string, err error) { +func parseInstanceAge(ctx *cli.Context) (age string, err error) { if !ctx.IsSet("instance-age") { return } @@ -172,6 +172,43 @@ var ( Name: "force-cn", Usage: `Always set the common name in provisioned certificates.`, } + requireEABFlag = cli.BoolFlag{ + Name: "require-eab", + Usage: `Require (and enable) External Account Binding for Account creation.`, + } + disableEABFlag = cli.BoolFlag{ + Name: "disable-eab", + Usage: `Disable External Account Binding for Account creation.`, + } + + // SCEP provisioner flags + scepChallengeFlag = cli.StringFlag{ + Name: "challenge", + Usage: `The SCEP to use as a shared secret between a client and the CA`, + } + scepCapabilitiesFlag = cli.StringSliceFlag{ + Name: "capabilities", + Usage: `The SCEP to advertise`, + } + scepIncludeRootFlag = cli.BoolFlag{ + Name: "include-root", + Usage: `Include the CA root certificate in the SCEP CA certificate chain`, + } + scepMinimumPublicKeyLengthFlag = cli.IntFlag{ + Name: "min-public-key-length", + Usage: `The minimum public key of the SCEP RSA encryption key`, + } + scepEncryptionAlgorithmIdentifierFlag = cli.IntFlag{ + Name: "encryption-algorithm-identifier", + Usage: `The for the SCEP encryption algorithm to use. + Valid values are 0 - 4, inclusive. The values correspond to: + 0: DES-CBC, + 1: AES-128-CBC, + 2: AES-256-CBC, + 3: AES-128-GCM, + 4: AES-256-GCM. + Defaults to DES-CBC (0) for legacy clients.`, + } // Cloud provisioner flags awsAccountFlag = cli.StringSliceFlag{ diff --git a/command/ca/provisionerbeta/update.go b/command/ca/provisionerbeta/update.go index 15a28afd..1f7941b7 100644 --- a/command/ca/provisionerbeta/update.go +++ b/command/ca/provisionerbeta/update.go @@ -39,7 +39,7 @@ func updateCommand() cli.Command { ACME -**step beta ca provisioner update** [**--force-cn**] +**step beta ca provisioner update** [**--force-cn**] [**--require-eab**] [**--disable-eab**] [**--admin-cert**=] [**--admin-key**=] [**--admin-provisioner**=] [**--admin-subject**=] [**--password-file**=] [**--ca-url**=] [**--root**=] [**--context**=] @@ -81,7 +81,14 @@ IID (AWS/GCP/Azure) [**--disable-custom-sans**] [**--disable-trust-on-first-use**] [**--admin-cert**=] [**--admin-key**=] [**--admin-provisioner**=] [**--admin-subject**=] [**--password-file**=] [**--ca-url**=] -[**--root**=] [**--context**=]`, +[**--root**=] [**--context**=] + +**step beta ca provisioner update** [**--force-cn**] [**--challenge**=] +[**--capabilities**=] [**--include-root**] [**--minimum-public-key-length**=] +[**--encryption-algorithm-identifier**=] [**--admin-cert**=] [**--admin-key**=] +[**--admin-provisioner**=] [**--admin-subject**=] [**--password-file**=] +[**--ca-url**=] [**--root**=] [**--context**=] +`, Flags: []cli.Flag{ cli.StringFlag{ Name: "name", @@ -169,6 +176,15 @@ provisioning tokens.`, // ACME provisioner flags forceCNFlag, + requireEABFlag, + disableEABFlag, + + // SCEP flags + scepChallengeFlag, + scepCapabilitiesFlag, + scepIncludeRootFlag, + scepMinimumPublicKeyLengthFlag, + scepEncryptionAlgorithmIdentifierFlag, // Cloud provisioner flags awsAccountFlag, @@ -236,7 +252,7 @@ step beta ca provisioner update x5c --x5c-root x5c_ca.crt Update an ACME provisioner: ''' -step beta ca provisioner update acme --force-cn +step beta ca provisioner update acme --force-cn --require-eab ''' Update an K8SSA provisioner: @@ -250,7 +266,7 @@ $ step beta ca provisioner update Azure \ --azure-resource-group identity --azure-resource-group accounting ''' -Update an GCP provisioner: +Update a GCP provisioner: ''' $ step beta ca provisioner update Google \ --disable-custom-sans --gcp-project internal --remove-gcp-project public @@ -259,6 +275,11 @@ $ step beta ca provisioner update Google \ Update an AWS provisioner: ''' $ step beta ca provisioner update Amazon --disable-custom-sans --disable-trust-on-first-use +''' + +Update a SCEP provisioner: +''' +step beta ca provisioner update my_scep_provisioner --force-cn '''`, } } @@ -309,9 +330,10 @@ func updateAction(ctx *cli.Context) (err error) { err = updateAzureDetails(ctx, p) case linkedca.Provisioner_GCP: err = updateGCPDetails(ctx, p) + case linkedca.Provisioner_SCEP: + err = updateSCEPDetails(ctx, p) case linkedca.Provisioner_NEBULA: err = updateNebulaDetails(ctx, p) - // TODO add SCEP provisioner support. default: return fmt.Errorf("unsupported provisioner type %s", p.Type.String()) } @@ -460,7 +482,7 @@ func updateClaims(ctx *cli.Context, p *linkedca.Provisioner) { func updateJWKDetails(ctx *cli.Context, p *linkedca.Provisioner) error { data, ok := p.Details.GetData().(*linkedca.ProvisionerDetails_JWK) if !ok { - return errors.New("error casting details to ACME type") + return errors.New("error casting details to JWK type") } details := data.JWK @@ -580,6 +602,17 @@ func updateACMEDetails(ctx *cli.Context, p *linkedca.Provisioner) error { if ctx.IsSet("force-cn") { details.ForceCn = ctx.Bool("force-cn") } + requireEABSet := ctx.IsSet("require-eab") + disableEABSet := ctx.IsSet("disable-eab") + if requireEABSet && disableEABSet { + return errs.IncompatibleFlagWithFlag(ctx, "require-eab", "disable-eab") + } + if requireEABSet { + details.RequireEab = ctx.Bool("require-eab") + } + if disableEABSet { + details.RequireEab = false + } return nil } @@ -733,13 +766,13 @@ func updateOIDCDetails(ctx *cli.Context, p *linkedca.Provisioner) error { func updateAWSDetails(ctx *cli.Context, p *linkedca.Provisioner) error { data, ok := p.Details.GetData().(*linkedca.ProvisionerDetails_AWS) if !ok { - return errors.New("error casting details to OIDC type") + return errors.New("error casting details to AWS type") } details := data.AWS var err error if ctx.IsSet("instance-age") { - details.InstanceAge, err = parseIntaceAge(ctx) + details.InstanceAge, err = parseInstanceAge(ctx) if err != nil { return err } @@ -762,7 +795,7 @@ func updateAWSDetails(ctx *cli.Context, p *linkedca.Provisioner) error { func updateAzureDetails(ctx *cli.Context, p *linkedca.Provisioner) error { data, ok := p.Details.GetData().(*linkedca.ProvisionerDetails_Azure) if !ok { - return errors.New("error casting details to OIDC type") + return errors.New("error casting details to Azure type") } details := data.Azure @@ -787,13 +820,13 @@ func updateAzureDetails(ctx *cli.Context, p *linkedca.Provisioner) error { func updateGCPDetails(ctx *cli.Context, p *linkedca.Provisioner) error { data, ok := p.Details.GetData().(*linkedca.ProvisionerDetails_GCP) if !ok { - return errors.New("error casting details to OIDC type") + return errors.New("error casting details to GCP type") } details := data.GCP var err error if ctx.IsSet("instance-age") { - details.InstanceAge, err = parseIntaceAge(ctx) + details.InstanceAge, err = parseInstanceAge(ctx) if err != nil { return err } @@ -818,3 +851,32 @@ func updateGCPDetails(ctx *cli.Context, p *linkedca.Provisioner) error { } return nil } + +func updateSCEPDetails(ctx *cli.Context, p *linkedca.Provisioner) error { + data, ok := p.Details.GetData().(*linkedca.ProvisionerDetails_SCEP) + if !ok { + return errors.New("error casting details to SCEP type") + } + details := data.SCEP + + if ctx.IsSet("force-cn") { + details.ForceCn = ctx.Bool("force-cn") + } + if ctx.IsSet("challenge") { + details.Challenge = ctx.String("challenge") + } + if ctx.IsSet("capabilities") { + details.Capabilities = ctx.StringSlice("capabilities") + } + if ctx.IsSet("min-public-key-length") { + details.MinimumPublicKeyLength = int32(ctx.Int("min-public-key-length")) + } + if ctx.IsSet("include-root") { + details.IncludeRoot = ctx.Bool("include-root") + } + if ctx.IsSet("encryption-algorithm-identifier") { + details.EncryptionAlgorithmIdentifier = int32(ctx.Int("encryption-algorithm-identifier")) + } + + return nil +} diff --git a/command/crl/crl.go b/command/crl/crl.go new file mode 100644 index 00000000..1c5530f2 --- /dev/null +++ b/command/crl/crl.go @@ -0,0 +1,29 @@ +package crl + +import ( + "github.com/urfave/cli" + "go.step.sm/cli-utils/command" +) + +// init creates and registers the crl command +func init() { + cmd := cli.Command{ + Name: "crl", + Usage: "initialize and manage a certificate revocation list", + UsageText: "**step crl** [arguments] [global-flags] [subcommand-flags]", + Description: `**step crl** command group provides facilities to create, manage and inspect a +certificate revocation list (CRL). + +## EXAMPLES + +Inspect a CRL: +''' +$ step crl inspect http://ca.example.com/crls/exampleca.crl +'''`, + Subcommands: cli.Commands{ + inspectCommand(), + }, + } + + command.Register(cmd) +} diff --git a/command/crl/crl_extensions.go b/command/crl/crl_extensions.go new file mode 100644 index 00000000..dfa33b53 --- /dev/null +++ b/command/crl/crl_extensions.go @@ -0,0 +1,211 @@ +package crl + +import ( + "bytes" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/hex" + "encoding/json" + "fmt" + "math/big" + "strconv" +) + +var ( + oidExtensionReasonCode = asn1.ObjectIdentifier{2, 5, 29, 21} + oidExtensionCRLNumber = asn1.ObjectIdentifier{2, 5, 29, 20} + oidExtensionAuthorityKeyID = asn1.ObjectIdentifier{2, 5, 29, 35} + oidExtensionIssuingDistributionPoint = asn1.ObjectIdentifier{2, 5, 29, 28} +) + +func parseReasonCode(b []byte) string { + var reasonCode asn1.Enumerated + if _, err := asn1.Unmarshal(b, &reasonCode); err != nil { + return sanitizeBytes(b) + } + switch reasonCode { + case 0: + return "Unspecified" + case 1: + return "Key Compromise" + case 2: + return "CA Compromise" + case 3: + return "Affiliation Changed" + case 4: + return "Superseded" + case 5: + return "Cessation Of Operation" + case 6: + return "Certificate Hold" + case 8: + return "Remove From CRL" + case 9: + return "Privilege Withdrawn" + case 10: + return "AA Compromise" + default: + return fmt.Sprintf("ReasonCode(%d): unknown", reasonCode) + } +} + +// RFC 5280, 4.2.1.1 +type authorityKeyID struct { + ID []byte `asn1:"optional,tag:0"` +} + +// RFC 5280, 5.2.5 +type distributionPoint struct { + DistributionPoint distributionPointName `asn1:"optional,tag:0"` + OnlyContainsUserCerts bool `asn1:"optional,tag:1"` + OnlyContainsCACerts bool `asn1:"optional,tag:2"` + OnlySomeReasons asn1.BitString `asn1:"optional,tag:3"` + IndirectCRL bool `asn1:"optional,tag:4"` + OnlyContainsAttributeCerts bool `asn1:"optional,tag:5"` +} + +type distributionPointName struct { + FullName []asn1.RawValue `asn1:"optional,tag:0"` + RelativeName pkix.RDNSequence `asn1:"optional,tag:1"` +} + +func (d distributionPoint) FullNames() []string { + var names []string + for _, v := range d.DistributionPoint.FullName { + switch v.Class { + case 2: + names = append(names, fmt.Sprintf("URI:%s", v.Bytes)) + default: + names = append(names, fmt.Sprintf("Class(%d):%s", v.Class, v.Bytes)) + } + } + return names +} + +type Extension struct { + Name string `json:"-"` + Details []string `json:"-"` + json map[string]interface{} +} + +func (e *Extension) MarshalJSON() ([]byte, error) { + return json.Marshal(e.json) +} + +func (e *Extension) AddDetail(format string, args ...interface{}) { + e.Details = append(e.Details, fmt.Sprintf(format, args...)) +} + +func newExtension(e pkix.Extension) Extension { + var ext Extension + switch { + case e.Id.Equal(oidExtensionReasonCode): + ext.Name = "X509v3 CRL Reason Code:" + value := parseReasonCode(e.Value) + ext.AddDetail(value) + ext.json = map[string]interface{}{ + "crl_reason_code": value, + } + + case e.Id.Equal(oidExtensionCRLNumber): + ext.Name = "X509v3 CRL Number:" + var n *big.Int + if _, err := asn1.Unmarshal(e.Value, &n); err == nil { + ext.AddDetail(n.String()) + ext.json = map[string]interface{}{ + "crl_number": n.String(), + } + } else { + ext.AddDetail(sanitizeBytes(e.Value)) + ext.json = map[string]interface{}{ + "crl_number": e.Value, + } + } + + case e.Id.Equal(oidExtensionAuthorityKeyID): + var v authorityKeyID + ext.Name = "X509v3 Authority Key Identifier:" + ext.json = map[string]interface{}{ + "authority_key_id": hex.EncodeToString(e.Value), + } + if _, err := asn1.Unmarshal(e.Value, &v); err == nil { + var s string + for _, b := range v.ID { + s += fmt.Sprintf(":%02X", b) + } + ext.AddDetail("keyid" + s) + } else { + ext.AddDetail(sanitizeBytes(e.Value)) + } + case e.Id.Equal(oidExtensionIssuingDistributionPoint): + ext.Name = "X509v3 Issuing Distribution Point:" + + var v distributionPoint + if _, err := asn1.Unmarshal(e.Value, &v); err != nil { + ext.AddDetail(sanitizeBytes(e.Value)) + ext.json = map[string]interface{}{ + "issuing_distribution_point": e.Value, + } + } else { + names := v.FullNames() + if len(names) > 0 { + ext.AddDetail("Full Name:") + for _, n := range names { + ext.AddDetail(" " + n) + } + } + js := map[string]interface{}{ + "full_names": names, + } + + // Only one of this should be set to true. But for inspect we + // will allow more than one. + if v.OnlyContainsUserCerts { + ext.AddDetail("Only User Certificates") + js["only_user_certificates"] = true + } + if v.OnlyContainsCACerts { + ext.AddDetail("Only CA Certificates") + js["only_ca_certificates"] = true + } + if v.OnlyContainsAttributeCerts { + ext.AddDetail("Only Attribute Certificates") + js["only_attribute_certificates"] = true + } + if len(v.OnlySomeReasons.Bytes) > 0 { + ext.AddDetail("Reasons: %x", v.OnlySomeReasons.Bytes) + js["only_some_reasons"] = v.OnlySomeReasons.Bytes + } + + ext.json = map[string]interface{}{ + "issuing_distribution_point": js, + } + } + default: + ext.Name = e.Id.String() + ext.AddDetail(sanitizeBytes(e.Value)) + ext.json = map[string]interface{}{ + ext.Name: e.Value, + } + } + + if e.Critical { + ext.Name += " critical" + ext.json["critical"] = true + } + + return ext +} + +func sanitizeBytes(b []byte) string { + value := bytes.Runes(b) + sanitized := make([]rune, len(value)) + for i, r := range value { + if strconv.IsPrint(r) && r != '�' { + sanitized[i] = r + } else { + sanitized[i] = '.' + } + } + return string(sanitized) +} diff --git a/command/crl/inspect.go b/command/crl/inspect.go new file mode 100644 index 00000000..7eabe01a --- /dev/null +++ b/command/crl/inspect.go @@ -0,0 +1,523 @@ +package crl + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/json" + "encoding/pem" + "fmt" + "io" + "net" + "net/http" + "net/url" + "os" + "strconv" + "strings" + "time" + + "github.com/pkg/errors" + "github.com/smallstep/cli/flags" + "github.com/smallstep/cli/utils" + "github.com/urfave/cli" + "go.step.sm/cli-utils/command" + "go.step.sm/cli-utils/errs" + "go.step.sm/crypto/pemutil" + "go.step.sm/crypto/x509util" +) + +func inspectCommand() cli.Command { + return cli.Command{ + Name: "inspect", + Action: command.ActionFunc(inspectAction), + Usage: "print certificate revocation list (CRL) details in human-readable format", + UsageText: `**step crl inspect** `, + Description: `**step crl inspect** validates and prints the details of a certificate revocation list (CRL). +A CRL is considered valid if its signature is valid, the CA is not expired, and the next update time is in the future. + +## POSITIONAL ARGUMENTS + + +: The file or URL where the CRL is. If <--from> is passed it will inspect +the certificate and extract the CRL distribution point from. + +## EXAMPLES + +Inspect a CRL: +''' +$ step crl inspect --insecure http://ca.example.com/crls/exampleca.crl +''' + +Inspect and validate a CRL in a file: +''' +$ step crl inspect --ca ca.crt exampleca.crl +''' + +Format the CRL in JSON: +''' +$ step crl inspect --insecure --format json exampleca.crl +''' + +Inspect the CRL from the CRL distribution point of a given url: +''' +$ step crl inspect --from https://www.google.com +'''`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "format", + Value: "text", + Usage: `The output format for printing the introspection details. + +: is a string and must be one of: + + **text** + : Print output in unstructured text suitable for a human to read. + This is the default format. + + **json** + : Print output in JSON format. + + **pem** + : Print output in PEM format.`, + }, + cli.StringFlag{ + Name: "ca", + Usage: `The certificate used to validate the CRL.`, + }, + cli.BoolFlag{ + Name: "from", + Usage: `Extract CRL and CA from the URL passed as argument.`, + }, + cli.StringSliceFlag{ + Name: "roots", + Usage: `Root certificate(s) that will be used to verify the +authenticity of the remote server. + +: is a case-sensitive string and may be one of: + + **file** + : Relative or full path to a file. All certificates in the file will be used for path validation. + + **list of files** + : Comma-separated list of relative or full file paths. Every PEM encoded certificate from each file will be used for path validation. + + **directory** + : Relative or full path to a directory. Every PEM encoded certificate from each file in the directory will be used for path validation.`, + }, + flags.Insecure, + }, + } +} + +func inspectAction(ctx *cli.Context) error { + if err := errs.MinMaxNumberOfArguments(ctx, 0, 1); err != nil { + return err + } + + isFrom := ctx.Bool("from") + + // Require --insecure + if !isFrom && ctx.String("ca") == "" && !ctx.Bool("insecure") { + return errs.InsecureCommand(ctx) + } + + var tlsConfig *tls.Config + httpClient := http.Client{} + if roots := ctx.String("roots"); roots != "" { + pool, err := x509util.ReadCertPool(roots) + if err != nil { + return err + } + tlsConfig = &tls.Config{ + RootCAs: pool, + } + tr := http.DefaultTransport.(*http.Transport).Clone() + tr.TLSClientConfig = tlsConfig + httpClient.Transport = tr + } + + crlFile := ctx.Args().First() + if crlFile == "" { + crlFile = "-" + } + + var isURL bool + for _, p := range []string{"http://", "https://"} { + if strings.HasPrefix(strings.ToLower(crlFile), p) { + isURL = true + break + } + } + + var caCerts []*x509.Certificate + if filename := ctx.String("ca"); filename != "" { + var err error + if caCerts, err = pemutil.ReadCertificateBundle(filename); err != nil { + return err + } + } + + if isFrom { + var bundle []*x509.Certificate + if isURL { + u, err := url.Parse(crlFile) + if err != nil { + return errors.Wrapf(err, "error parsing %s", crlFile) + } + if _, _, err := net.SplitHostPort(u.Host); err != nil { + u.Host = net.JoinHostPort(u.Host, "443") + } + conn, err := tls.Dial("tcp", u.Host, tlsConfig) + if err != nil { + return errors.Wrapf(err, "error connecting %s", crlFile) + } + bundle = conn.ConnectionState().PeerCertificates + } else { + var err error + if bundle, err = pemutil.ReadCertificateBundle(crlFile); err != nil { + return err + } + } + + isURL = true + if len(bundle[0].CRLDistributionPoints) == 0 { + return errors.Errorf("failed to get CRL distribution points from %s", crlFile) + } + + crlFile = bundle[0].CRLDistributionPoints[0] + if len(bundle) > 1 { + caCerts = append(caCerts, bundle[1:]...) + } + + if len(caCerts) == 0 && !ctx.Bool("insecure") { + return errs.InsecureCommand(ctx) + } + } + + var ( + b []byte + err error + ) + if isURL { + resp, err := httpClient.Get(crlFile) + if err != nil { + return errors.Wrap(err, "error downloading crl") + } + if resp.StatusCode >= 400 { + return errors.Errorf("error downloading crl: status code %d", resp.StatusCode) + } + b, err = io.ReadAll(resp.Body) + if err != nil { + return errors.Wrap(err, "error downloading crl") + } + } else { + b, err = utils.ReadFile(crlFile) + if err != nil { + return err + } + } + + crl, err := ParseCRL(b) + if err != nil { + return errors.Wrap(err, "error parsing crl") + } + + if len(caCerts) > 0 { + for _, crt := range caCerts { + if (crt.KeyUsage&x509.KeyUsageCRLSign) == 0 || len(crt.SubjectKeyId) == 0 { + continue + } + if bytes.Equal(crt.SubjectKeyId, crl.authorityKeyID) { + crl.Signature.Valid = crl.Verify(crt) + break + } + } + } + + switch ctx.String("format") { + case "json": + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + if err := enc.Encode(crl); err != nil { + return errors.Wrap(err, "error marshaling crl") + } + case "pem": + pem.Encode(os.Stdout, &pem.Block{ + Type: "X509 CRL", + Bytes: b, + }) + default: + printCRL(crl) + } + + return nil +} + +// CRL is the JSON representation of a certificate revocation list. +type CRL struct { + Version int `json:"version"` + SignatureAlgorithm SignatureAlgorithm `json:"signature_algorithm"` + Issuer DistinguisedName `json:"issuer"` + ThisUpdate time.Time `json:"this_update"` + NextUpdate time.Time `json:"next_update"` + RevokedCertificates []RevokedCertificate `json:"revoked_certificates"` + Extensions []Extension `json:"extensions,omitempty"` + Signature *Signature `json:"signature"` + authorityKeyID []byte + raw []byte +} + +func ParseCRL(b []byte) (*CRL, error) { + crl, err := x509.ParseCRL(b) + if err != nil { + return nil, errors.Wrap(err, "error parsing crl") + } + tcrl := crl.TBSCertList + + certs := make([]RevokedCertificate, len(tcrl.RevokedCertificates)) + for i, c := range tcrl.RevokedCertificates { + certs[i] = newRevokedCertificate(c) + } + + var issuerKeyID []byte + extensions := make([]Extension, len(tcrl.Extensions)) + for i, e := range tcrl.Extensions { + extensions[i] = newExtension(e) + if e.Id.Equal(oidExtensionAuthorityKeyID) { + var v authorityKeyID + if _, err := asn1.Unmarshal(e.Value, &v); err == nil { + issuerKeyID = v.ID + } + } + } + + return &CRL{ + Version: tcrl.Version + 1, + SignatureAlgorithm: newSignatureAlgorithm(tcrl.Signature), + Issuer: newDistinguishedName(tcrl.Issuer), + ThisUpdate: tcrl.ThisUpdate, + NextUpdate: tcrl.NextUpdate, + RevokedCertificates: certs, + Extensions: extensions, + Signature: &Signature{ + SignatureAlgorithm: newSignatureAlgorithm(tcrl.Signature), + Value: crl.SignatureValue.Bytes, + Valid: false, + }, + authorityKeyID: issuerKeyID, + raw: crl.TBSCertList.Raw, + }, nil +} + +func (c *CRL) Verify(ca *x509.Certificate) bool { + now := time.Now() + if now.After(c.NextUpdate) || now.After(ca.NotAfter) { + return false + } + + var sum []byte + var hash crypto.Hash + if hash = c.SignatureAlgorithm.hash; hash > 0 { + h := hash.New() + h.Write(c.raw) + sum = h.Sum(nil) + } + + sig := c.Signature.Value + switch pub := ca.PublicKey.(type) { + case *ecdsa.PublicKey: + return ecdsa.VerifyASN1(pub, sum, sig) + case *rsa.PublicKey: + switch c.SignatureAlgorithm.algo { + case x509.SHA256WithRSAPSS, x509.SHA384WithRSAPSS, x509.SHA512WithRSAPSS: + return rsa.VerifyPSS(pub, hash, sum, sig, &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthAuto, + }) == nil + default: + return rsa.VerifyPKCS1v15(pub, hash, sum, sig) == nil + } + case ed25519.PublicKey: + return ed25519.Verify(pub, c.raw, sig) + default: + return false + } +} + +func printCRL(crl *CRL) { + fmt.Println("Certificate Revocation List (CRL):") + fmt.Println(" Data:") + fmt.Printf(" Valid: %v\n", crl.Signature.Valid) + fmt.Printf(" Version: %d (0x%x)\n", crl.Version, crl.Version-1) + fmt.Println(" Signature algorithm:", crl.SignatureAlgorithm) + fmt.Println(" Issuer:", crl.Issuer) + fmt.Println(" Last Update:", crl.ThisUpdate.UTC()) + fmt.Println(" Next Update:", crl.NextUpdate.UTC()) + fmt.Println(" CRL Extensions:") + for _, e := range crl.Extensions { + fmt.Println(spacer(12) + e.Name) + for _, s := range e.Details { + fmt.Println(spacer(16) + s) + } + } + if len(crl.RevokedCertificates) == 0 { + fmt.Println(spacer(8) + "No Revoked Certificates.") + } else { + fmt.Println(spacer(8) + "Revoked Certificates:") + for _, crt := range crl.RevokedCertificates { + fmt.Printf(spacer(12)+"Serial Number: %s (0x%X)\n", crt.SerialNumber, crt.SerialNumberBytes) + fmt.Println(spacer(16)+"Revocation Date:", crt.RevocationTime.UTC()) + if len(crt.Extensions) > 0 { + fmt.Println(spacer(16) + "CRL Entry Extensions:") + for _, e := range crt.Extensions { + fmt.Println(spacer(20) + e.Name) + for _, s := range e.Details { + fmt.Println(spacer(24) + s) + } + } + } + } + } + + fmt.Println(" Signature Algorithm:", crl.Signature.SignatureAlgorithm) + printBytes(crl.Signature.Value, spacer(8)) +} + +// Signature is the JSON representation of a CRL signature. +type Signature struct { + SignatureAlgorithm SignatureAlgorithm `json:"signature_algorithm"` + Value []byte `json:"value"` + Valid bool `json:"valid"` +} + +// DistinguisedName is the JSON representation of the CRL issuer. +type DistinguisedName struct { + Country []string `json:"country,omitempty"` + Organization []string `json:"organization,omitempty"` + OrganizationalUnit []string `json:"organizational_unit,omitempty"` + Locality []string `json:"locality,omitempty"` + Province []string `json:"province,omitempty"` + StreetAddress []string `json:"street_address,omitempty"` + PostalCode []string `json:"postal_code,omitempty"` + SerialNumber string `json:"serial_number,omitempty"` + CommonName string `json:"common_name,omitempty"` + ExtraNames map[string][]interface{} `json:"extra_names,omitempty"` + raw pkix.RDNSequence +} + +// String returns the one line representation of the distinguished name. +func (d DistinguisedName) String() string { + var parts []string + for _, dn := range d.raw { + v := strings.ReplaceAll(pkix.RDNSequence{dn}.String(), "\\,", ",") + parts = append(parts, v) + } + return strings.Join(parts, " ") +} + +func newDistinguishedName(seq pkix.RDNSequence) DistinguisedName { + var n pkix.Name + n.FillFromRDNSequence(&seq) + + var extraNames map[string][]interface{} + if len(n.ExtraNames) > 0 { + extraNames = make(map[string][]interface{}) + for _, tv := range n.ExtraNames { + oid := tv.Type.String() + if s, ok := tv.Value.(string); ok { + extraNames[oid] = append(extraNames[oid], s) + continue + } + if b, err := asn1.Marshal(tv.Value); err == nil { + extraNames[oid] = append(extraNames[oid], b) + continue + } + extraNames[oid] = append(extraNames[oid], escapeValue(tv.Value)) + } + } + + return DistinguisedName{ + Country: n.Country, + Organization: n.Organization, + OrganizationalUnit: n.OrganizationalUnit, + Locality: n.Locality, + Province: n.Province, + StreetAddress: n.StreetAddress, + PostalCode: n.PostalCode, + SerialNumber: n.SerialNumber, + CommonName: n.CommonName, + ExtraNames: extraNames, + raw: seq, + } +} + +// RevokedCertificate is the JSON representation of a certificate in a CRL. +type RevokedCertificate struct { + SerialNumber string `json:"serial_number"` + RevocationTime time.Time `json:"revocation_time"` + Extensions []Extension `json:"extensions,omitempty"` + SerialNumberBytes []byte `json:"-"` +} + +func newRevokedCertificate(c pkix.RevokedCertificate) RevokedCertificate { + var extensions []Extension + + return RevokedCertificate{ + SerialNumber: c.SerialNumber.String(), + RevocationTime: c.RevocationTime.UTC(), + Extensions: extensions, + SerialNumberBytes: c.SerialNumber.Bytes(), + } +} + +func spacer(i int) string { + return fmt.Sprintf("%"+strconv.Itoa(i)+"s", "") +} + +func printBytes(bs []byte, prefix string) { + for i, b := range bs { + if i == 0 { + fmt.Print(prefix) + } else if (i % 16) == 0 { + fmt.Print("\n" + prefix) + } + fmt.Printf("%02x", b) + if i != len(bs)-1 { + fmt.Print(":") + } + } + fmt.Println() +} + +func escapeValue(v interface{}) string { + s := fmt.Sprint(v) + escaped := make([]rune, 0, len(s)) + + for k, c := range s { + escape := false + + switch c { + case ',', '+', '"', '\\', '<', '>', ';': + escape = true + + case ' ': + escape = k == 0 || k == len(s)-1 + + case '#': + escape = k == 0 + } + + if escape { + escaped = append(escaped, '\\', c) + } else { + escaped = append(escaped, c) + } + } + + return string(escaped) +} diff --git a/command/crl/signature_algorithms.go b/command/crl/signature_algorithms.go new file mode 100644 index 00000000..0cfffd98 --- /dev/null +++ b/command/crl/signature_algorithms.go @@ -0,0 +1,160 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package crl + +import ( + "bytes" + "crypto" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" +) + +// OIDs for signature algorithms +var ( + oidSignatureMD2WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 2} + oidSignatureMD5WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 4} + oidSignatureSHA1WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 5} + oidSignatureSHA256WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 11} + oidSignatureSHA384WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 12} + oidSignatureSHA512WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 13} + oidSignatureRSAPSS = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 10} + oidSignatureDSAWithSHA1 = asn1.ObjectIdentifier{1, 2, 840, 10040, 4, 3} + oidSignatureDSAWithSHA256 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 3, 2} + oidSignatureECDSAWithSHA1 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 1} + oidSignatureECDSAWithSHA256 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 2} + oidSignatureECDSAWithSHA384 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 3} + oidSignatureECDSAWithSHA512 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 4} + oidSignatureEd25519 = asn1.ObjectIdentifier{1, 3, 101, 112} + + oidSHA256 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1} + oidSHA384 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 2} + oidSHA512 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 3} + + oidMGF1 = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 8} + + // oidISOSignatureSHA1WithRSA means the same as oidSignatureSHA1WithRSA + // but it's specified by ISO. Microsoft's makecert.exe has been known + // to produce certificates with this OID. + oidISOSignatureSHA1WithRSA = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 29} +) + +var signatureAlgorithmDetails = []struct { + algo x509.SignatureAlgorithm + oid asn1.ObjectIdentifier + hash crypto.Hash +}{ + {x509.MD2WithRSA, oidSignatureMD2WithRSA, crypto.Hash(0)}, // no value for MD2 + {x509.MD5WithRSA, oidSignatureMD5WithRSA, crypto.MD5}, + {x509.SHA1WithRSA, oidSignatureSHA1WithRSA, crypto.SHA1}, + {x509.SHA1WithRSA, oidISOSignatureSHA1WithRSA, crypto.SHA1}, + {x509.SHA256WithRSA, oidSignatureSHA256WithRSA, crypto.SHA256}, + {x509.SHA384WithRSA, oidSignatureSHA384WithRSA, crypto.SHA384}, + {x509.SHA512WithRSA, oidSignatureSHA512WithRSA, crypto.SHA512}, + {x509.SHA256WithRSAPSS, oidSignatureRSAPSS, crypto.SHA256}, + {x509.SHA384WithRSAPSS, oidSignatureRSAPSS, crypto.SHA384}, + {x509.SHA512WithRSAPSS, oidSignatureRSAPSS, crypto.SHA512}, + {x509.DSAWithSHA1, oidSignatureDSAWithSHA1, crypto.SHA1}, + {x509.DSAWithSHA256, oidSignatureDSAWithSHA256, crypto.SHA256}, + {x509.ECDSAWithSHA1, oidSignatureECDSAWithSHA1, crypto.SHA1}, + {x509.ECDSAWithSHA256, oidSignatureECDSAWithSHA256, crypto.SHA256}, + {x509.ECDSAWithSHA384, oidSignatureECDSAWithSHA384, crypto.SHA384}, + {x509.ECDSAWithSHA512, oidSignatureECDSAWithSHA512, crypto.SHA512}, + {x509.PureEd25519, oidSignatureEd25519, crypto.Hash(0)}, +} + +type SignatureAlgorithm struct { + Name string `json:"name"` + OID string `json:"oid"` + algo x509.SignatureAlgorithm + hash crypto.Hash +} + +func (s SignatureAlgorithm) String() string { + if s.Name == "" { + return s.OID + } + return s.Name +} + +// pssParameters reflects the parameters in an AlgorithmIdentifier that +// specifies RSA PSS. See RFC 3447, Appendix A.2.3. +type pssParameters struct { + // The following three fields are not marked as + // optional because the default values specify SHA-1, + // which is no longer suitable for use in signatures. + Hash pkix.AlgorithmIdentifier `asn1:"explicit,tag:0"` + MGF pkix.AlgorithmIdentifier `asn1:"explicit,tag:1"` + SaltLength int `asn1:"explicit,tag:2"` + TrailerField int `asn1:"optional,explicit,tag:3,default:1"` +} + +func newSignatureAlgorithm(ai pkix.AlgorithmIdentifier) SignatureAlgorithm { + sa := SignatureAlgorithm{ + OID: ai.Algorithm.String(), + } + + if ai.Algorithm.Equal(oidSignatureEd25519) { + // RFC 8410, Section 3 + // > For all of the OIDs, the parameters MUST be absent. + if len(ai.Parameters.FullBytes) != 0 { + return sa + } + } + + if !ai.Algorithm.Equal(oidSignatureRSAPSS) { + for _, details := range signatureAlgorithmDetails { + if ai.Algorithm.Equal(details.oid) { + sa.Name = details.algo.String() + sa.algo = details.algo + sa.hash = details.hash + } + } + return sa + } + + // RSA PSS is special because it encodes important parameters + // in the Parameters. + + var params pssParameters + if _, err := asn1.Unmarshal(ai.Parameters.FullBytes, ¶ms); err != nil { + return sa + } + + var mgf1HashFunc pkix.AlgorithmIdentifier + if _, err := asn1.Unmarshal(params.MGF.Parameters.FullBytes, &mgf1HashFunc); err != nil { + return sa + } + + // PSS is greatly overburdened with options. This code forces them into + // three buckets by requiring that the MGF1 hash function always match the + // message hash function (as recommended in RFC 3447, Section 8.1), that the + // salt length matches the hash length, and that the trailer field has the + // default value. + if (len(params.Hash.Parameters.FullBytes) != 0 && !bytes.Equal(params.Hash.Parameters.FullBytes, asn1.NullBytes)) || + !params.MGF.Algorithm.Equal(oidMGF1) || + !mgf1HashFunc.Algorithm.Equal(params.Hash.Algorithm) || + (len(mgf1HashFunc.Parameters.FullBytes) != 0 && !bytes.Equal(mgf1HashFunc.Parameters.FullBytes, asn1.NullBytes)) || + params.TrailerField != 1 { + return sa + } + + switch { + case params.Hash.Algorithm.Equal(oidSHA256) && params.SaltLength == 32: + sa.Name = x509.SHA256WithRSAPSS.String() + sa.algo = x509.SHA256WithRSAPSS + sa.hash = crypto.SHA256 + case params.Hash.Algorithm.Equal(oidSHA384) && params.SaltLength == 48: + sa.Name = x509.SHA384WithRSAPSS.String() + sa.algo = x509.SHA384WithRSAPSS + sa.hash = crypto.SHA384 + case params.Hash.Algorithm.Equal(oidSHA512) && params.SaltLength == 64: + sa.Name = x509.SHA512WithRSAPSS.String() + sa.algo = x509.SHA512WithRSAPSS + sa.hash = crypto.SHA512 + } + + return sa +} diff --git a/command/oauth/cmd.go b/command/oauth/cmd.go index cb00a340..917ff116 100644 --- a/command/oauth/cmd.go +++ b/command/oauth/cmd.go @@ -67,21 +67,25 @@ func init() { Usage: "authorization and single sign-on using OAuth & OIDC", UsageText: `**step oauth** [**--provider**=] [**--client-id**= **--client-secret**=] -[**--scope**= ...] [**--bare** [**--oidc**]] [**--header** [**--oidc**]] [**--prompt**=] +[**--scope**= ...] [**--bare** [**--oidc**]] [**--header** [**--oidc**]] +[**--prompt**=] [**--auth-param**=] **step oauth** **--authorization-endpoint**= **--token-endpoint**= **--client-id**= **--client-secret**= -[**--scope**= ...] [**--bare** [**--oidc**]] [**--header** [**--oidc**]] [**--prompt**=] +[**--scope**= ...] [**--bare** [**--oidc**]] [**--header** [**--oidc**]] +[**--prompt**=] [**--auth-param**=] **step oauth** [**--account**=] [**--authorization-endpoint**=] [**--token-endpoint**=] -[**--scope**= ...] [**--bare** [**--oidc**]] [**--header** [**--oidc**]] [**--prompt**=] +[**--scope**= ...] [**--bare** [**--oidc**]] [**--header** [**--oidc**]] +[**--prompt**=] [**--auth-param**=] **step oauth** **--account**= **--jwt** -[**--scope**= ...] [**--header**] [**-bare**] [**--prompt**=]`, +[**--scope**= ...] [**--header**] [**-bare**] [**--prompt**=] +[**--auth-param**=]`, Description: `**step oauth** command implements the OAuth 2.0 authorization flow. OAuth is an open standard for access delegation, commonly used as a way for @@ -135,6 +139,12 @@ Use a custom OAuth2.0 server: ''' $ step oauth --client-id my-client-id --client-secret my-client-secret \ --provider https://example.org +''' + +Use additional authentication parameters: +''' +$ step oauth --client-id my-client-id --client-secret my-client-secret \ + --provider https://example.org --auth-param "access_type=offline" '''`, Flags: []cli.Flag{ cli.StringFlag{ @@ -186,6 +196,12 @@ $ step oauth --client-id my-client-id --client-secret my-client-secret \ Name: "scope", Usage: "OAuth scopes", }, + cli.StringSliceFlag{ + Name: "auth-param", + Usage: `OAuth additional authentication parameters to include as part of the URL query. +Use this flag multiple times to add multiple parameters. This flag expects a +'key' and 'value' in the format '--auth-param "key=value"'.`, + }, cli.StringFlag{ Name: "prompt", Usage: `Whether the Authorization Server prompts the End-User for reauthentication and consent. @@ -336,7 +352,25 @@ func oauthCmd(c *cli.Context) error { prompt = c.String("prompt") } - o, err := newOauth(opts.Provider, clientID, clientSecret, authzEp, tokenEp, scope, prompt, opts) + authParams := url.Values{} + for _, keyval := range c.StringSlice("auth-param") { + parts := strings.SplitN(keyval, "=", 2) + var k, v string + switch len(parts) { + case 1: + k, v = parts[0], "" + case 2: + k, v = parts[0], parts[1] + default: + return errs.InvalidFlagValue(c, "auth-param", keyval, "") + } + if k == "" { + return errs.InvalidFlagValue(c, "auth-param", keyval, "") + } + authParams.Add(k, v) + } + + o, err := newOauth(opts.Provider, clientID, clientSecret, authzEp, tokenEp, scope, prompt, authParams, opts) if err != nil { return err } @@ -438,11 +472,12 @@ type oauth struct { CallbackPath string terminalRedirect string browser string + authParams url.Values errCh chan error tokCh chan *token } -func newOauth(provider, clientID, clientSecret, authzEp, tokenEp, scope, prompt string, opts *options) (*oauth, error) { +func newOauth(provider, clientID, clientSecret, authzEp, tokenEp, scope, prompt string, authParams url.Values, opts *options) (*oauth, error) { state, err := randutil.Alphanumeric(32) if err != nil { return nil, err @@ -479,6 +514,7 @@ func newOauth(provider, clientID, clientSecret, authzEp, tokenEp, scope, prompt CallbackPath: opts.CallbackPath, terminalRedirect: opts.TerminalRedirect, browser: opts.Browser, + authParams: authParams, errCh: make(chan error), tokCh: make(chan *token), }, nil @@ -519,6 +555,7 @@ func newOauth(provider, clientID, clientSecret, authzEp, tokenEp, scope, prompt CallbackPath: opts.CallbackPath, terminalRedirect: opts.TerminalRedirect, browser: opts.Browser, + authParams: authParams, errCh: make(chan error), tokCh: make(chan *token), }, nil @@ -902,6 +939,11 @@ func (o *oauth) Auth() (string, error) { if o.loginHint != "" { q.Add("login_hint", o.loginHint) } + for k, vs := range o.authParams { + for _, v := range vs { + q.Add(k, v) + } + } u.RawQuery = q.Encode() return u.String(), nil } diff --git a/command/ssh/certificate.go b/command/ssh/certificate.go index 15b2206c..1e2342e5 100644 --- a/command/ssh/certificate.go +++ b/command/ssh/certificate.go @@ -40,8 +40,8 @@ func certificateCommand() cli.Command { [**--add-user**] [**--not-before**=] [**--not-after**=] [**--token**=] [**--issuer**=] [**--no-password**] [**--insecure**] [**--force**] [**--x5c-cert**=] -[**--x5c-key**=] [**--k8ssa-token-path**=] [**--ca-url**=] -[**--root**=] [**--context**=]`, +[**--x5c-key**=] [**--k8ssa-token-path**=] [**--no-agent**] +[**--ca-url**=] [**--root**=] [**--context**=]`, Description: `**step ssh certificate** command generates an SSH key pair and creates a certificate using [step certificates](https://github.com/smallstep/certificates). @@ -95,6 +95,11 @@ Generate a new SSH key pair and user certificate: $ step ssh certificate mariano@work id_ecdsa ''' +Generate a new SSH key pair and user certificate and do not add to SSH agent: +''' +$ step ssh certificate mariano@work id_ecdsa --no-agent +''' + Generate a new SSH key pair and user certificate and set the lifetime to 2hrs: ''' $ step ssh certificate mariano@work id_ecdsa --not-after 2h @@ -170,6 +175,10 @@ $ step ssh certificate --token $TOKEN mariano@work id_ecdsa flags.NebulaCert, flags.NebulaKey, flags.K8sSATokenPathFlag, + cli.BoolFlag{ + Name: "no-agent", + Usage: "Do not add the generated certificate and associated private key to the SSH agent.", + }, flags.CaConfig, flags.CaURL, flags.Root, @@ -462,7 +471,7 @@ func certificateAction(ctx *cli.Context) error { ui.PrintSelected("Certificate", crtFile) // Attempt to add key to agent if private key defined. - if priv != nil && certType == provisioner.SSHUserCert { + if !ctx.Bool("no-agent") && priv != nil && certType == provisioner.SSHUserCert { if agent, err := sshutil.DialAgent(); err != nil { ui.Printf(`{{ "%s" | red }} {{ "SSH Agent:" | bold }} %v`+"\n", ui.IconBad, err) } else { diff --git a/debian/postinstall.sh b/debian/postinstall.sh deleted file mode 100644 index 40e825ee..00000000 --- a/debian/postinstall.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh - -if [ "$1" = "configure" ]; then - if [ -f /usr/share/bash-completion/completions/step-cli ]; then - update-alternatives \ - --install /usr/bin/step step /usr/bin/step-cli 50 \ - --slave /usr/share/bash-completion/completions/step step.bash-completion /usr/share/bash-completion/completions/step-cli - fi - if [ -f /etc/bash_completion.d/step-cli ]; then - update-alternatives \ - --install /usr/bin/step step /usr/bin/step-cli 50 \ - --slave /etc/bash_completion.d/step step.bash-completion /etc/bash_completion.d/step-cli - fi -fi diff --git a/debian/preremove.sh b/debian/preremove.sh deleted file mode 100644 index 777ec5cd..00000000 --- a/debian/preremove.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -if [ "$1" = "remove" ]; then - update-alternatives --remove step /usr/bin/step-cli -fi diff --git a/flags/flags.go b/flags/flags.go index 9c259b1c..cac0af8e 100644 --- a/flags/flags.go +++ b/flags/flags.go @@ -117,6 +117,18 @@ be written to disk unencrypted. This is not recommended. Requires **--insecure** certificate.`, } + // Limit is a cli.Flag used to limit the number of entities returned in API requests. + Limit = cli.UintFlag{ + Name: "limit", + Usage: `The number of entities to return per (paging) API request.`, + } + + // NoPager is a cli.Flag used to disable usage of $PAGER for paging purposes. + NoPager = cli.BoolFlag{ + Name: "no-pager", + Usage: `Disables usage of $PAGER for paging purposes`, + } + // NotBefore is a cli.Flag used to pass the start period of the certificate // validity. NotBefore = cli.StringFlag{ diff --git a/go.mod b/go.mod index 6846224f..b55049bb 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/shurcooL/sanitized_anchor_name v1.0.0 github.com/slackhq/nebula v1.5.2 github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 - github.com/smallstep/certificates v0.18.1-rc1.0.20220112015040-57f9e5415160 + github.com/smallstep/certificates v0.18.1-rc1.0.20220131122744-4a0cfd24e568 github.com/smallstep/certinfo v1.6.0 github.com/smallstep/truststore v0.10.1 github.com/smallstep/zcrypto v0.0.0-20210924233136-66c2600f6e71 @@ -25,12 +25,12 @@ require ( github.com/stretchr/testify v1.7.0 github.com/urfave/cli v1.22.5 go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 - go.step.sm/cli-utils v0.7.1 - go.step.sm/crypto v0.14.0 - go.step.sm/linkedca v0.9.0 + go.step.sm/cli-utils v0.7.2 + go.step.sm/crypto v0.15.0 + go.step.sm/linkedca v0.9.2 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 - golang.org/x/net v0.0.0-20220105145211-5b0dc2dfae98 - golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e + golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d + golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 google.golang.org/protobuf v1.27.1 gopkg.in/square/go-jose.v2 v2.6.0 diff --git a/go.sum b/go.sum index 89325222..4d0b01bf 100644 --- a/go.sum +++ b/go.sum @@ -207,10 +207,14 @@ github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403 h1:cqQfy1jclcSy/FwLjemeg3SR1yaINm74aQyupQ0Bl8M= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed h1:OZmjad4L3H8ncOIR8rnb5MREYqG8ixi5+WbeUsquF0c= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 h1:hzAQntlaYRkVSFEfj9OTWlVV1H155FMD8BTKktLv0QI= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1 h1:zH8ljVhhq7yC0MIeUL/IviMtY8hx2mK8cN9wEYb8ggw= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5 h1:xD/lrqdvwsc+O2bjSSi3YqY73Ke3LAiSCx49aCesA0E= github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= @@ -283,8 +287,9 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0 h1:dulLQAYQFYtG5MTplgNGHWuV2D+OBD+Z8lmDBmbLg+s= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021 h1:fP+fF0up6oPY49OrjPrhIJ8yQfdIM85NXMLkMg1EXVs= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.3.0-java h1:bV5JGEB1ouEzZa0hgVDFFiClrUEuGWRaAc/3mxR2QK0= github.com/envoyproxy/protoc-gen-validate v0.3.0-java/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= @@ -789,8 +794,8 @@ github.com/slackhq/nebula v1.5.2/go.mod h1:xaCM6wqbFk/NRmmUe1bv88fWBm3a1UioXJVIp github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5/go.mod h1:TC9A4+RjIOS+HyTH7wG17/gSqVv95uDw2J64dQZx7RE= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= -github.com/smallstep/certificates v0.18.1-rc1.0.20220112015040-57f9e5415160 h1:F6rpaAIbg8JcW7E+hflQPz8mfUe8xC0LUgnIromBzag= -github.com/smallstep/certificates v0.18.1-rc1.0.20220112015040-57f9e5415160/go.mod h1:G+ZCrHnwT9k6M3bQjvyB5LcmFEeuPO3zudFDgXdmZ2M= +github.com/smallstep/certificates v0.18.1-rc1.0.20220131122744-4a0cfd24e568 h1:RdjkWAbEbT/uAPogNTlu3KU/r7Lyo4fU0d/KGVgaDtQ= +github.com/smallstep/certificates v0.18.1-rc1.0.20220131122744-4a0cfd24e568/go.mod h1:1p5avcwsX+CYrFWD1OTUnpzi4b8wd7LbFYn/OEIH7Q4= github.com/smallstep/certinfo v1.6.0 h1:o1eS9+iE6OPLRdnRFiYqAtXYR2FioNUt8q4CIj7X3Nk= github.com/smallstep/certinfo v1.6.0/go.mod h1:DsKAlSDLWsywdiVBCfqqVdRuny77wqiI+NFskLM7Ods= github.com/smallstep/nosql v0.3.9 h1:YPy5PR3PXClqmpFaVv0wfXDXDc7NXGBE1auyU2c87dc= @@ -970,13 +975,13 @@ go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16g go.opentelemetry.io/proto/otlp v0.7.0 h1:rwOQPCuKAKmwGKq2aVNnYIibI6wnV7EvzgfTCzcdGg8= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.step.sm/cli-utils v0.7.0/go.mod h1:Ur6bqA/yl636kCUJbp30J7Unv5JJ226eW2KqXPDwF/E= -go.step.sm/cli-utils v0.7.1 h1:+ZCRWbSc3gfj+LA1d6GgjtJXIljl3qxnrufgRF851bs= -go.step.sm/cli-utils v0.7.1/go.mod h1:Ur6bqA/yl636kCUJbp30J7Unv5JJ226eW2KqXPDwF/E= +go.step.sm/cli-utils v0.7.2 h1:kUNNhGRWAad3bLkhvbLjVr3Dqs5DgxCZQcUspWaQCIQ= +go.step.sm/cli-utils v0.7.2/go.mod h1:Ur6bqA/yl636kCUJbp30J7Unv5JJ226eW2KqXPDwF/E= go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0= -go.step.sm/crypto v0.14.0 h1:HzSkUDwqKhODKpsTxevJz956U2xVDZ3sDdGQVwR6Ttw= -go.step.sm/crypto v0.14.0/go.mod h1:3G0yQr5lQqfEG0CMYz8apC/qMtjLRQlzflL2AxkcN+g= -go.step.sm/linkedca v0.9.0 h1:xKXZoRXy4B7LeGBZozq62IQ0p3v8dT33O9UOMpVtRtI= -go.step.sm/linkedca v0.9.0/go.mod h1:5uTRjozEGSPAZal9xJqlaD38cvJcLe3o1VAFVjqcORo= +go.step.sm/crypto v0.15.0 h1:VioBln+x3+RoejgeBhvxkLGVYdWRy6PFiAaUUN29/E0= +go.step.sm/crypto v0.15.0/go.mod h1:3G0yQr5lQqfEG0CMYz8apC/qMtjLRQlzflL2AxkcN+g= +go.step.sm/linkedca v0.9.2 h1:CpAkd174sLXFfrOZrbPEiTzik91QRj3+L0omsiwsiok= +go.step.sm/linkedca v0.9.2/go.mod h1:5uTRjozEGSPAZal9xJqlaD38cvJcLe3o1VAFVjqcORo= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -1117,8 +1122,8 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220105145211-5b0dc2dfae98 h1:+6WJMRLHlD7X7frgp7TUZ36RnQzSf9wVVTNakEp+nqY= -golang.org/x/net v0.0.0-20220105145211-5b0dc2dfae98/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d h1:1n1fc535VhN8SYtD4cDUyNlfpAF2ROMM9+11equK3hs= +golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1232,8 +1237,8 @@ golang.org/x/sys v0.0.0-20210915083310-ed5796bab164/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211031064116-611d5d643895/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1324,7 +1329,6 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1431,8 +1435,8 @@ google.golang.org/genproto v0.0.0-20210429181445-86c259c2b4ab/go.mod h1:P3QM42oQ google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210719143636-1d5a45f8e492 h1:7yQQsvnwjfEahbNNEKcBHv3mR+HnB1ctGY/z1JXzx8M= -google.golang.org/genproto v0.0.0-20210719143636-1d5a45f8e492/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5 h1:zzNejm+EgrbLfDZ6lu9Uud2IVvHySPl8vQzf04laR5Q= +google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -1462,8 +1466,9 @@ google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.39.0 h1:Klz8I9kdtkIN6EpHHUOMLCYhTn/2WAe5a0s1hcBkdTI= -google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.43.0 h1:Eeu7bZtDZ2DpRCsLhUlcrLnvYaMK1Gz86a+hMVvELmM= +google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= diff --git a/scripts/postinstall.sh b/scripts/postinstall.sh new file mode 100644 index 00000000..f7ac21b8 --- /dev/null +++ b/scripts/postinstall.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +updateAlternatives() { + update-alternatives \ + --install /usr/bin/step step /usr/bin/step-cli 50 \ + --slave /usr/share/bash-completion/completions/step step.bash-completion /usr/share/bash-completion/completions/step-cli +} + +cleanInstall() { + updateAlternatives +} + +upgrade() { + updateAlternatives +} + +action="$1" +if [ "$1" = "configure" ] && [ -z "$2" ]; then + action="install" +elif [ "$1" = "configure" ] && [ -n "$2" ]; then + action="upgrade" +fi + +case "$action" in + "1" | "install") + cleanInstall + ;; + "2" | "upgrade") + upgrade + ;; + *) + cleanInstall + ;; +esac diff --git a/scripts/postremove.sh b/scripts/postremove.sh new file mode 100644 index 00000000..1f5e7a4a --- /dev/null +++ b/scripts/postremove.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +removeAlternatives() { + update-alternatives --remove step /usr/bin/step-cli +} + +upgrade() { + : +} + +remove() { + removeAlternatives +} + +action="$1" +if [ "$1" = "remove" ]; then + action="remove" +elif [ "$1" = "upgrade" ] && [ -n "$2" ]; then + action="upgrade" +elif [ "$1" = "disappear" ]; then + action="remove" +fi + +case "$action" in + "0" | "remove") + remove + ;; + "1" | "upgrade") + upgrade + ;; + *) + remove + ;; +esac diff --git a/utils/cautils/acmeutils.go b/utils/cautils/acmeutils.go index c5fd964a..0eeb51d5 100644 --- a/utils/cautils/acmeutils.go +++ b/utils/cautils/acmeutils.go @@ -3,11 +3,13 @@ package cautils import ( "context" "crypto/rand" + "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/json" "encoding/pem" "fmt" + "net" "net/http" "os" "strings" @@ -267,18 +269,18 @@ func finalizeOrder(ac *ca.ACMEClient, o *acme.Order, csr *x509.CertificateReques return fo, nil } -func validateSANsForACME(sans []string) ([]string, error) { +func validateSANsForACME(sans []string) ([]string, []net.IP, error) { dnsNames, ips, emails, uris := splitSANs(sans) - if len(ips) > 0 || len(emails) > 0 || len(uris) > 0 { - return nil, errors.New("IP Address, Email Address, and URI SANs are not supported for ACME flow") + if len(emails) > 0 || len(uris) > 0 { + return nil, nil, errors.New("Email Address and URI SANs are not supported for ACME flow") } for _, dns := range dnsNames { if strings.Contains(dns, "*") { - return nil, errors.Errorf("wildcard dnsnames (%s) require dns validation, "+ + return nil, nil, errors.Errorf("wildcard dnsnames (%s) require dns validation, "+ "which is currently not implemented in this client", dns) } } - return dnsNames, nil + return dnsNames, ips, nil } type acmeFlowOp func(*acmeFlow) error @@ -360,8 +362,45 @@ func newACMEFlow(ctx *cli.Context, ops ...acmeFlowOp) (*acmeFlow, error) { return af, nil } +func (af *acmeFlow) getClientTruststoreOption(mergeRootCAs bool) (ca.ClientOption, error) { + root := "" + if af.ctx.IsSet("root") { + root = af.ctx.String("root") + // If there's an error reading the local root ca, ignore the error and use the system store + } else if _, err := os.Stat(pki.GetRootCAPath()); err == nil { + root = pki.GetRootCAPath() + } + + // 1. Merge local RootCA with system store + if mergeRootCAs && len(root) > 0 { + rootCAs, err := x509.SystemCertPool() + if err != nil || rootCAs == nil { + rootCAs = x509.NewCertPool() + } + + cert, err := os.ReadFile(root) + if err != nil { + return nil, errors.Wrap(err, "failed to read local root ca") + } + + if ok := rootCAs.AppendCertsFromPEM(cert); !ok { + return nil, errors.New("failed to append local root ca to system cert pool") + } + + return ca.WithTransport(&http.Transport{TLSClientConfig: &tls.Config{RootCAs: rootCAs}}), nil + } + + // Use local Root CA only + if len(root) > 0 { + return ca.WithRootFile(root), nil + } + + // Use system store only + return ca.WithTransport(http.DefaultTransport), nil +} + func (af *acmeFlow) GetCertificate() ([]*x509.Certificate, error) { - dnsNames, err := validateSANsForACME(af.sans) + dnsNames, ips, err := validateSANsForACME(af.sans) if err != nil { return nil, err } @@ -373,19 +412,31 @@ func (af *acmeFlow) GetCertificate() ([]*x509.Certificate, error) { Value: dns, }) } + for _, ip := range ips { + idents = append(idents, acme.Identifier{ + Type: "ip", + Value: ip.String(), + }) + } var ( orderPayload []byte clientOps []ca.ClientOption ) + + ops, err := af.getClientTruststoreOption(af.ctx.IsSet("acme")) + if err != nil { + return nil, err + } + + clientOps = append(clientOps, ops) + if strings.Contains(af.acmeDir, "letsencrypt") { // LetsEncrypt does not support NotBefore and NotAfter attributes in orders. if af.ctx.IsSet("not-before") || af.ctx.IsSet("not-after") { return nil, errors.New("LetsEncrypt public CA does not support NotBefore/NotAfter " + "attributes for certificates. Instead, each certificate has a default lifetime of 3 months.") } - // Use default transport for public CAs - clientOps = append(clientOps, ca.WithTransport(http.DefaultTransport)) // LetsEncrypt requires that the Common Name of the Certificate also be // represented as a DNSName in the SAN extension, and therefore must be // authorized as part of the ACME order. @@ -409,21 +460,39 @@ func (af *acmeFlow) GetCertificate() ([]*x509.Certificate, error) { return nil, errors.Wrap(err, "error marshaling new letsencrypt order request") } } else { - // If the CA is not public then a root file is required. - root := af.ctx.String("root") - if root == "" { - root = pki.GetRootCAPath() - if _, err := os.Stat(root); err != nil { - return nil, errs.RequiredFlag(af.ctx, "root") - } - } - clientOps = append(clientOps, ca.WithRootFile(root)) // parse times or durations nbf, naf, err := flags.ParseTimeDuration(af.ctx) if err != nil { return nil, err } + // check if the list of identifiers for which to + // request a certificate already contains the subject + hasSubject := false + for _, n := range idents { + if n.Value == af.subject { + hasSubject = true + } + } + // if the subject is not yet included in the slice + // of identifiers, it is added to either the DNS names + // or IP addresses slice and the corresponding type of + // identifier is added to the slice of identifers. + if !hasSubject { + if ip := net.ParseIP(af.subject); ip != nil { + ips = append(ips, ip) + idents = append(idents, acme.Identifier{ + Type: "ip", + Value: ip.String(), + }) + } else { + dnsNames = append(dnsNames, af.subject) + idents = append(idents, acme.Identifier{ + Type: "dns", + Value: af.subject, + }) + } + } nor := acmeAPI.NewOrderRequest{ Identifiers: idents, NotAfter: naf.Time(), @@ -464,7 +533,8 @@ func (af *acmeFlow) GetCertificate() ([]*x509.Certificate, error) { Subject: pkix.Name{ CommonName: af.subject, }, - DNSNames: dnsNames, + DNSNames: dnsNames, + IPAddresses: ips, } var csrBytes []byte csrBytes, err = x509.CreateCertificateRequest(rand.Reader, _csr, af.priv)