Use Certificate Requests to issue Kubelet Certificates and set CA (#261)

Signed-off-by: Daniel Weiße <dw@edgeless.systems>
This commit is contained in:
Daniel Weiße 2022-07-15 09:33:11 +02:00 committed by GitHub
parent 49e98286a9
commit c6ff34f4d2
13 changed files with 451 additions and 159 deletions

View file

@ -1,18 +1,18 @@
package kubernetesca
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"strings"
"time"
"github.com/edgelesssys/constellation/bootstrapper/util"
"github.com/edgelesssys/constellation/internal/file"
"github.com/edgelesssys/constellation/internal/logger"
kubeconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
)
const (
@ -35,22 +35,22 @@ func New(log *logger.Logger, fileHandler file.Handler) *KubernetesCA {
}
// GetCertificate creates a certificate for a node and signs it using the Kubernetes root CA.
func (c KubernetesCA) GetCertificate(nodeName string) (cert []byte, key []byte, err error) {
func (c KubernetesCA) GetCertificate(csr []byte) (cert []byte, err error) {
c.log.Debugf("Loading Kubernetes CA certificate")
parentCertRaw, err := c.file.Read(caCertFilename)
if err != nil {
return nil, nil, err
return nil, err
}
parentCertPEM, _ := pem.Decode(parentCertRaw)
parentCert, err := x509.ParseCertificate(parentCertPEM.Bytes)
if err != nil {
return nil, nil, err
return nil, err
}
c.log.Debugf("Loading Kubernetes CA private key")
parentKeyRaw, err := c.file.Read(caKeyFilename)
if err != nil {
return nil, nil, err
return nil, err
}
parentKeyPEM, _ := pem.Decode(parentKeyRaw)
var parentKey any
@ -62,30 +62,34 @@ func (c KubernetesCA) GetCertificate(nodeName string) (cert []byte, key []byte,
case "PRIVATE KEY":
parentKey, err = x509.ParsePKCS8PrivateKey(parentKeyPEM.Bytes)
default:
return nil, nil, fmt.Errorf("unsupported key type %q", parentCertPEM.Type)
return nil, fmt.Errorf("unsupported key type %q", parentCertPEM.Type)
}
if err != nil {
return nil, nil, err
return nil, err
}
c.log.Infof("Creating kubelet private key")
privK, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
certRequest, err := x509.ParseCertificateRequest(csr)
if err != nil {
return nil, nil, err
return nil, err
}
keyBytes, err := x509.MarshalECPrivateKey(privK)
if err != nil {
return nil, nil, err
if err := certRequest.CheckSignature(); err != nil {
return nil, err
}
kubeletKey := pem.EncodeToMemory(&pem.Block{
Type: "EC PRIVATE KEY",
Bytes: keyBytes,
})
c.log.Infof("Creating kubelet certificate")
if len(certRequest.Subject.Organization) != 1 {
return nil, errors.New("certificate request must have exactly one organization")
}
if certRequest.Subject.Organization[0] != kubeconstants.NodesGroup {
return nil, fmt.Errorf("certificate request must have organization %q but has %q", kubeconstants.NodesGroup, certRequest.Subject.Organization[0])
}
if !strings.HasPrefix(certRequest.Subject.CommonName, kubeconstants.NodesUserPrefix) {
return nil, fmt.Errorf("certificate request must have common name prefix %q but is %q", kubeconstants.NodesUserPrefix, certRequest.Subject.CommonName)
}
serialNumber, err := util.GenerateCertificateSerialNumber()
if err != nil {
return nil, nil, err
return nil, err
}
now := time.Now()
@ -95,25 +99,25 @@ func (c KubernetesCA) GetCertificate(nodeName string) (cert []byte, key []byte,
SerialNumber: serialNumber,
NotBefore: now.Add(-2 * time.Hour),
NotAfter: now.Add(24 * 365 * time.Hour),
Subject: pkix.Name{
Organization: []string{"system:nodes"},
CommonName: fmt.Sprintf("system:node:%s", nodeName),
},
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
Subject: certRequest.Subject,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{
x509.ExtKeyUsageClientAuth,
x509.ExtKeyUsageServerAuth,
},
IsCA: false,
BasicConstraintsValid: true,
IPAddresses: certRequest.IPAddresses,
}
certRaw, err := x509.CreateCertificate(rand.Reader, certTmpl, parentCert, &privK.PublicKey, parentKey)
certRaw, err := x509.CreateCertificate(rand.Reader, certTmpl, parentCert, certRequest.PublicKey, parentKey)
if err != nil {
return nil, nil, err
return nil, err
}
kubeletCert := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: certRaw,
})
return kubeletCert, kubeletKey, nil
return kubeletCert, nil
}

View file

@ -10,6 +10,7 @@ import (
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"strings"
"testing"
"time"
@ -19,6 +20,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
kubeconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
)
func TestMain(m *testing.M) {
@ -38,45 +40,126 @@ Q29uc3RlbGxhdGlvbg==
invalidCert := []byte(`-----BEGIN CERTIFICATE-----
Q29uc3RlbGxhdGlvbg==
-----END CERTIFICATE-----`)
defaultSigningRequestFunc := func() ([]byte, error) {
privK, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
csrTemplate := &x509.CertificateRequest{
Subject: pkix.Name{
Organization: []string{kubeconstants.NodesGroup},
CommonName: kubeconstants.NodesUserPrefix + "test-node",
},
}
return x509.CreateCertificateRequest(rand.Reader, csrTemplate, privK)
}
testCases := map[string]struct {
caCert []byte
caKey []byte
wantErr bool
caCert []byte
caKey []byte
createSigningRequest func() ([]byte, error)
wantErr bool
}{
"success ec key": {
caCert: ecCert,
caKey: ecKey,
caCert: ecCert,
caKey: ecKey,
createSigningRequest: defaultSigningRequestFunc,
},
"success rsa key": {
caCert: rsaCert,
caKey: rsaKey,
caCert: rsaCert,
caKey: rsaKey,
createSigningRequest: defaultSigningRequestFunc,
},
"success any key": {
caCert: testCert,
caKey: testKey,
caCert: testCert,
caKey: testKey,
createSigningRequest: defaultSigningRequestFunc,
},
"unsupported key": {
caCert: ecCert,
caKey: unsupportedKey,
wantErr: true,
caCert: ecCert,
caKey: unsupportedKey,
createSigningRequest: defaultSigningRequestFunc,
wantErr: true,
},
"invalid key": {
caCert: ecCert,
caKey: invalidKey,
wantErr: true,
caCert: ecCert,
caKey: invalidKey,
createSigningRequest: defaultSigningRequestFunc,
wantErr: true,
},
"invalid certificate": {
caCert: invalidCert,
caKey: ecKey,
wantErr: true,
caCert: invalidCert,
caKey: ecKey,
createSigningRequest: defaultSigningRequestFunc,
wantErr: true,
},
"no ca certificate": {
caKey: ecKey,
wantErr: true,
caKey: ecKey,
createSigningRequest: defaultSigningRequestFunc,
wantErr: true,
},
"no ca key": {
caCert: ecCert,
caCert: ecCert,
createSigningRequest: defaultSigningRequestFunc,
wantErr: true,
},
"no signing request": {
caCert: ecCert,
caKey: ecKey,
createSigningRequest: func() ([]byte, error) { return nil, nil },
wantErr: true,
},
"incorrect common name format": {
caCert: ecCert,
caKey: ecKey,
createSigningRequest: func() ([]byte, error) {
privK, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
csrTemplate := &x509.CertificateRequest{
Subject: pkix.Name{
Organization: []string{kubeconstants.NodesGroup},
CommonName: "test-node",
},
}
return x509.CreateCertificateRequest(rand.Reader, csrTemplate, privK)
},
wantErr: true,
},
"incorrect organization format": {
caCert: ecCert,
caKey: ecKey,
createSigningRequest: func() ([]byte, error) {
privK, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
csrTemplate := &x509.CertificateRequest{
Subject: pkix.Name{
Organization: []string{"test"},
CommonName: kubeconstants.NodesUserPrefix + "test-node",
},
}
return x509.CreateCertificateRequest(rand.Reader, csrTemplate, privK)
},
wantErr: true,
},
"no organization": {
caCert: ecCert,
caKey: ecKey,
createSigningRequest: func() ([]byte, error) {
privK, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
csrTemplate := &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: kubeconstants.NodesUserPrefix + "test-node",
},
}
return x509.CreateCertificateRequest(rand.Reader, csrTemplate, privK)
},
wantErr: true,
},
}
@ -100,8 +183,9 @@ Q29uc3RlbGxhdGlvbg==
file,
)
nodeName := "test"
kubeCert, kubeKey, err := ca.GetCertificate(nodeName)
signingRequest, err := tc.createSigningRequest()
require.NoError(err)
kubeCert, err := ca.GetCertificate(signingRequest)
if tc.wantErr {
assert.Error(err)
return
@ -112,19 +196,12 @@ Q29uc3RlbGxhdGlvbg==
require.NotNil(certPEM)
cert, err := x509.ParseCertificate(certPEM.Bytes)
require.NoError(err)
assert.Equal("system:node:"+nodeName, cert.Subject.CommonName)
assert.Equal("system:nodes", cert.Subject.Organization[0])
assert.True(strings.HasPrefix(cert.Subject.CommonName, kubeconstants.NodesUserPrefix))
assert.Equal(kubeconstants.NodesGroup, cert.Subject.Organization[0])
assert.Equal(x509.KeyUsageDigitalSignature|x509.KeyUsageKeyEncipherment, cert.KeyUsage)
assert.Equal(x509.ExtKeyUsageClientAuth, cert.ExtKeyUsage[0])
assert.False(cert.IsCA)
assert.True(cert.BasicConstraintsValid)
keyPEM, _ := pem.Decode(kubeKey)
require.NotNil(keyPEM)
key, err := x509.ParseECPrivateKey(keyPEM.Bytes)
require.NoError(err)
require.IsType(&ecdsa.PublicKey{}, cert.PublicKey)
assert.Equal(&key.PublicKey, cert.PublicKey.(*ecdsa.PublicKey))
})
}
}