1
0
mirror of https://github.com/smallstep/cli.git synced 2025-08-07 16:02:54 +03:00

Merge branch 'master' into nebulous

This commit is contained in:
Mariano Cano
2022-02-01 16:39:53 -08:00
28 changed files with 1830 additions and 135 deletions

View File

@@ -20,6 +20,7 @@ builds:
- linux_386
- linux_amd64
- linux_arm64
- linux_arm_5
- linux_arm_6
- linux_arm_7
- linux_mips
@@ -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:

View File

@@ -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

View File

@@ -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"

19
command/ca/acme/acme.go Normal file
View File

@@ -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** <subcommand> [arguments] [global-flags] [subcommand-flags]",
Description: `**step beta ca acme** command group provides facilities for managing ACME.`,
Subcommands: cli.Commands{
eab.Command(),
},
}
}

View File

@@ -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** <provisioner> [<reference>]
[**--admin-cert**=<file>] [**--admin-key**=<file>]
[**--admin-provisioner**=<string>] [**--admin-subject**=<string>]
[**--password-file**=<file>] [**--ca-url**=<uri>] [**--root**=<file>]
[**--context**=<name>]`,
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
<provisioner>
: Name of the provisioner to which the ACME EAB key will be added
<reference>
: (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
}

View File

@@ -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** <subcommand> [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 <provisioner>
'''
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
'''
`,
}
}

178
command/ca/acme/eab/list.go Normal file
View File

@@ -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** <provisioner> [<reference>]
[**--limit**=<number>] [**--admin-cert**=<file>] [**--admin-key**=<file>]
[**--admin-provisioner**=<string>] [**--admin-subject**=<string>]
[**--password-file**=<file>] [**--ca-url**=<uri>] [**--root**=<file>]
[**--context**=<name>]`,
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
<provisioner>
: Name of the provisioner to list ACME EAB keys for
<reference>
: (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)
}
}

View File

@@ -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** <provisioner> <key_id>
[**--admin-cert**=<file>] [**--admin-key**=<file>]
[**--admin-provisioner**=<string>] [**--admin-subject**=<string>]
[**--password-file**=<file>] [**--ca-url**=<uri>] [**--root**=<file>]
[**--context**=<name>]`,
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
<provisioner>
: Name of the provisioner to remove an ACME EAB key for
<key_id>
: 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
}

View File

@@ -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(),
},
}
}

View File

@@ -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 {

View File

@@ -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)

View File

@@ -76,10 +76,16 @@ func addCommand() cli.Command {
[**--admin-subject**=<subject>] [**--password-file**=<file>] [**--ca-url**=<uri>]
[**--root**=<file>] [**--context**=<name>]
**step beta ca provisioner add** <name> **--type**=ACME [**--force-cn**]
**step beta ca provisioner add** <name> **--type**=ACME [**--force-cn**] [**--require-eab**]
[**--admin-cert**=<file>] [**--admin-key**=<file>] [**--admin-provisioner**=<name>]
[**--admin-subject**=<subject>] [**--password-file**=<file>] [**--ca-url**=<uri>]
[**--root**=<file>] [**--context**=<name>]`,
[**--root**=<file>] [**--context**=<name>]
**step beta ca provisioner add** <name> **--type**=SCEP [**--force-cn**] [**--challenge**=<challenge>]
[**--capabilities**=<capabilities>] [**--include-root**] [**--min-public-key-length**=<length>]
[**--encryption-algorithm-identifier**=<id>] [**--admin-cert**=<file>] [**--admin-key**=<file>]
[**--admin-provisioner**=<string>] [**--admin-subject**=<string>] [**--password-file**=<file>]
[**--ca-url**=<uri>] [**--root**=<file>] [**--context**=<name>]`,
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
}

View File

