Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

kuma-cp: validate certificates that users want to employ as a "provided" CA #565

Merged
merged 2 commits into from
Feb 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

Changes:

* feature: validate certificates that users want to use as a `provided` CA
[#565](https://github.com/Kong/kuma/pull/565)
* fix: add MADS port to K8S install script
[#564](https://github.com/Kong/kuma/pull/564)
* feature: sanitize metrics for StatsD and Prometheus
Expand Down
78 changes: 76 additions & 2 deletions app/kumactl/cmd/manage/ca/provided_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,26 @@ package ca_test

import (
"bytes"
"github.com/Kong/kuma/app/kumactl/pkg/ca"
"io/ioutil"
"path/filepath"

"github.com/Kong/kuma/app/kumactl/pkg/ca"

"github.com/Kong/kuma/app/kumactl/cmd"
kumactl_cmd "github.com/Kong/kuma/app/kumactl/pkg/cmd"
"github.com/Kong/kuma/pkg/catalog"
catalog_client "github.com/Kong/kuma/pkg/catalog/client"
kumactl_config "github.com/Kong/kuma/pkg/config/app/kumactl/v1alpha1"
"github.com/Kong/kuma/pkg/core/ca/provided/rest/types"
error_types "github.com/Kong/kuma/pkg/core/rest/errors/types"
test_catalog "github.com/Kong/kuma/pkg/test/catalog"
"github.com/Kong/kuma/pkg/tls"
"github.com/spf13/cobra"

"github.com/ghodss/yaml"

. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
. "github.com/onsi/gomega"
)

Expand All @@ -25,6 +30,7 @@ var _ ca.ProvidedCaClient = &staticProvidedCaClient{}
type staticProvidedCaClient struct {
addMesh string
addPair tls.KeyPair
addErr error

deleteCertMesh string
deleteCertId string
Expand All @@ -38,6 +44,9 @@ type staticProvidedCaClient struct {
func (s *staticProvidedCaClient) AddSigningCertificate(mesh string, pair tls.KeyPair) (types.SigningCert, error) {
s.addMesh = mesh
s.addPair = pair
if s.addErr != nil {
return types.SigningCert{}, s.addErr
}
return types.SigningCert{
Id: "id-13456",
}, nil
Expand Down Expand Up @@ -92,7 +101,7 @@ var _ = Describe("kumactl manage provided ca", func() {
rootCmd.SetOut(buf)
})

It("should add certificate", func() {
It("should add proper CA certificate", func() {
// setup
certBytes, err := ioutil.ReadFile(filepath.Join("testdata", "cert.pem"))
Expect(err).ToNot(HaveOccurred())
Expand Down Expand Up @@ -122,6 +131,71 @@ var _ = Describe("kumactl manage provided ca", func() {
Expect(buf.String()).To(Equal(`added certificate "id-13456"`))
})

Describe("should not add improper CA certificate", func() {

type testCase struct {
addErr string
expectedOut string
}

DescribeTable("should reject invalid cert",
func(given testCase) {
// setup

addErr := error_types.Error{}
// when
err := yaml.Unmarshal([]byte(given.addErr), &addErr)
// then
Expect(err).ToNot(HaveOccurred())
// and
client.addErr = &addErr

// given
rootCmd.SetArgs([]string{
"manage", "ca", "provided", "certificates", "add",
"--mesh", "demo",
"--key-file", filepath.Join("testdata", "cert.key"),
"--cert-file", filepath.Join("testdata", "cert.pem"),
})

// when
err = rootCmd.Execute()
// then
Expect(err).To(HaveOccurred())

// and
Expect(buf.String()).To(Equal(given.expectedOut))
},
Entry("1 violation", testCase{
addErr: `
title: 'Could not add signing cert'
details: 'Resource is not valid'
causes:
- field: cert
message: "key usage extension 'keyCertSign' must be set (see X509-SVID: 4.3. Key Usage)"
`,
expectedOut: `Error: Could not add signing cert (Resource is not valid)
* cert: key usage extension 'keyCertSign' must be set (see X509-SVID: 4.3. Key Usage)
`,
}),
Entry("N violations", testCase{
addErr: `
title: 'Could not add signing cert'
details: 'Resource is not valid'
causes:
- field: cert
message: "basic constraint 'CA' must be set to 'true' (see X509-SVID: 4.1. Basic Constraints)"
- field: cert
message: "key usage extension 'keyCertSign' must be set (see X509-SVID: 4.3. Key Usage)"
`,
expectedOut: `Error: Could not add signing cert (Resource is not valid)
* cert: basic constraint 'CA' must be set to 'true' (see X509-SVID: 4.1. Basic Constraints)
* cert: key usage extension 'keyCertSign' must be set (see X509-SVID: 4.3. Key Usage)
`,
}),
)
})

It("should delete certificate", func() {
// given
rootCmd.SetArgs([]string{
Expand Down
44 changes: 2 additions & 42 deletions pkg/core/ca/builtin/issuer/issuer.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package issuer

import (
"bytes"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net/url"
"time"
Expand All @@ -35,7 +33,7 @@ func NewRootCA(mesh string) (*util_tls.KeyPair, error) {
if err != nil {
return nil, errors.Wrap(err, "failed to generate X509 certificate")
}
return keyPair(key, cert)
return util_tls.ToKeyPair(key, cert)
}

func newCACert(signer crypto.Signer, trustDomain string) ([]byte, error) {
Expand Down Expand Up @@ -73,7 +71,7 @@ func NewWorkloadCert(ca util_tls.KeyPair, mesh string, workload string) (*util_t
if err != nil {
return nil, errors.Wrap(err, "failed to generate X509 certificate")
}
return keyPair(workloadKey, workloadCert)
return util_tls.ToKeyPair(workloadKey, workloadCert)
}

func newWorkloadCert(signer crypto.PrivateKey, parent *x509.Certificate, trustDomain string, workload string, publicKey crypto.PublicKey) ([]byte, error) {
Expand Down Expand Up @@ -111,41 +109,3 @@ func loadKeyPair(pair util_tls.KeyPair) (crypto.PrivateKey, *x509.Certificate, e
}
return root.PrivateKey, rootCert, nil
}

func keyPair(key interface{}, cert []byte) (*util_tls.KeyPair, error) {
keyPem, err := pemEncodeKey(key)
if err != nil {
return nil, errors.Wrap(err, "failed to PEM encode a private key")
}
certPem, err := pemEncodeCert(cert)
if err != nil {
return nil, errors.Wrap(err, "failed to PEM encode a certificate")
}
return &util_tls.KeyPair{
CertPEM: certPem,
KeyPEM: keyPem,
}, nil
}

func pemEncodeKey(priv interface{}) ([]byte, error) {
var block *pem.Block
switch k := priv.(type) {
case *rsa.PrivateKey:
block = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}
default:
return nil, errors.Errorf("unsupported private key type %T", priv)
}
var keyBuf bytes.Buffer
if err := pem.Encode(&keyBuf, block); err != nil {
return nil, err
}
return keyBuf.Bytes(), nil
}

func pemEncodeCert(derBytes []byte) ([]byte, error) {
var certBuf bytes.Buffer
if err := pem.Encode(&certBuf, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
return nil, err
}
return certBuf.Bytes(), nil
}
53 changes: 53 additions & 0 deletions pkg/core/ca/provided/ca_cert_validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package provided

import (
"crypto/tls"
"crypto/x509"
"fmt"

"github.com/Kong/kuma/pkg/core/validators"

util_tls "github.com/Kong/kuma/pkg/tls"
)

func ValidateCaCert(signingPair util_tls.KeyPair) error {
err := validateCaCert(signingPair)
return err.OrNil()
}

func validateCaCert(signingPair util_tls.KeyPair) (verr validators.ValidationError) {
tlsKeyPair, err := tls.X509KeyPair(signingPair.CertPEM, signingPair.KeyPEM)
if err != nil {
verr.AddViolation(".", fmt.Sprintf("not a valid TLS key pair: %s", err))
return
}
if len(tlsKeyPair.Certificate) != 1 {
verr.AddViolation("cert", "certificate must be a root CA (certificate chains are not allowed)") // Envoy constraint
return
}
cert, err := x509.ParseCertificate(tlsKeyPair.Certificate[0])
if err != nil {
verr.AddViolation("cert", fmt.Sprintf("not a valid x509 certificate: %s", err))
return
}
if cert.Issuer.String() != cert.Subject.String() {
verr.AddViolation("cert", "certificate must be self-signed (intermediate CAs are not allowed)") // Envoy constraint
}
if !cert.IsCA {
verr.AddViolation("cert", "basic constraint 'CA' must be set to 'true' (see X509-SVID: 4.1. Basic Constraints)")
}
if cert.KeyUsage&x509.KeyUsageCertSign == 0 {
verr.AddViolation("cert", "key usage extension 'keyCertSign' must be set (see X509-SVID: 4.3. Key Usage)")
}
if cert.KeyUsage&x509.KeyUsageDigitalSignature != 0 {
verr.AddViolation("cert", "key usage extension 'digitalSignature' must NOT be set (see X509-SVID: Appendix A. X.509 Field Reference)")
}
if cert.KeyUsage&x509.KeyUsageKeyAgreement != 0 {
verr.AddViolation("cert", "key usage extension 'keyAgreement' must NOT be set (see X509-SVID: Appendix A. X.509 Field Reference)")
}
if cert.KeyUsage&x509.KeyUsageKeyEncipherment != 0 {
verr.AddViolation("cert", "key usage extension 'keyEncipherment' must NOT be set (see X509-SVID: Appendix A. X.509 Field Reference)")
}

return
}
Loading