Skip to content

Commit

Permalink
Merge pull request #122 from smallstep/max/acme-standalone
Browse files Browse the repository at this point in the history
ACME client support for standalone and webroot mode
  • Loading branch information
dopey authored Sep 13, 2019
2 parents e097873 + 1868ec3 commit 7e4724b
Show file tree
Hide file tree
Showing 13 changed files with 810 additions and 32 deletions.
14 changes: 9 additions & 5 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

94 changes: 89 additions & 5 deletions command/ca/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ func certificateCommand() cli.Command {
Action: command.ActionFunc(certificateAction),
Usage: "generate a new private key and certificate signed by the root certificate",
UsageText: `**step ca certificate** <subject> <crt-file> <key-file>
[**--token**=<token>] [**--issuer**=<name>] [**--ca-url**=<uri>] [**--root**=<file>]
[**--not-before**=<time|duration>] [**--not-after**=<time|duration>] [**--san**=<SAN>]
[**--token**=<token>] [**--issuer**=<name>] [**--ca-url**=<uri>] [**--root**=<file>]
[**--not-before**=<time|duration>] [**--not-after**=<time|duration>]
[**--san**=<SAN>] [**--acme**=<path>] [**--standalone**] [**--webroot**=<path>]
[**--contact**=<email>] [**--http-listen**=<address>] [**--bundle**]
[**--kty**=<type>] [**--curve**=<curve>] [**--size**=<size>] [**--console**]`,
Description: `**step ca certificate** command generates a new certificate pair
Expand Down Expand Up @@ -82,8 +84,36 @@ $ step ca certificate [email protected] joe.crt joe.key --issuer Google --console
Request a new certificate with an RSA public key (default is ECDSA256):
'''
$ step ca certificate foo.internal foo.crt foo.key --kty RSA --size 4096
'''
**step CA ACME** - In order to use the step CA ACME protocol you must add a
ACME provisioner to the step CA config. See **step ca provisioner add -h**.
Request a new certificate using the step CA ACME server and a standalone server
to serve the challenges locally (standalone mode is the default):
'''
$ step ca certificate foobar foo.crt foo.key --provisioner my-acme-provisioner --san foo.internal --san bar.internal
'''
Request a new certificate using the step CA ACME server and an existing server
along with webroot mode to serve the challenges locally:
'''
$ step ca certificate foobar foo.crt foo.key --provisioner my-acme-provisioner --webroot "./acme-www" \
--san foo.internal --san bar.internal
'''
Request a new certificate using the ACME protocol not served via the step CA
(e.g. letsencrypt). NOTE: Let's Encrypt requires that the Subject Common Name
of a requested certificate be validated as an Identifier in the ACME order along
with any other SANS. Therefore, the Common Name must be a valid DNS Name. The
step CA does not impose this requirement.
'''
$ step ca certificate foo.internal foo.crt foo.key \
--acme https://acme-staging-v02.api.letsencrypt.org/directory --san bar.internal
'''`,
Flags: []cli.Flag{
consoleFlag,
flags.CaConfig,
flags.CaURL,
flags.Curve,
flags.Force,
Expand All @@ -95,8 +125,6 @@ $ step ca certificate foo.internal foo.crt foo.key --kty RSA --size 4096
flags.Size,
flags.Token,
flags.Offline,
flags.CaConfig,
consoleFlag,
cli.StringSliceFlag{
Name: "san",
Usage: `Add DNS Name, IP Address, or Email Address Subjective Alternative Names (SANs)
Expand All @@ -105,6 +133,51 @@ this token must match the complete set of subjective alternative names in the
token 1:1. Use the '--san' flag multiple times to configure multiple SANs. The
'--san' flag and the '--token' flag are mutually exlusive.`,
},
cli.StringFlag{
Name: "acme",
Usage: `ACME directory URL to be used for requesting certificates via the ACME protocol.
Use this flag to define an ACME server other than the Step CA. If this flag is
absent and an ACME provisioner has been selected then the '--ca-url' flag must be defined.`,
},
cli.BoolFlag{
Name: "standalone",
Usage: `Get a certificate using the ACME protocol and standalone mode for validation.
Standalone is a mode in which the step process will run a server that will
will respond to ACME challenge validation requests. Standalone is the default
mode for serving challenge validation requests.`,
},
cli.StringFlag{
Name: "webroot",
Usage: `Get a certificate using the ACME protocol and webroot mode for validation.
Webroot is a mode in which the step process will write a challenge file to a location
being served by an existing fileserver in order to respond to ACME challenge
validation requests.`,
},
cli.StringSliceFlag{
Name: "contact",
Usage: `Email addresses for contact as part of the ACME protocol. These contacts
may be used to warn of certificate expration or other certificate lifetime events.
Use the '--contact' flag multiple times to configure multiple contacts.`,
},
cli.StringFlag{
Name: "http-listen",
Usage: `Use a non-standard http address, behind a reverse proxy or load balancer, for
serving ACME challenges. The default address is :80, which requires super user
(sudo) privileges. This flag must be used in conjunction with the '--standalone'
flag.`,
Value: ":80",
},
/*
TODO: Not implemented yet.
cli.StringFlag{
Name: "https-listen",
Usage: `Use a non-standard https address, behind a reverse proxy or load balancer, for
serving ACME challenges. The default address is :443, which requires super user
(sudo) privileges. This flag must be used in conjunction with the '--standalone'
flag.`,
Value: ":443",
},
*/
},
}
}
Expand All @@ -117,6 +190,7 @@ func certificateAction(ctx *cli.Context) error {
args := ctx.Args()
subject := args.Get(0)
crtFile, keyFile := args.Get(1), args.Get(2)

tok := ctx.String("token")
offline := ctx.Bool("offline")
sans := ctx.StringSlice("san")
Expand All @@ -134,8 +208,18 @@ func certificateAction(ctx *cli.Context) error {
}

if len(tok) == 0 {
// Use the ACME protocol with a different certificate authority.
if ctx.IsSet("acme") {
return cautils.ACMECreateCertFlow(ctx, "")
}
if tok, err = flow.GenerateToken(ctx, subject, sans); err != nil {
return err
switch k := err.(type) {
// Use the ACME flow with the step certificate authority.
case *cautils.ErrACMEToken:
return cautils.ACMECreateCertFlow(ctx, k.Name)
default:
return err
}
}
}

Expand Down
32 changes: 31 additions & 1 deletion command/ca/provisioner/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ func addCommand() cli.Command {
[**--aws-account**=<id>]
[**--gcp-service-account**=<name>] [**--gcp-project**=<name>]
[**--azure-tenant**=<id>] [**--azure-resource-group**=<name>]
[**--instance-age**=<duration>] [**--disable-custom-sans**] [**--disable-trust-on-first-use**]`,
[**--instance-age**=<duration>] [**--disable-custom-sans**] [**--disable-trust-on-first-use**]
**step ca provisioner add** <name> **--type**=ACME **--ca-config**=<file>`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "ca-config",
Expand All @@ -57,6 +59,9 @@ and must be one of:
**Azure**
: Uses Microsoft Azure identity tokens.
**ACME**
: Uses the ACME protocol to create certificates.
`,
},
cli.BoolFlag{
Expand Down Expand Up @@ -211,6 +216,10 @@ document and will allow multiple certificates from the same instance:
'''
$ step ca provisioner add Amazon --type AWS --ca-config ca.json \
--aws-account 123456789 --disable-custom-sans --disable-trust-on-first-use
Add an ACME provisioner.
'''
$ step ca provisioner add acme-smallstep --type ACME --ca-config ca.json
'''`,
}
}
Expand Down Expand Up @@ -255,6 +264,8 @@ func addAction(ctx *cli.Context) (err error) {
list, err = addAzureProvisioner(ctx, name, provMap)
case provisioner.TypeGCP:
list, err = addGCPProvisioner(ctx, name, provMap)
case provisioner.TypeACME:
list, err = addACMEProvisioner(ctx, name, provMap)
default:
return errors.Errorf("unknown type %s: this should not happen", typ)
}
Expand Down Expand Up @@ -478,6 +489,23 @@ func addGCPProvisioner(ctx *cli.Context, name string, provMap map[string]bool) (
return
}

