You've already forked step-ca-cli
mirror of
https://github.com/smallstep/cli.git
synced 2026-10-24 11:19:19 +03:00
935 lines
28 KiB
Go
935 lines
28 KiB
Go
package certificate
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/urfave/cli"
|
|
|
|
"github.com/smallstep/cli-utils/command"
|
|
"github.com/smallstep/cli-utils/errs"
|
|
"github.com/smallstep/cli-utils/fileutil"
|
|
"github.com/smallstep/cli-utils/ui"
|
|
"go.step.sm/crypto/keyutil"
|
|
"go.step.sm/crypto/pemutil"
|
|
"go.step.sm/crypto/x509util"
|
|
|
|
"github.com/smallstep/cli/flags"
|
|
"github.com/smallstep/cli/internal/cryptoutil"
|
|
"github.com/smallstep/cli/utils"
|
|
)
|
|
|
|
const (
|
|
// Supported profiles
|
|
profileLeaf = "leaf"
|
|
profileSelfSigned = "self-signed"
|
|
profileIntermediateCA = "intermediate-ca"
|
|
profileRootCA = "root-ca"
|
|
profileCSR = "csr" // Used only on sign
|
|
|
|
// Default durations
|
|
defaultLeafValidity = 24 * time.Hour
|
|
defaultSelfSignedValidity = 24 * time.Hour
|
|
defaultIntermediateValidity = time.Hour * 24 * 365 * 10
|
|
defaultRootValidity = time.Hour * 24 * 365 * 10
|
|
defaultTemplatevalidity = 24 * time.Hour
|
|
)
|
|
|
|
func createCommand() cli.Command {
|
|
return cli.Command{
|
|
Name: "create",
|
|
Action: command.ActionFunc(createAction),
|
|
Usage: "create a certificate or certificate signing request",
|
|
UsageText: `**step certificate create** <subject> <crt-file> <key-file>
|
|
[**--kty**=<type>] [**--curve**=<curve>] [**--size**=<size>]
|
|
[**--csr**] [**--profile**=<profile>] [**--template**=<file>]
|
|
[**--set**=<key=value>] [**--set-file**=<file>]
|
|
[**--not-before**=<duration>] [**--not-after**=<duration>] [**--san**=<SAN>]
|
|
[**--ca**=<issuer-cert>] [**--ca-kms**=<uri>]
|
|
[**--ca-key**=<issuer-key>] [**--ca-password-file**=<file>]
|
|
[**--kms**=<uri>] [**--key**=<file>] [**--password-file**=<file>]
|
|
[**--bundle**] [**--skip-csr-signature**]
|
|
[**--no-password**] [**--subtle**] [**--insecure**]`,
|
|
Description: `**step certificate create** generates a certificate or a
|
|
certificate signing request (CSR) that can be signed later using 'step
|
|
certificate sign' (or some other tool) to produce a certificate.
|
|
|
|
By default this command creates x.509 certificates or CSRs for use with TLS. If
|
|
you need something else, you can customize the output using templates. See **TEMPLATES** below.
|
|
|
|
## POSITIONAL ARGUMENTS
|
|
|
|
<subject>
|
|
: The subject of the certificate. Typically this is a hostname for services or an email address for people.
|
|
|
|
<crt-file>
|
|
: File to write CRT or CSR to (PEM format)
|
|
|
|
<key-file>
|
|
: File to write private key to (PEM format). This argument is optional if **--key** is passed.
|
|
|
|
## EXIT CODES
|
|
|
|
This command returns 0 on success and \>0 if any error occurs.
|
|
|
|
## TEMPLATES
|
|
|
|
With templates, you can customize the generated certificate or CSR.
|
|
Templates are JSON files representing a [certificate](https://pkg.go.dev/go.step.sm/crypto/x509util?tab=doc#Certificate) [1]
|
|
or a [certificate request](https://pkg.go.dev/go.step.sm/crypto/x509util?tab=doc#CertificateRequest) [2].
|
|
They use Golang's [<text/template>](https://golang.org/pkg/text/template/) package [3] and
|
|
[<Sprig>](https://masterminds.github.io/sprig/) functions [4].
|
|
|
|
Here's the default template used for generating a leaf certificate:
|
|
'''
|
|
{
|
|
"subject": {{ toJson .Subject }},
|
|
"sans": {{ toJson .SANs }},
|
|
{{- if typeIs "*_rsa.PublicKey" .Insecure.CR.PublicKey }}
|
|
"keyUsage": ["keyEncipherment", "digitalSignature"],
|
|
{{- else }}
|
|
"keyUsage": ["digitalSignature"],
|
|
{{- end }}
|
|
"extKeyUsage": ["serverAuth", "clientAuth"]
|
|
}
|
|
'''
|
|
|
|
And this is the default template for a CSR:
|
|
'''
|
|
{
|
|
"subject": {{ toJson .Subject }},
|
|
"sans": {{ toJson .SANs }}
|
|
}
|
|
'''
|
|
|
|
In a custom template, you can change the **subject**, **dnsNames**,
|
|
**emailAddresses**, **ipAddresses**, and **uris**, and you can add custom
|
|
x.509 **extensions** or set the **signatureAlgorithm**.
|
|
|
|
For certificate templates, the common extensions **keyUsage**, **extKeyUsage**, and
|
|
**basicConstraints** are also represented as JSON fields.
|
|
|
|
Two variables are available in templates: **.Subject** contains the <subject> argument,
|
|
and **.SANs** contains the SANs provided with the **--san** flag.
|
|
|
|
Both .Subject and .SANs are objects, and they must be converted to JSON to be used in
|
|
the template, you can do this using Sprig's **toJson** function. On the .Subject
|
|
object you can access the common name string using the template variable
|
|
**.Subject.CommonName**. In **EXAMPLES** below, you can see how these
|
|
variables are used in a certificate request.
|
|
|
|
For more information on the template properties and functions see:
|
|
'''raw
|
|
[1] https://pkg.go.dev/go.step.sm/crypto/x509util?tab=doc#Certificate
|
|
[2] https://pkg.go.dev/go.step.sm/crypto/x509util?tab=doc#CertificateRequest
|
|
[3] https://golang.org/pkg/text/template/
|
|
[4] https://masterminds.github.io/sprig/
|
|
'''
|
|
|
|
## EXAMPLES
|
|
|
|
Create a CSR and key:
|
|
|
|
'''
|
|
$ step certificate create foo foo.csr foo.key --csr
|
|
'''
|
|
|
|
Create a CSR using an existing private key:
|
|
|
|
'''
|
|
$ step certificate create --csr --key key.priv foo foo.csr
|
|
'''
|
|
|
|
Create a CSR using an existing encrypted private key:
|
|
|
|
'''
|
|
$ step certificate create --csr --key key.priv --password-file key.pass foo foo.csr
|
|
'''
|
|
|
|
Create a CSR and key with custom Subject Alternative Names:
|
|
|
|
'''
|
|
$ step certificate create foo foo.csr foo.key --csr \
|
|
--san inter.smallstep.com --san 1.1.1.1 --san ca.smallstep.com
|
|
'''
|
|
|
|
Create a CSR and key - do not encrypt the key when writing to disk:
|
|
|
|
'''
|
|
$ step certificate create foo foo.csr foo.key --csr --no-password --insecure
|
|
'''
|
|
|
|
Create a root certificate and key:
|
|
|
|
'''
|
|
$ step certificate create root-ca root-ca.crt root-ca.key --profile root-ca
|
|
'''
|
|
|
|
Create an intermediate certificate and key:
|
|
|
|
'''
|
|
$ step certificate create intermediate-ca intermediate-ca.crt intermediate-ca.key \
|
|
--profile intermediate-ca --ca ./root-ca.crt --ca-key ./root-ca.key
|
|
'''
|
|
|
|
Create a leaf certificate and key:
|
|
|
|
'''
|
|
$ step certificate create foo foo.crt foo.key --profile leaf \
|
|
--ca ./intermediate-ca.crt --ca-key ./intermediate-ca.key
|
|
'''
|
|
|
|
Create a leaf certificate and encrypt the private key:
|
|
|
|
'''
|
|
$ step certificate create foo foo.crt foo.key --profile leaf \
|
|
--password-file ./leaf.pass \
|
|
--ca ./intermediate-ca.crt --ca-key ./intermediate-ca.key
|
|
'''
|
|
|
|
Create a leaf certificate and decrypt the CA private key:
|
|
|
|
'''
|
|
$ step certificate create foo foo.crt foo.key --profile leaf \
|
|
--ca ./intermediate-ca.crt --ca-key ./intermediate-ca.key --ca-password-file ./intermediate.pass
|
|
'''
|
|
|
|
Create a leaf certificate and key with custom Subject Alternative Names:
|
|
|
|
'''
|
|
$ step certificate create foo foo.crt foo.key --profile leaf \
|
|
--ca ./intermediate-ca.crt --ca-key ./intermediate-ca.key \
|
|
--san inter.smallstep.com --san 1.1.1.1 --san ca.smallstep.com
|
|
'''
|
|
|
|
Create a leaf certificate and key with custom validity:
|
|
|
|
'''
|
|
$ step certificate create foo foo.crt foo.key --profile leaf \
|
|
--ca ./intermediate-ca.crt --ca-key ./intermediate-ca.key \
|
|
--not-before 24h --not-after 2160h
|
|
'''
|
|
|
|
Create a self-signed leaf certificate and key:
|
|
|
|
'''
|
|
$ step certificate create self-signed-leaf.local leaf.crt leaf.key --profile self-signed --subtle
|
|
'''
|
|
|
|
Create a root certificate and key with underlying OKP Ed25519:
|
|
|
|
'''
|
|
$ step certificate create root-ca root-ca.crt root-ca.key --profile root-ca \
|
|
--kty OKP --curve Ed25519
|
|
'''
|
|
|
|
Create an intermediate certificate and key with underlying EC P-256 key pair:
|
|
|
|
'''
|
|
$ step certificate create intermediate-ca intermediate-ca.crt intermediate-ca.key \
|
|
--profile intermediate-ca --ca ./root-ca.crt --ca-key ./root-ca.key --kty EC --curve P-256
|
|
'''
|
|
|
|
Create a leaf certificate and key with underlying RSA 2048 key pair:
|
|
|
|
'''
|
|
$ step certificate create foo foo.crt foo.key --profile leaf \
|
|
--ca ./intermediate-ca.crt --ca-key ./intermediate-ca.key --kty RSA --size 2048
|
|
'''
|
|
|
|
Create a CSR and key with underlying OKP Ed25519:
|
|
|
|
'''
|
|
$ step certificate create foo foo.csr foo.key --csr --kty OKP --curve Ed25519
|
|
'''
|
|
|
|
Create a root certificate using a custom template. The root certificate will
|
|
have a path length constraint that allows at least 2 intermediates:
|
|
'''
|
|
$ cat root.tpl
|
|
{
|
|
"subject": {
|
|
"commonName": "Acme Corporation Root CA"
|
|
},
|
|
"issuer": {
|
|
"commonName": "Acme Corporation Root CA"
|
|
},
|
|
"keyUsage": ["certSign", "crlSign"],
|
|
"basicConstraints": {
|
|
"isCA": true,
|
|
"maxPathLen": 2
|
|
}
|
|
}
|
|
$ step certificate create --template root.tpl \
|
|
"Acme Corporation Root CA" root_ca.crt root_ca_key
|
|
'''
|
|
|
|
Create an intermediate certificate using the previous root. By extending the
|
|
maxPathLen we are enabling this intermediate sign leaf and intermediate
|
|
certificates. We will also make the subject configurable using the **--set** and
|
|
**--set-file** flags.
|
|
'''
|
|
$ cat intermediate.tpl
|
|
{
|
|
"subject": {
|
|
"country": {{ toJson .Insecure.User.country }},
|
|
"organization": {{ toJson .Insecure.User.organization }},
|
|
"organizationalUnit": {{ toJson .Insecure.User.organizationUnit }},
|
|
"commonName": {{toJson .Subject.CommonName }}
|
|
},
|
|
"keyUsage": ["certSign", "crlSign"],
|
|
"basicConstraints": {
|
|
"isCA": true,
|
|
"maxPathLen": 1
|
|
}
|
|
}
|
|
$ cat organization.json
|
|
{
|
|
"country": "US",
|
|
"organization": "Acme Corporation",
|
|
"organizationUnit": "HQ"
|
|
}
|
|
$ step certificate create --template intermediate.tpl \
|
|
--ca root_ca.crt --ca-key root_ca_key \
|
|
--set-file organization.json --set organizationUnit=Engineering \
|
|
"Acme Corporation Intermediate CA" intermediate_ca.crt intermediate_ca_key
|
|
'''
|
|
|
|
Sign a new intermediate using the previous intermediate, now with path
|
|
length 0 using the **--profile** flag:
|
|
'''
|
|
$ step certificate create --profile intermediate-ca \
|
|
--ca intermediate_ca.crt --ca-key intermediate_ca_key \
|
|
"Coyote Corporation" coyote_ca.crt coyote_ca_key
|
|
'''
|
|
|
|
Create a leaf certificate, that is the default profile and bundle it with
|
|
the two intermediate certificates and validate it:
|
|
'''
|
|
$ step certificate create --ca coyote_ca.crt --ca-key coyote_ca_key \
|
|
"coyote@acme.corp" leaf.crt coyote.key
|
|
$ cat leaf.crt coyote_ca.crt intermediate_ca.crt > coyote.crt
|
|
$ step certificate verify --roots root_ca.crt coyote.crt
|
|
'''
|
|
|
|
Create a certificate request using a template:
|
|
'''
|
|
$ cat csr.tpl
|
|
{
|
|
"subject": {
|
|
"country": "US",
|
|
"organization": "Coyote Corporation",
|
|
"commonName": "{{ .Subject.CommonName }}"
|
|
},
|
|
"sans": {{ toJson .SANs }}
|
|
}
|
|
$ step certificate create --csr --template csr.tpl --san coyote@acme.corp \
|
|
"Wile E. Coyote" coyote.csr coyote.key
|
|
'''
|
|
|
|
Create a CSR using <step-kms-plugin>:
|
|
'''
|
|
$ step certificate create --csr --key 'yubikey:slot-id=9a?pin=value=123456' coyote@acme.corp coyote.csr
|
|
'''
|
|
|
|
Create a root certificate using <step-kms-plugin>:
|
|
'''
|
|
$ step certificate create --profile root-ca --key 'yubikey:slot-id=9a?pin=value=123456' 'KMS Root' root_ca.crt
|
|
'''
|
|
|
|
Create a root certificate using <step-kms-plugin> and the <--kms> flag:
|
|
'''
|
|
$ step kms create \
|
|
--kms 'pkcs11:module-path=/usr/local/lib/softhsm/libsofthsm2.so;token=smallstep?pin-value=password' \
|
|
'pkcs11:id=4000;object=root-key'
|
|
$ step certificate create \
|
|
--profile root-ca \
|
|
--kms 'pkcs11:module-path=/usr/local/lib/softhsm/libsofthsm2.so;token=smallstep?pin-value=password' \
|
|
--key 'pkcs11:id=4000' \
|
|
'KMS Root' root_ca.crt
|
|
'''
|
|
|
|
Create an intermediate certificate using <step-kms-plugin>:
|
|
'''
|
|
$ step kms create \
|
|
--kms 'pkcs11:module-path=/usr/local/lib/softhsm/libsofthsm2.so;token=smallstep?pin-value=password' \
|
|
'pkcs11:id=4001;object=intermediate-key'
|
|
$ step certificate create \
|
|
--profile intermediate-ca \
|
|
--ca-kms 'pkcs11:module-path=/usr/local/lib/softhsm/libsofthsm2.so;token=smallstep?pin-value=password'
|
|
--ca root_ca.crt --ca-key 'pkcs11:id=4000' \
|
|
--kms 'pkcs11:module-path=/usr/local/lib/softhsm/libsofthsm2.so;token=smallstep?pin-value=password' \
|
|
--key 'pkcs11:id=4001' \
|
|
'My KMS Intermediate' intermediate_ca.crt
|
|
'''
|
|
|
|
Create an intermediate certificate for an RSA decryption key in Google Cloud KMS, signed by a root stored on disk, using <step-kms-plugin>:
|
|
'''
|
|
$ step certificate create \
|
|
--profile intermediate-ca \
|
|
--ca root_ca.crt --ca-key root_ca_key \
|
|
--kms cloudkms: \
|
|
--key 'projects/myProjectID/locations/global/keyRings/myKeyRing/cryptoKeys/myKey/cryptoKeyVersions/1' \
|
|
--skip-csr-signature \
|
|
'My RSA Intermediate' intermediate_rsa_ca.crt
|
|
'''
|
|
|
|
Create an intermediate certificate for an RSA signing key in Google Cloud KMS, signed by a root stored in an HSM, using <step-kms-plugin>:
|
|
'''
|
|
$ step certificate create \
|
|
--profile intermediate-ca \
|
|
--ca-kms 'pkcs11:module-path=/usr/local/lib/softhsm/libsofthsm2.so;token=smallstep?pin-value=password' \
|
|
--ca root_ca.crt --ca-key 'pkcs11:id=4000' \
|
|
--kms cloudkms: \
|
|
--key 'projects/myProjectID/locations/global/keyRings/myKeyRing/cryptoKeys/myKey/cryptoKeyVersions/1' \
|
|
'My RSA Intermediate' intermediate_rsa_ca.crt
|
|
'''
|
|
`,
|
|
Flags: []cli.Flag{
|
|
flags.KTY,
|
|
flags.Size,
|
|
flags.Curve,
|
|
cli.BoolFlag{
|
|
Name: "csr",
|
|
Usage: `Generate a certificate signing request (CSR) instead of a certificate.`,
|
|
},
|
|
cli.StringFlag{
|
|
Name: "profile",
|
|
Value: profileLeaf,
|
|
Usage: `The certificate profile sets various certificate details such as
|
|
certificate use and expiration. The default profile is 'leaf' which is suitable
|
|
for a client or server using TLS.
|
|
|
|
: <profile> is a case-sensitive string and must be one of:
|
|
|
|
**leaf**
|
|
: Generate a leaf x.509 certificate suitable for use with TLS.
|
|
|
|
**intermediate-ca**
|
|
: Generate a certificate that can be used to sign additional leaf certificates.
|
|
|
|
**root-ca**
|
|
: Generate a new self-signed root certificate suitable for use as a root CA.
|
|
|
|
**self-signed**
|
|
: Generate a new self-signed leaf certificate suitable for use with TLS.
|
|
This profile requires the **--subtle** flag because the use of self-signed leaf
|
|
certificates is discouraged unless absolutely necessary.`,
|
|
},
|
|
flags.Template,
|
|
flags.TemplateSet,
|
|
flags.TemplateSetFile,
|
|
cli.StringFlag{
|
|
Name: "not-before",
|
|
Usage: `The <time|duration> set in the NotBefore property of the certificate. If a
|
|
<time> is used it is expected to be in RFC 3339 format. If a <duration> is
|
|
used, it is a sequence of decimal numbers, each with optional fraction and a
|
|
unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns",
|
|
"us" (or "µs"), "ms", "s", "m", "h".`,
|
|
},
|
|
cli.StringFlag{
|
|
Name: "not-after",
|
|
Usage: `The <time|duration> set in the NotAfter property of the certificate. If a
|
|
<time> is used it is expected to be in RFC 3339 format. If a <duration> is
|
|
used, it is a sequence of decimal numbers, each with optional fraction and a
|
|
unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns",
|
|
"us" (or "µs"), "ms", "s", "m", "h".`,
|
|
},
|
|
cli.StringSliceFlag{
|
|
Name: "san",
|
|
Usage: `Add DNS or IP Address Subjective Alternative Names (SANs). Use the '--san'
|
|
flag multiple times to configure multiple SANs.`,
|
|
},
|
|
cli.StringFlag{
|
|
Name: "ca",
|
|
Usage: `The certificate authority used to issue the new certificate (PEM file).`,
|
|
},
|
|
cli.StringFlag{
|
|
Name: "ca-kms",
|
|
Usage: "The <uri> to configure the KMS used for signing the certificate",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "ca-key",
|
|
Usage: `The certificate authority private key used to sign the new certificate (PEM file).`,
|
|
},
|
|
cli.StringFlag{
|
|
Name: "ca-password-file",
|
|
Usage: `The path to the <file> containing the password to
|
|
decrypt the CA private key.`,
|
|
},
|
|
flags.KMSUri,
|
|
cli.StringFlag{
|
|
Name: "key",
|
|
Usage: "The <file> of the private key to use instead of creating a new one (PEM file).",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "password-file",
|
|
Usage: `The path to the <file> containing the password to
|
|
encrypt the new private key or decrypt the user submitted private key.`,
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "no-password",
|
|
Usage: `Do not ask for a password to encrypt the private key.
|
|
Sensitive key material will be written to disk unencrypted. This is not
|
|
recommended. Requires **--insecure** flag.`,
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "bundle",
|
|
Usage: `Bundle the new leaf certificate with the signing certificate. This flag requires
|
|
the **--ca** flag.`,
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "skip-csr-signature",
|
|
Usage: "Skip creating and signing a CSR.",
|
|
},
|
|
flags.Force,
|
|
flags.Subtle,
|
|
flags.InsecureHidden,
|
|
},
|
|
}
|
|
}
|
|
|
|
func createAction(ctx *cli.Context) error {
|
|
minArg := 2
|
|
if key := ctx.String("key"); key == "" {
|
|
minArg = 3
|
|
}
|
|
if err := errs.MinMaxNumberOfArguments(ctx, minArg, 3); err != nil {
|
|
return err
|
|
}
|
|
|
|
insecureMode := ctx.Bool("insecure")
|
|
noPass := ctx.Bool("no-password")
|
|
if noPass && !insecureMode {
|
|
return errs.RequiredWithFlag(ctx, "no-password", "insecure")
|
|
}
|
|
|
|
subject := ctx.Args().Get(0)
|
|
crtFile := ctx.Args().Get(1)
|
|
keyFile := ctx.Args().Get(2)
|
|
if crtFile == keyFile {
|
|
return errs.EqualArguments(ctx, "<crt-file>", "<key-file>")
|
|
}
|
|
|
|
notBefore, ok := flags.ParseTimeOrDuration(ctx.String("not-before"))
|
|
if !ok {
|
|
return errs.InvalidFlagValue(ctx, "not-before", ctx.String("not-before"), "")
|
|
}
|
|
notAfter, ok := flags.ParseTimeOrDuration(ctx.String("not-after"))
|
|
if !ok {
|
|
return errs.InvalidFlagValue(ctx, "not-after", ctx.String("not-after"), "")
|
|
}
|
|
if !notAfter.IsZero() && !notBefore.IsZero() && notBefore.After(notAfter) {
|
|
return errs.IncompatibleFlagValues(ctx, "not-before", ctx.String("not-before"), "not-after", ctx.String("not-after"))
|
|
}
|
|
|
|
var (
|
|
sans = ctx.StringSlice("san")
|
|
profile = ctx.String("profile")
|
|
templateFile = ctx.String("template")
|
|
bundle = ctx.Bool("bundle")
|
|
subtle = ctx.Bool("subtle")
|
|
skipCSRSignature = ctx.Bool("skip-csr-signature")
|
|
)
|
|
|
|
if ctx.IsSet("profile") && templateFile != "" {
|
|
return errs.IncompatibleFlagWithFlag(ctx, "profile", "template")
|
|
}
|
|
|
|
if ctx.Bool("csr") && skipCSRSignature {
|
|
return errs.IncompatibleFlagWithFlag(ctx, "csr", "skip-csr-signature")
|
|
}
|
|
|
|
// Read template if passed
|
|
var template string
|
|
if templateFile != "" {
|
|
b, err := utils.ReadFile(templateFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
template = string(b)
|
|
}
|
|
|
|
// Parse --set and --set-file
|
|
userData, err := flags.GetTemplateData(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Read or generate key pair.
|
|
// When the flag --key has a public key, priv will be nil.
|
|
pub, priv, err := parseOrCreateKey(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create certificate request
|
|
if ctx.Bool("csr") {
|
|
if priv == nil {
|
|
return errors.New("invalid value for flag --key: a private key is required")
|
|
}
|
|
if bundle {
|
|
return errs.IncompatibleFlagWithFlag(ctx, "bundle", "csr")
|
|
}
|
|
if profile != "" && profile != profileLeaf {
|
|
return errs.IncompatibleFlagWithFlag(ctx, "profile", "csr")
|
|
}
|
|
if ctx.IsSet("ca") {
|
|
return errs.IncompatibleFlagWithFlag(ctx, "ca", "csr")
|
|
}
|
|
if ctx.IsSet("ca-key") {
|
|
return errs.IncompatibleFlagWithFlag(ctx, "ca-key", "csr")
|
|
}
|
|
if ctx.IsSet("ca-password-file") {
|
|
return errs.IncompatibleFlagWithFlag(ctx, "ca-password-file", "csr")
|
|
}
|
|
if ctx.IsSet("not-before") {
|
|
return errs.IncompatibleFlagWithFlag(ctx, "not-before", "csr")
|
|
}
|
|
if ctx.IsSet("not-after") {
|
|
return errs.IncompatibleFlagWithFlag(ctx, "not-after", "csr")
|
|
}
|
|
|
|
// Use subject as default san
|
|
if len(sans) == 0 {
|
|
sans = append(sans, subject)
|
|
}
|
|
|
|
// Use default template if empty
|
|
if template == "" {
|
|
template = x509util.DefaultCertificateRequestTemplate
|
|
}
|
|
|
|
// Create certificate request
|
|
data := x509util.CreateTemplateData(subject, sans)
|
|
data.SetUserData(userData)
|
|
csr, err := x509util.NewCertificateRequest(priv, x509util.WithTemplate(template, data))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cr, err := csr.GetCertificateRequest()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
block, err := pemutil.Serialize(cr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Save key and certificate request
|
|
if keyFile != "" {
|
|
if err := savePrivateKey(ctx, keyFile, priv, noPass); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err = fileutil.WriteFile(crtFile, pem.EncodeToMemory(block), 0o600); err != nil {
|
|
return errs.FileError(err, crtFile)
|
|
}
|
|
|
|
ui.Printf("Your certificate signing request has been saved in %s.\n", crtFile)
|
|
if keyFile != "" {
|
|
ui.Printf("Your private key has been saved in %s.\n", keyFile)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Bundle is only valid for leaf certificates
|
|
if bundle && profile != profileLeaf {
|
|
return errs.IncompatibleFlagValue(ctx, "bundle", "profile", profile)
|
|
}
|
|
|
|
// Subtle is required on self-signed certificates
|
|
if !subtle && profile == profileSelfSigned {
|
|
return errs.RequiredWithFlagValue(ctx, "profile", "self-signed", "subtle")
|
|
}
|
|
|
|
// When a public key is given with the flag --key, priv is nil and
|
|
// --skip-csr-signature is required.
|
|
if priv == nil && !skipCSRSignature {
|
|
return errors.New("flag '--skip-csr-signature' is required with a public key")
|
|
}
|
|
|
|
// Parse --ca and --ca-key flags and check when those flags are required.
|
|
parent, signer, err := parseSigner(ctx, priv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Use subject as default SAN when using a template or for leaf and self-signed certificates.
|
|
if len(sans) == 0 && (template != "" || profile == profileLeaf || profile == profileSelfSigned) {
|
|
sans = append(sans, subject)
|
|
}
|
|
|
|
var defaultValidity time.Duration
|
|
if template == "" {
|
|
switch profile {
|
|
case profileLeaf:
|
|
template = x509util.DefaultLeafTemplate
|
|
defaultValidity = defaultLeafValidity
|
|
case profileIntermediateCA:
|
|
template = x509util.DefaultIntermediateTemplate
|
|
defaultValidity = defaultIntermediateValidity
|
|
case profileRootCA:
|
|
template = x509util.DefaultRootTemplate
|
|
defaultValidity = defaultRootValidity
|
|
case profileSelfSigned:
|
|
template = x509util.DefaultLeafTemplate
|
|
defaultValidity = defaultSelfSignedValidity
|
|
default:
|
|
return errs.InvalidFlagValue(ctx, "profile", profile, "leaf, intermediate-ca, root-ca, self-signed")
|
|
}
|
|
} else {
|
|
defaultValidity = defaultTemplatevalidity
|
|
}
|
|
|
|
// Create X.509 certificate
|
|
templateData := x509util.CreateTemplateData(subject, sans)
|
|
templateData.SetUserData(userData)
|
|
|
|
certTemplate := &x509.Certificate{}
|
|
if skipCSRSignature {
|
|
certTemplate.PublicKey = pub
|
|
certificate, err := x509util.NewCertificateFromX509(certTemplate, x509util.WithTemplate(template, templateData))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
certTemplate = certificate.GetCertificate()
|
|
} else {
|
|
// Create X.509 certificate used as base for the certificate
|
|
cr, err := x509util.CreateCertificateRequest(subject, sans, priv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
certificate, err := x509util.NewCertificate(cr, x509util.WithTemplate(template, templateData))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
certTemplate = certificate.GetCertificate()
|
|
}
|
|
|
|
if parent == nil {
|
|
parent = certTemplate
|
|
}
|
|
|
|
// Set certificate validity
|
|
certTemplate.NotBefore = notBefore
|
|
certTemplate.NotAfter = notAfter
|
|
|
|
if certTemplate.NotBefore.IsZero() {
|
|
certTemplate.NotBefore = time.Now()
|
|
}
|
|
if certTemplate.NotAfter.IsZero() {
|
|
certTemplate.NotAfter = certTemplate.NotBefore.Add(defaultValidity)
|
|
}
|
|
// Check that the certificate is not already expired
|
|
if certTemplate.NotBefore.After(certTemplate.NotAfter) {
|
|
return errors.Errorf("invalid value '%s' for flag '--not-after': certificate is already expired", ctx.String("not-after"))
|
|
}
|
|
|
|
cert, err := x509util.CreateCertificate(certTemplate, parent, pub, signer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Serialize certificate
|
|
block, err := pemutil.Serialize(cert)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pubBytes := pem.EncodeToMemory(block)
|
|
if bundle {
|
|
if block, err = pemutil.Serialize(parent); err != nil {
|
|
return err
|
|
}
|
|
pubBytes = append(pubBytes, pem.EncodeToMemory(block)...)
|
|
}
|
|
|
|
// Save key and certificate request
|
|
if keyFile != "" && priv != nil && !cryptoutil.IsKMSSigner(priv) {
|
|
if err := savePrivateKey(ctx, keyFile, priv, noPass); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err = fileutil.WriteFile(crtFile, pubBytes, 0o600); err != nil {
|
|
return errs.FileError(err, crtFile)
|
|
}
|
|
|
|
ui.Printf("Your certificate has been saved in %s.\n", crtFile)
|
|
if keyFile != "" {
|
|
ui.Printf("Your private key has been saved in %s.\n", keyFile)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseOrCreateKey(ctx *cli.Context) (crypto.PublicKey, crypto.Signer, error) {
|
|
var (
|
|
kms = ctx.String("kms")
|
|
keyFile = ctx.String("key")
|
|
)
|
|
|
|
// Validate key parameters and generate key pair
|
|
if keyFile == "" {
|
|
insecureMode := ctx.Bool("insecure")
|
|
kty, crv, size, err := utils.GetKeyDetailsFromCLI(ctx, insecureMode, "kty", "curve", "size")
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if insecureMode { // put keyutil in insecure mode, allowing RSA keys shorter than 2048 bits
|
|
undoInsecure := keyutil.Insecure()
|
|
defer undoInsecure()
|
|
}
|
|
pub, priv, err := keyutil.GenerateKeyPair(kty, crv, size)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
signer, ok := priv.(crypto.Signer)
|
|
if !ok {
|
|
return nil, nil, errors.Errorf("private key of type %T is not a crypto.Signer", priv)
|
|
}
|
|
return pub, signer, nil
|
|
}
|
|
|
|
// Validate incompatible flags and read a key file
|
|
switch {
|
|
case ctx.IsSet("kty"):
|
|
return nil, nil, errs.IncompatibleFlag(ctx, "key", "kty")
|
|
case ctx.IsSet("crv"):
|
|
return nil, nil, errs.IncompatibleFlag(ctx, "key", "crv")
|
|
case ctx.IsSet("size"):
|
|
return nil, nil, errs.IncompatibleFlag(ctx, "key", "size")
|
|
}
|
|
|
|
opts := []pemutil.Options{}
|
|
passFile := ctx.String("password-file")
|
|
if passFile != "" {
|
|
opts = append(opts, pemutil.WithPasswordFile(passFile))
|
|
}
|
|
|
|
var pub crypto.PublicKey
|
|
var signer crypto.Signer
|
|
|
|
signer, err := cryptoutil.CreateSigner(kms, keyFile, opts...)
|
|
if err != nil {
|
|
// TODO: check sentinel error; if it's not a signer, it could be a public key instead
|
|
pub, err = cryptoutil.PublicKey(kms, keyFile, opts...)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
} else {
|
|
pub = signer.Public()
|
|
// Make sure we can sign X509 certificates with it.
|
|
if !cryptoutil.IsX509Signer(signer) {
|
|
return nil, nil, errs.InvalidFlagValueMsg(ctx, "key", keyFile, "the given key cannot sign X509 certificates")
|
|
}
|
|
}
|
|
|
|
return pub, signer, nil
|
|
}
|
|
|
|
// parseSigner returns the parent certificate and key for leaf and intermediate
|
|
// certificates. When a template is used, it will return the key only if the
|
|
// flags --ca and --ca-key are passed.
|
|
func parseSigner(ctx *cli.Context, defaultSigner crypto.Signer) (*x509.Certificate, crypto.Signer, error) {
|
|
var (
|
|
caCert = ctx.String("ca")
|
|
caKey = ctx.String("ca-key")
|
|
caKMS = ctx.String("ca-kms")
|
|
profile = ctx.String("profile")
|
|
template = ctx.String("template")
|
|
)
|
|
|
|
// Check required flags when profile is used.
|
|
if template == "" {
|
|
switch profile {
|
|
case profileLeaf, profileIntermediateCA:
|
|
if caCert == "" {
|
|
return nil, nil, errs.RequiredWithFlagValue(ctx, "profile", profile, "ca")
|
|
}
|
|
if caKey == "" {
|
|
return nil, nil, errs.RequiredWithFlagValue(ctx, "profile", profile, "ca-key")
|
|
}
|
|
case profileRootCA, profileSelfSigned:
|
|
if caCert != "" {
|
|
return nil, nil, errs.IncompatibleFlagValue(ctx, "ca", "profile", profile)
|
|
}
|
|
if caKey != "" {
|
|
return nil, nil, errs.IncompatibleFlagValue(ctx, "ca-key", "profile", profile)
|
|
}
|
|
default:
|
|
return nil, nil, errs.InvalidFlagValue(ctx, "profile", profile, "leaf, intermediate-ca, root-ca, self-signed")
|
|
}
|
|
}
|
|
|
|
// Root, self-signed, or template with no parent.
|
|
if caCert == "" && caKey == "" {
|
|
return nil, defaultSigner, nil
|
|
}
|
|
|
|
// Leaf, intermediate or template with
|
|
switch {
|
|
case caCert != "" && caKey == "":
|
|
return nil, nil, errs.RequiredWithFlag(ctx, "ca", "ca-key")
|
|
case caCert == "" && caKey != "":
|
|
return nil, nil, errs.RequiredWithFlag(ctx, "ca-key", "ca")
|
|
}
|
|
|
|
// Parse --ca as a certificate.
|
|
cert, err := pemutil.ReadCertificate(caCert)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Parse --ca-key as a crypto.Signer.
|
|
passFile := ctx.String("ca-password-file")
|
|
opts := []pemutil.Options{}
|
|
if passFile != "" {
|
|
opts = append(opts, pemutil.WithPasswordFile(passFile))
|
|
}
|
|
|
|
signer, err := cryptoutil.CreateSigner(caKMS, caKey, opts...)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Make sure we can sign X509 certificates with it.
|
|
if !cryptoutil.IsX509Signer(signer) {
|
|
return nil, nil, errs.InvalidFlagValueMsg(ctx, "ca-key", caKey, "the given key cannot sign X509 certificates")
|
|
}
|
|
|
|
return cert, signer, nil
|
|
}
|
|
|
|
// savePrivateKey saves the given key, asking the password if necessary.
|
|
func savePrivateKey(ctx *cli.Context, filename string, priv interface{}, insecure bool) error {
|
|
var err error
|
|
if insecure {
|
|
_, err = pemutil.Serialize(priv, pemutil.ToFile(filename, 0o600))
|
|
return err
|
|
}
|
|
|
|
var pass []byte
|
|
if passFile := ctx.String("password-file"); passFile != "" {
|
|
pass, err = utils.ReadPasswordFromFile(passFile)
|
|
if err != nil {
|
|
return errors.Wrap(err, "error reading encrypting password from file")
|
|
}
|
|
} else {
|
|
pass, err = ui.PromptPassword("Please enter the password to encrypt the private key",
|
|
ui.WithValidateNotEmpty())
|
|
if err != nil {
|
|
return errors.Wrap(err, "error reading password")
|
|
}
|
|
}
|
|
_, err = pemutil.Serialize(priv, pemutil.WithPassword(pass), pemutil.ToFile(filename, 0o600))
|
|
return err
|
|
}
|