From d09c39d8e6c85e842c55381c21c3eb8be67d2a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francis=20Rebou=C3=A7as?= Date: Wed, 26 Aug 2026 18:58:15 -0300 Subject: [PATCH] feat: add --intermediate-file flag to step ca certificate Add a new --intermediate-file flag to 'step ca certificate' that writes the intermediate certificate chain to a separate file, while the leaf certificate is written to the main certificate file. This is useful for clients that don't accept full certificate chains and require the leaf and intermediate certificates in separate files. Closes #1360 --- command/ca/certificate.go | 12 +++++++++++- flags/flags.go | 7 +++++++ utils/cautils/certificate_flow.go | 29 +++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) 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)