func addACMEProvisioner(ctx *cli.Context, name string, provMap map[string]bool) (list provisioner.List, err error) {
p := &provisioner.ACME{
Type: provisioner.TypeACME.String(),
Name: name,
}

// Check for duplicates
if _, ok := provMap[p.GetID()]; !ok {
provMap[p.GetID()] = true
} else {
return nil, errors.Errorf("duplicated provisioner: CA config already contains a provisioner with ID==%s", p.GetID())
}

list = append(list, p)
return
}

func parseIntaceAge(ctx *cli.Context) (provisioner.Duration, error) {
age := ctx.Duration("instance-age")
if age == 0 {
Expand All @@ -502,6 +530,8 @@ func parseProvisionerType(ctx *cli.Context) (provisioner.Type, error) {
return provisioner.TypeAWS, nil
case "azure":
return provisioner.TypeAzure, nil
case "acme":
return provisioner.TypeACME, nil
default:
return 0, errs.InvalidFlagValue(ctx, "type", typ, "JWK, OIDC, AWS, Azure, GCP")
}
Expand Down
14 changes: 11 additions & 3 deletions command/ca/provisioner/remove.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ and must be one of:
: Use Google instance identity tokens.
**Azure**
: Uses Microsoft Azure identity tokens.`,
: Uses Microsoft Azure identity tokens.
**ACME**
: Uses ACME protocol.`,
},
},
Description: `**step ca provisioner remove** removes one or more provisioners
Expand Down Expand Up @@ -84,6 +87,11 @@ $ step ca provisioner remove Google --ca-config ca.json \
Remove the cloud identity provisioner given name and a type:
'''
$ step ca provisioner remove Amazon --ca-config ca.json --type AWS
'''
Remove the ACME provisioner by name:
'''
$ step ca provisioner remove Amazon --ca-config ca.json --type AWS
'''`,
}
}
Expand Down Expand Up @@ -145,8 +153,8 @@ func removeAction(ctx *cli.Context) error {
if clientID != "" && pp.ClientID != clientID {
provisioners = append(provisioners, p)
}
case *provisioner.AWS, *provisioner.Azure, *provisioner.GCP:
// they are filtered by type
case *provisioner.AWS, *provisioner.Azure, *provisioner.GCP, *provisioner.ACME:
// they are filtered by type and name.
default:
continue
}
Expand Down
Loading

0 comments on commit 7e4724b

Please sign in to comment.