Skip to content

Commit

Permalink
feat(kumactl) add ECDSA certificate generator support
Browse files Browse the repository at this point in the history
Add kumactl support for generating ECDSA TLS certificates. The default is
still RSA certificates, but adding an easy way to generate a self-signed
ECDSA certificate is useful for testing different TLS configurations.

Signed-off-by: James Peach <james.peach@konghq.com>
  • Loading branch information
jpeach committed Nov 5, 2021
1 parent 7575883 commit b430cfa
Show file tree
Hide file tree
Showing 12 changed files with 148 additions and 59 deletions.
4 changes: 4 additions & 0 deletions app/kumactl/cmd/completion/testdata/bash.golden
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,10 @@ _kumactl_generate_tls-certificate()
two_word_flags+=("--key-file")
local_nonpersistent_flags+=("--key-file")
local_nonpersistent_flags+=("--key-file=")
flags+=("--key-type=")
two_word_flags+=("--key-type")
local_nonpersistent_flags+=("--key-type")
local_nonpersistent_flags+=("--key-type=")
flags+=("--type=")
two_word_flags+=("--type")
local_nonpersistent_flags+=("--type")
Expand Down
15 changes: 14 additions & 1 deletion app/kumactl/cmd/generate/generate_certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type generateCertificateContext struct {
key string
cert string
certType string
keyType string
hostnames []string
}
}
Expand Down Expand Up @@ -53,7 +54,18 @@ func NewGenerateCertificateCmd(pctx *kumactl_cmd.RootContext) *cobra.Command {
return errors.Errorf("invalid certificate type %q", certType)
}

keyPair, err := NewSelfSignedCert(ctx.args.hostnames[0], certType, ctx.args.hostnames...)
keyType := tls.DefaultKeyType
switch ctx.args.keyType {
case "":
case "rsa":
keyType = tls.RSAKeyType
case "ecdsa":
keyType = tls.ECDSAKeyType
default:
return errors.Errorf("invalid key type %q", ctx.args.keyType)
}

