diff --git a/command/ca/certificate.go b/command/ca/certificate.go index 74ea4db9..1a2d1dbf 100644 --- a/command/ca/certificate.go +++ b/command/ca/certificate.go @@ -31,6 +31,7 @@ func certificateCommand() cli.Command { [**--contact**=] [**--http-listen**=
] [**--kty**=] [**--curve**=] [**--size**=] [**--console**] [**--x5c-cert**=] [**--x5c-key**=] [**--k8ssa-token-path**=] +[**--intermediate-file**=] [**--offline**] [**--password-file**] [**--ca-url**=] [**--root**=] [**--context**=]`, Description: `**step ca certificate** command generates a new certificate pair @@ -156,6 +157,11 @@ 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 +''' + +Request a new certificate and write the intermediate chain to a separate file: +''' +$ step ca certificate foo.internal foo.crt foo.key --intermediate-file intermediate.crt '''`, Flags: []cli.Flag{ cli.StringSliceFlag{ @@ -164,6 +170,7 @@ $ step ca certificate foo.internal foo.crt foo.key \ that should be authorized. Use the '--san' flag multiple times to configure multiple SANs. The '--san' flag and the '--token' flag are mutually exclusive.`, }, + flags.IntermediateFile, cli.StringFlag{ Name: "attestation-ca-url", Usage: "The base url of the Attestation CA to use", @@ -292,7 +299,7 @@ func certificateAction(ctx *cli.Context) error { return errors.New("token is not supported") } - if err := flow.Sign(ctx, tok, req.CsrPEM, crtFile); err != nil { + if err := flow.SignWithIntermediate(ctx, tok, req.CsrPEM, crtFile, ctx.String("intermediate-file")); err != nil { return err } @@ -303,5 +310,8 @@ func certificateAction(ctx *cli.Context) error { ui.PrintSelected("Certificate", crtFile) ui.PrintSelected("Private Key", keyFile) + if intermediateFile := ctx.String("intermediate-file"); intermediateFile != "" { + ui.PrintSelected("Intermediate Certificate", intermediateFile) + } return nil } diff --git a/flags/flags.go b/flags/flags.go index d185e624..ed2aa6a7 100644 --- a/flags/flags.go +++ b/flags/flags.go @@ -119,6 +119,13 @@ unset, default is P-256 for EC keys and Ed25519 for OKP keys. Usage: `The path to the containing the password to encrypt or decrypt the private key.`, } + // IntermediateFile is a cli.Flag used to write the intermediate certificate + // chain to a separate file. + IntermediateFile = cli.StringFlag{ + Name: "intermediate-file", + Usage: `The path to the where the intermediate certificate chain will be written.`, + } + // NoPassword is a cli.Flag used to avoid using a password to encrypt private // keys. NoPassword = cli.BoolFlag{ diff --git a/utils/cautils/certificate_flow.go b/utils/cautils/certificate_flow.go index 77609eee..aad61521 100644 --- a/utils/cautils/certificate_flow.go +++ b/utils/cautils/certificate_flow.go @@ -248,6 +248,12 @@ func (f *CertificateFlow) GenerateIdentityToken(ctx *cli.Context) (string, error // Sign signs the CSR using the online or the offline certificate authority. func (f *CertificateFlow) Sign(ctx *cli.Context, tok string, csr api.CertificateRequest, crtFile string) error { + return f.SignWithIntermediate(ctx, tok, csr, crtFile, "") +} + +// SignWithIntermediate signs the CSR and optionally writes the intermediate +// certificate chain to a separate file. +func (f *CertificateFlow) SignWithIntermediate(ctx *cli.Context, tok string, csr api.CertificateRequest, crtFile, intermediateFile string) error { client, err := f.GetClient(ctx, tok) if err != nil { return err @@ -281,6 +287,29 @@ func (f *CertificateFlow) Sign(ctx *cli.Context, tok string, csr api.Certificate if len(resp.CertChainPEM) == 0 { resp.CertChainPEM = []api.Certificate{resp.ServerPEM, resp.CaPEM} } + + if intermediateFile != "" && len(resp.CertChainPEM) > 1 { + // Write leaf certificate to crtFile + leafPEM, err := pemutil.Serialize(resp.CertChainPEM[0].Certificate) + if err != nil { + return errors.Wrap(err, "error serializing leaf certificate from step-ca API response") + } + if err := fileutil.WriteFile(crtFile, pem.EncodeToMemory(leafPEM), 0o600); err != nil { + return err + } + + // Write intermediate chain to intermediateFile + var chainData []byte + for _, certPEM := range resp.CertChainPEM[1:] { + pemblk, err := pemutil.Serialize(certPEM.Certificate) + if err != nil { + return errors.Wrap(err, "error serializing intermediate certificate from step-ca API response") + } + chainData = append(chainData, pem.EncodeToMemory(pemblk)...) + } + return fileutil.WriteFile(intermediateFile, chainData, 0o600) + } + var data []byte for _, certPEM := range resp.CertChainPEM { pemblk, err := pemutil.Serialize(certPEM.Certificate)