@@ -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 <challenge> to use as a shared secret between a client and the CA`,
}
scepCapabilitiesFlag = cli.StringSliceFlag{
Name: "capabilities",
Usage: `The SCEP <capabilities> 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 <length> of the SCEP RSA encryption key`,
}
scepEncryptionAlgorithmIdentifierFlag = cli.IntFlag{
Name: "encryption-algorithm-identifier",
Usage: `The <id> 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{

View File

@@ -39,7 +39,7 @@ func updateCommand() cli.Command {
ACME
**step beta ca provisioner update** <name> [**--force-cn**]
**step beta ca provisioner update** <name> [**--force-cn**] [**--require-eab**] [**--disable-eab**]
[**--admin-cert**=<file>] [**--admin-key**=<file>] [**--admin-provisioner**=<name>]
[**--admin-subject**=<subject>] [**--password-file**=<file>] [**--ca-url**=<uri>]
[**--root**=<file>] [**--context**=<name>]
@@ -81,7 +81,14 @@ IID (AWS/GCP/Azure)
[**--disable-custom-sans**] [**--disable-trust-on-first-use**]
[**--admin-cert**=<file>] [**--admin-key**=<file>] [**--admin-provisioner**=<name>]
[**--admin-subject**=<subject>] [**--password-file**=<file>] [**--ca-url**=<uri>]
[**--root**=<file>] [**--context**=<name>]`,
[**--root**=<file>] [**--context**=<name>]
**step beta ca provisioner update** <name> [**--force-cn**] [**--challenge**=<challenge>]
[**--capabilities**=<capabilities>] [**--include-root**] [**--minimum-public-key-length**=<length>]
[**--encryption-algorithm-identifier**=<id>] [**--admin-cert**=<file>] [**--admin-key**=<file>]
[**--admin-provisioner**=<name>] [**--admin-subject**=<subject>] [**--password-file**=<file>]
[**--ca-url**=<uri>] [**--root**=<file>] [**--context**=<name>]
`,
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
}

29
command/crl/crl.go Normal file
View File

@@ -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** <subcommand> [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)
}

View File

@@ -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 != '<27>' {
sanitized[i] = r
} else {
sanitized[i] = '.'
}
}
return string(sanitized)
}

523
command/crl/inspect.go Normal file
View File

@@ -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** <file|url>`,
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
<file|url>
: 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.
: <format> 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 <file> 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.
: <roots> 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)
}

View File

@@ -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, &params); 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
}

View File

@@ -67,21 +67,25 @@ func init() {
Usage: "authorization and single sign-on using OAuth & OIDC",
UsageText: `**step oauth**
[**--provider**=<provider>] [**--client-id**=<client-id> **--client-secret**=<client-secret>]
[**--scope**=<scope> ...] [**--bare** [**--oidc**]] [**--header** [**--oidc**]] [**--prompt**=<prompt>]
[**--scope**=<scope> ...] [**--bare** [**--oidc**]] [**--header** [**--oidc**]]
[**--prompt**=<prompt>] [**--auth-param**=<key=value>]
**step oauth**
**--authorization-endpoint**=<authorization-endpoint>
**--token-endpoint**=<token-endpoint>
**--client-id**=<client-id> **--client-secret**=<client-secret>
[**--scope**=<scope> ...] [**--bare** [**--oidc**]] [**--header** [**--oidc**]] [**--prompt**=<prompt>]
[**--scope**=<scope> ...] [**--bare** [**--oidc**]] [**--header** [**--oidc**]]
[**--prompt**=<prompt>] [**--auth-param**=<key=value>]
**step oauth** [**--account**=<account>]
[**--authorization-endpoint**=<authorization-endpoint>]
[**--token-endpoint**=<token-endpoint>]
[**--scope**=<scope> ...] [**--bare** [**--oidc**]] [**--header** [**--oidc**]] [**--prompt**=<prompt>]
[**--scope**=<scope> ...] [**--bare** [**--oidc**]] [**--header** [**--oidc**]]
[**--prompt**=<prompt>] [**--auth-param**=<key=value>]
**step oauth** **--account**=<account> **--jwt**
[**--scope**=<scope> ...] [**--header**] [**-bare**] [**--prompt**=<prompt>]`,
[**--scope**=<scope> ...] [**--header**] [**-bare**] [**--prompt**=<prompt>]
[**--auth-param**=<key=value>]`,
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
}

View File

@@ -40,8 +40,8 @@ func certificateCommand() cli.Command {
[**--add-user**] [**--not-before**=<time|duration>]
[**--not-after**=<time|duration>] [**--token**=<token>] [**--issuer**=<name>]
[**--no-password**] [**--insecure**] [**--force**] [**--x5c-cert**=<file>]
[**--x5c-key**=<file>] [**--k8ssa-token-path**=<file>] [**--ca-url**=<uri>]
[**--root**=<file>] [**--context**=<name>]`,
[**--x5c-key**=<file>] [**--k8ssa-token-path**=<file>] [**--no-agent**]
[**--ca-url**=<uri>] [**--root**=<file>] [**--context**=<name>]`,
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 {

14
debian/postinstall.sh vendored
View File

@@ -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

5
debian/preremove.sh vendored
View File

@@ -1,5 +0,0 @@
#!/bin/sh
if [ "$1" = "remove" ]; then
update-alternatives --remove step /usr/bin/step-cli
fi

View File

@@ -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{

12
go.mod
View File

@@ -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

45
go.sum
View File

@@ -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=

34
scripts/postinstall.sh Normal file
View File

@@ -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

34
scripts/postremove.sh Normal file
View File

@@ -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

View File

@@ -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)