keyPair, err := NewSelfSignedCert(ctx.args.hostnames[0], certType, keyType, ctx.args.hostnames...)
if err != nil {
return errors.Wrap(err, "could not generate certificate")
}
Expand Down Expand Up @@ -87,6 +99,7 @@ func NewGenerateCertificateCmd(pctx *kumactl_cmd.RootContext) *cobra.Command {
cmd.Flags().StringVar(&ctx.args.key, "key-file", "key.pem", "path to a file with a generated private key ('-' for stdout)")
cmd.Flags().StringVar(&ctx.args.cert, "cert-file", "cert.pem", "path to a file with a generated TLS certificate ('-' for stdout)")
cmd.Flags().StringVar(&ctx.args.certType, "type", "", kuma_cmd.UsageOptions("type of the certificate", "client", "server"))
cmd.Flags().StringVar(&ctx.args.keyType, "key-type", "", kuma_cmd.UsageOptions("type of the private key", "rsa", "ecdsa"))
cmd.Flags().StringSliceVar(&ctx.args.hostnames, "hostname", []string{}, "DNS hostname(s) to issue the certificate for")
_ = cmd.MarkFlagRequired("type")
_ = cmd.MarkFlagRequired("hostname")
Expand Down
147 changes: 107 additions & 40 deletions app/kumactl/cmd/generate/generate_certificate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import (
"fmt"
"io/ioutil"
"os"
"reflect"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/spf13/cobra"

"github.com/kumahq/kuma/app/kumactl/cmd/generate"
"github.com/kumahq/kuma/pkg/tls"
Expand All @@ -16,7 +18,7 @@ import (

var _ = Describe("kumactl generate tls-certificate", func() {

var backupNewSelfSignedCert func(string, tls.CertType, ...string) (tls.KeyPair, error)
var backupNewSelfSignedCert func(string, tls.CertType, tls.KeyType, ...string) (tls.KeyPair, error)
BeforeEach(func() {
backupNewSelfSignedCert = generate.NewSelfSignedCert
})
Expand All @@ -30,7 +32,27 @@ var _ = Describe("kumactl generate tls-certificate", func() {
var stdout *bytes.Buffer
var stderr *bytes.Buffer

AssertOutput := func(keyPath string, certPath string) {
Do := func(cmd *cobra.Command) {
cmd.SetOut(stdout)
cmd.SetErr(stderr)

// when
err := cmd.Execute()

// then
Expect(err).ToNot(HaveOccurred())

// and
keyBytes, err := ioutil.ReadAll(keyFile)
Expect(err).ToNot(HaveOccurred())
Expect(string(keyBytes)).To(Equal("KEY"))

// and
certBytes, err := ioutil.ReadAll(certFile)
Expect(err).ToNot(HaveOccurred())
Expect(string(certBytes)).To(Equal("CERT"))

// and
Expect(stdout.String()).To(ContainSubstring(
fmt.Sprintf("Private key saved in %s", keyFile.Name())),
)
Expand All @@ -52,9 +74,55 @@ var _ = Describe("kumactl generate tls-certificate", func() {
stderr = &bytes.Buffer{}
})

Context("ECDSA certificates", func() {
BeforeEach(func() {
generate.NewSelfSignedCert = func(commonName string, certType tls.CertType, keyType tls.KeyType, hosts ...string) (tls.KeyPair, error) {
Expect(reflect.ValueOf(keyType)).To(Equal(reflect.ValueOf(tls.ECDSAKeyType)))
Expect(commonName).To(Equal("hostname"))
Expect(hosts).To(ConsistOf("hostname"))
return tls.KeyPair{
CertPEM: []byte("CERT"),
KeyPEM: []byte("KEY"),
}, nil
}
})

It("should generate client certificate", func() {
// given
rootCmd := test.DefaultTestingRootCmd()
rootCmd.SetArgs([]string{"generate", "tls-certificate",
"--key-file", keyFile.Name(),
"--cert-file", certFile.Name(),
"--type", "client",
"--key-type", "ecdsa",
"--hostname", "hostname",
})

// then
Do(rootCmd)
})

It("should generate server certificate", func() {
// given
rootCmd := test.DefaultTestingRootCmd()
rootCmd.SetArgs([]string{"generate", "tls-certificate",
"--key-file", keyFile.Name(),
"--cert-file", certFile.Name(),
"--type", "server",
"--key-type", "ecdsa",
"--hostname", "hostname",
})

// then
Do(rootCmd)
})

})

Context("client certificate", func() {
BeforeEach(func() {
generate.NewSelfSignedCert = func(commonName string, certType tls.CertType, hosts ...string) (tls.KeyPair, error) {
generate.NewSelfSignedCert = func(commonName string, certType tls.CertType, keyType tls.KeyType, hosts ...string) (tls.KeyPair, error) {
Expect(reflect.ValueOf(keyType)).To(Equal(reflect.ValueOf(tls.DefaultKeyType)))
Expect(commonName).To(Equal("client-name"))
Expect(certType).To(Equal(tls.ClientCertType))
Expect(hosts).To(ConsistOf("client-name"))
Expand All @@ -74,29 +142,10 @@ var _ = Describe("kumactl generate tls-certificate", func() {
"--type", "client",
"--hostname", "client-name",
})
rootCmd.SetOut(stdout)
rootCmd.SetErr(stderr)

// when
err := rootCmd.Execute()

// then
Expect(err).ToNot(HaveOccurred())

// and
keyBytes, err := ioutil.ReadAll(keyFile)
Expect(err).ToNot(HaveOccurred())
Expect(string(keyBytes)).To(Equal("KEY"))

// and
certBytes, err := ioutil.ReadAll(certFile)
Expect(err).ToNot(HaveOccurred())
Expect(string(certBytes)).To(Equal("CERT"))

// and
AssertOutput(keyFile.Name(), certFile.Name())
Do(rootCmd)
})

It("should validate that --hostname is present", func() {
// given
rootCmd := test.DefaultTestingRootCmd()
Expand All @@ -119,7 +168,7 @@ var _ = Describe("kumactl generate tls-certificate", func() {

Context("server certificate", func() {
BeforeEach(func() {
generate.NewSelfSignedCert = func(commonName string, certType tls.CertType, hosts ...string) (tls.KeyPair, error) {
generate.NewSelfSignedCert = func(commonName string, certType tls.CertType, keyType tls.KeyType, hosts ...string) (tls.KeyPair, error) {
Expect(commonName).To(Equal("kuma1.internal"))
Expect(certType).To(Equal(tls.ServerCertType))
Expect(hosts).To(ConsistOf("kuma1.internal", "kuma2.internal"))
Expand All @@ -140,36 +189,54 @@ var _ = Describe("kumactl generate tls-certificate", func() {
"--hostname", "kuma1.internal",
"--hostname", "kuma2.internal",
})

// then
Do(rootCmd)
})

It("should generate an ECDSA server certificate", func() {
// given
rootCmd := test.DefaultTestingRootCmd()
rootCmd.SetArgs([]string{"generate", "tls-certificate",
"--key-file", keyFile.Name(),
"--cert-file", certFile.Name(),
"--type", "server",
"--key-type", "ecdsa",
"--hostname", "kuma1.internal",
"--hostname", "kuma2.internal",
})

// then
Do(rootCmd)
})

It("should validate that --hostname is present", func() {
// given
rootCmd := test.DefaultTestingRootCmd()
rootCmd.SetArgs([]string{"generate", "tls-certificate",
"--key-file", keyFile.Name(),
"--cert-file", certFile.Name(),
"--type", "server",
})
rootCmd.SetOut(stdout)
rootCmd.SetErr(stderr)

// when
err := rootCmd.Execute()

// then
Expect(err).ToNot(HaveOccurred())

// and
keyBytes, err := ioutil.ReadAll(keyFile)
Expect(err).ToNot(HaveOccurred())
Expect(string(keyBytes)).To(Equal("KEY"))

// and
certBytes, err := ioutil.ReadAll(certFile)
Expect(err).ToNot(HaveOccurred())
Expect(string(certBytes)).To(Equal("CERT"))

// and
AssertOutput(keyFile.Name(), certFile.Name())
Expect(err).To(MatchError("required flag(s) \"hostname\" not set"))
})

It("should validate that --hostname is present", func() {
It("should validate that --key-type is checked", func() {
// given
rootCmd := test.DefaultTestingRootCmd()
rootCmd.SetArgs([]string{"generate", "tls-certificate",
"--key-file", keyFile.Name(),
"--cert-file", certFile.Name(),
"--type", "server",
"--hostname", "foo",
"--key-type", "phoney",
})
rootCmd.SetOut(stdout)
rootCmd.SetErr(stderr)
Expand All @@ -178,7 +245,7 @@ var _ = Describe("kumactl generate tls-certificate", func() {
err := rootCmd.Execute()

// then
Expect(err).To(MatchError("required flag(s) \"hostname\" not set"))
Expect(err).To(MatchError(`invalid key type "phoney"`))
})

})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"github.com/kumahq/kuma/app/kumactl/pkg/install/data"
"github.com/kumahq/kuma/deployments"
"github.com/kumahq/kuma/pkg/config/core"
"github.com/kumahq/kuma/pkg/tls"
kuma_version "github.com/kumahq/kuma/pkg/version"
)

Expand Down Expand Up @@ -58,7 +57,6 @@ type ImageEnvSecret struct {
type InstallCpContext struct {
Args InstallControlPlaneArgs
InstallCpTemplateFiles func(*InstallControlPlaneArgs) (data.FileList, error)
NewSelfSignedCert func(commonName string, certType tls.CertType, hosts ...string) (tls.KeyPair, error)
// When Kuma chart is embedded into other chart all the values need to have a prefix. You can set this prefix with this var.
HELMValuesPrefix string
}
Expand Down Expand Up @@ -96,7 +94,6 @@ func DefaultInstallCpContext() InstallCpContext {
Ingress_drainTime: "30s",
Ingress_service_type: "LoadBalancer",
},
NewSelfSignedCert: tls.NewSelfSignedCert,
InstallCpTemplateFiles: func(args *InstallControlPlaneArgs) (data.FileList, error) {
return data.ReadFiles(deployments.KumaChartFS())
},
Expand Down
7 changes: 0 additions & 7 deletions app/kumactl/cmd/install/install_control_plane_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (

"github.com/kumahq/kuma/app/kumactl/cmd"
kumactl_cmd "github.com/kumahq/kuma/app/kumactl/pkg/cmd"
"github.com/kumahq/kuma/pkg/tls"
"github.com/kumahq/kuma/pkg/util/test"
kuma_version "github.com/kumahq/kuma/pkg/version"
)
Expand Down Expand Up @@ -55,12 +54,6 @@ var _ = Describe("kumactl install control-plane", func() {
rootCtx.InstallCpContext.Args.ControlPlane_image_tag = "0.0.1"
rootCtx.InstallCpContext.Args.DataPlane_image_tag = "0.0.1"
rootCtx.InstallCpContext.Args.DataPlane_initImage_tag = "0.0.1"
rootCtx.InstallCpContext.NewSelfSignedCert = func(string, tls.CertType, ...string) (tls.KeyPair, error) {
return tls.KeyPair{
CertPEM: []byte("CERT"),
KeyPEM: []byte("KEY"),
}, nil
}

// given
rootCmd := cmd.NewRootCmd(rootCtx)
Expand Down
1 change: 1 addition & 0 deletions docs/cmd/kumactl/kumactl_generate_tls-certificate.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ kumactl generate tls-certificate --type=server|client --hostname=HOST1[,HOST2...
-h, --help help for tls-certificate
--hostname strings DNS hostname(s) to issue the certificate for
--key-file string path to a file with a generated private key ('-' for stdout) (default "key.pem")
--key-type string type of the private key: one of rsa|ecdsa
--type string type of the certificate: one of client|server
```

Expand Down
2 changes: 1 addition & 1 deletion pkg/api-server/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ var _ = Describe("Auth test", func() {

// we need to autogenerate cert dynamically for the external IP so the HTTPS client can validate san
func createCertsForIP(ip string) (certPath string, keyPath string) {
keyPair, err := tls.NewSelfSignedCert("kuma", tls.ServerCertType, "localhost", ip)
keyPair, err := tls.NewSelfSignedCert("kuma", tls.ServerCertType, tls.DefaultKeyType, "localhost", ip)
Expect(err).ToNot(HaveOccurred())
dir, err := ioutil.TempDir("", "temp-certs")
Expect(err).ToNot(HaveOccurred())
Expand Down
2 changes: 1 addition & 1 deletion pkg/core/bootstrap/autoconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func autoconfigureTLS(cfg *kuma_cp.Config) error {
return errors.Wrap(err, "could not get a hostname of the machine")
}
hosts := append([]string{hostname, "localhost"}, ips...)
cert, err := tls.NewSelfSignedCert("kuma-control-plane", tls.ServerCertType, hosts...)
cert, err := tls.NewSelfSignedCert("kuma-control-plane", tls.ServerCertType, tls.DefaultKeyType, hosts...)
if err != nil {
return errors.Wrap(err, "failed to auto-generate TLS certificate")
}
Expand Down
18 changes: 16 additions & 2 deletions pkg/tls/cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package tls

import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
Expand All @@ -25,8 +27,20 @@ const (
ClientCertType CertType = "client"
)

func NewSelfSignedCert(commonName string, certType CertType, hosts ...string) (KeyPair, error) {
key, err := util_rsa.GenerateKey(util_rsa.DefaultKeySize)
type KeyType func() (crypto.Signer, error)

var ECDSAKeyType KeyType = func() (crypto.Signer, error) {
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
}

var RSAKeyType KeyType = func() (crypto.Signer, error) {
return util_rsa.GenerateKey(util_rsa.DefaultKeySize)
}

var DefaultKeyType = RSAKeyType

func NewSelfSignedCert(commonName string, certType CertType, keyType KeyType, hosts ...string) (KeyPair, error) {
key, err := keyType()
if err != nil {
return KeyPair{}, errors.Wrap(err, "failed to generate TLS key")
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/xds/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func BuildControlPlaneContext(
claCache xds.CLACache,
secrets secrets.Secrets,
) (*ControlPlaneContext, error) {
adminKeyPair, err := tls.NewSelfSignedCert("admin", tls.ServerCertType, "localhost")
adminKeyPair, err := tls.NewSelfSignedCert("admin", tls.ServerCertType, tls.DefaultKeyType, "localhost")
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion test/framework/deployments/externalservice/universal.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func (u *universalDeployment) Deploy(cluster framework.Cluster) error {
env := []string{}

// ceritficates
cert, key, err := framework.CreateCertsFor([]string{"localhost", ip, name})
cert, key, err := framework.CreateCertsFor("localhost", ip, name)
if err != nil {
return err
}
Expand Down
Loading

0 comments on commit b430cfa

Please sign in to comment.