cluster/prodvider: rewrite against x509 lib for ed25519 support

This gets rid of cfssl for the kubernetes bits of prodvider, instead
using plain crypto/x509. This also allows to support our new fancy
ED25519 CA.

Change-Id: If677b3f4523014f56ea802b87499d1c0eb6d92e9
Reviewed-on: https://gerrit.hackerspace.pl/c/hscloud/+/1489
Reviewed-by: q3k <q3k@hackerspace.pl>
diff --git a/cluster/kube/lib/prodvider.libsonnet b/cluster/kube/lib/prodvider.libsonnet
index 30b1674..f131681 100644
--- a/cluster/kube/lib/prodvider.libsonnet
+++ b/cluster/kube/lib/prodvider.libsonnet
@@ -9,7 +9,7 @@
 
         cfg:: {
             namespace: "prodvider",
-            image: "registry.k0.hswaw.net/q3k/prodvider:315532800-21bacc96d76e4f2074e769dfc65ab43702f52d10",
+            image: "registry.k0.hswaw.net/q3k/prodvider:1680301337",
 
             apiEndpoint: error "API endpoint must be set",
 
@@ -19,7 +19,7 @@
                     key: importstr "../../secrets/plain/ca-kube-prodvider.key",
                 },
                 kube: {
-                    cert: importstr "../../certs/ca-kube.crt",
+                    cert: importstr "../../certs/ca-kube-new.crt",
                 },
             }
         },
diff --git a/cluster/prodvider/BUILD.bazel b/cluster/prodvider/BUILD.bazel
index 81689c0..00dedc6 100644
--- a/cluster/prodvider/BUILD.bazel
+++ b/cluster/prodvider/BUILD.bazel
@@ -63,5 +63,5 @@
     format = "Docker",
     registry = "registry.k0.hswaw.net",
     repository = "q3k/prodvider",
-    tag = "{BUILD_TIMESTAMP}-{STABLE_GIT_COMMIT}",
+    tag = "1680301337",
 )
diff --git a/cluster/prodvider/certs.go b/cluster/prodvider/certs.go
index 309af1f..acc89f9 100644
--- a/cluster/prodvider/certs.go
+++ b/cluster/prodvider/certs.go
@@ -1,113 +1,118 @@
 package main
 
 import (
+	"crypto/ed25519"
+	"crypto/rand"
 	"crypto/tls"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/pem"
 	"fmt"
+	"math/big"
+	"net"
 	"time"
 
-	"github.com/cloudflare/cfssl/csr"
-	"github.com/cloudflare/cfssl/signer"
 	"github.com/golang/glog"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials"
 )
 
+func serializeCert(der []byte) []byte {
+	return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
+}
+
+func serializeKey(priv ed25519.PrivateKey) []byte {
+	pkcs8, err := x509.MarshalPKCS8PrivateKey(priv)
+	if err != nil {
+		return nil
+	}
+
+	block := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: pkcs8})
+	return block
+}
+
 func (p *prodvider) selfCreds() grpc.ServerOption {
 	glog.Infof("Bootstrapping certificate for self (%q)...", flagProdviderCN)
 
-	// Create a key and CSR.
-	csrPEM, keyPEM, err := p.makeSelfCSR()
-	if err != nil {
-		glog.Exitf("Could not generate key and CSR for self: %v", err)
-	}
-
 	// Create a cert
-	certPEM, err := p.makeSelfCertificate(csrPEM)
+	keyRaw, certRaw, err := p.makeSelfCertificate()
 	if err != nil {
 		glog.Exitf("Could not sign certificate for self: %v", err)
 	}
 
-	serverCert, err := tls.X509KeyPair(certPEM, keyPEM)
+	serverCert, err := tls.X509KeyPair(serializeCert(certRaw), serializeKey(keyRaw))
 	if err != nil {
 		glog.Exitf("Could not use gRPC certificate: %v", err)
 	}
 
-	signerCert, _ := p.sign.Certificate("", "")
-	serverCert.Certificate = append(serverCert.Certificate, signerCert.Raw)
+	serverCert.Certificate = append(serverCert.Certificate, p.intermediateCACert.Raw)
 
 	return grpc.Creds(credentials.NewTLS(&tls.Config{
 		Certificates: []tls.Certificate{serverCert},
 	}))
 }
 
-func (p *prodvider) makeSelfCSR() ([]byte, []byte, error) {
-	signerCert, _ := p.sign.Certificate("", "")
-	req := &csr.CertificateRequest{
-		CN: flagProdviderCN,
-		KeyRequest: &csr.BasicKeyRequest{
-			A: "rsa",
-			S: 4096,
+func (p *prodvider) makeSelfCertificate() (ed25519.PrivateKey, []byte, error) {
+	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 127)
+	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
+	if err != nil {
+		return nil, nil, err
+	}
+	template := &x509.Certificate{
+		Subject: pkix.Name{
+			CommonName: flagProdviderCN,
 		},
-		Names: []csr.Name{
-			{
-				C:  signerCert.Subject.Country[0],
-				ST: signerCert.Subject.Province[0],
-				L:  signerCert.Subject.Locality[0],
-				O:  signerCert.Subject.Organization[0],
-				OU: signerCert.Subject.OrganizationalUnit[0],
-			},
+		NotBefore:    time.Now(),
+		NotAfter:     time.Now().Add(30 * 24 * time.Hour),
+		KeyUsage:     x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
+		ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+		SerialNumber: serialNumber,
+		DNSNames:     []string{flagProdviderCN},
+		IPAddresses: []net.IP{
+			{127, 0, 0, 1},
 		},
-		Hosts: []string{flagProdviderCN},
 	}
 
-	g := &csr.Generator{
-		Validator: func(req *csr.CertificateRequest) error { return nil },
+	pkey, skey, err := ed25519.GenerateKey(rand.Reader)
+	if err != nil {
+		return nil, nil, err
 	}
-
-	return g.ProcessRequest(req)
+	bytes, err := x509.CreateCertificate(rand.Reader, template, p.intermediateCACert, pkey, p.intermediateCAKey)
+	if err != nil {
+		return nil, nil, err
+	}
+	return skey, bytes, nil
 }
 
-func (p *prodvider) makeSelfCertificate(csr []byte) ([]byte, error) {
-	req := signer.SignRequest{
-		Hosts:   []string{flagProdviderCN},
-		Request: string(csr),
-		Profile: "server",
+func (p *prodvider) makeKubernetesCertificate(username, o string, notAfter time.Time) (ed25519.PrivateKey, []byte, error) {
+	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 127)
+	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
+	if err != nil {
+		return nil, nil, err
 	}
-	return p.sign.Sign(req)
-}
-
-func (p *prodvider) makeKubernetesCSR(username, o string) ([]byte, []byte, error) {
-	signerCert, _ := p.sign.Certificate("", "")
-	req := &csr.CertificateRequest{
-		CN: username,
-		KeyRequest: &csr.BasicKeyRequest{
-			A: "rsa",
-			S: 4096,
+	template := &x509.Certificate{
+		Subject: pkix.Name{
+			Organization:       []string{o},
+			OrganizationalUnit: []string{fmt.Sprintf("Prodvider Kubernetes Cert for %s/%s", username, o)},
+			CommonName:         username,
 		},
-		Names: []csr.Name{
-			{
-				C:  signerCert.Subject.Country[0],
-				ST: signerCert.Subject.Province[0],
-				L:  signerCert.Subject.Locality[0],
-				O:  o,
-				OU: fmt.Sprintf("Prodvider Kubernetes Cert for %s/%s", username, o),
-			},
+		NotBefore:   time.Now(),
+		NotAfter:    notAfter,
+		KeyUsage:    x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
+		ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
+		DNSNames: []string{
+			username,
 		},
+		SerialNumber: serialNumber,
 	}
 
-	g := &csr.Generator{
-		Validator: func(req *csr.CertificateRequest) error { return nil },
+	pkey, skey, err := ed25519.GenerateKey(rand.Reader)
+	if err != nil {
+		return nil, nil, err
 	}
-
-	return g.ProcessRequest(req)
-}
-
-func (p *prodvider) makeKubernetesCertificate(csr []byte, notAfter time.Time) ([]byte, error) {
-	req := signer.SignRequest{
-		Hosts:    []string{},
-		Request:  string(csr),
-		Profile:  "client",
-		NotAfter: notAfter,
+	bytes, err := x509.CreateCertificate(rand.Reader, template, p.intermediateCACert, pkey, p.intermediateCAKey)
+	if err != nil {
+		return nil, nil, err
 	}
-	return p.sign.Sign(req)
+	return skey, bytes, nil
 }
diff --git a/cluster/prodvider/kubernetes.go b/cluster/prodvider/kubernetes.go
index d7ad535..4f73ce4 100644
--- a/cluster/prodvider/kubernetes.go
+++ b/cluster/prodvider/kubernetes.go
@@ -2,7 +2,6 @@
 
 import (
 	"context"
-	"encoding/pem"
 	"fmt"
 	"time"
 
@@ -19,62 +18,46 @@
 
 func (p *prodvider) kubernetesCreds(username string) (*pb.KubernetesKeys, error) {
 	o := fmt.Sprintf("sso:%s", username)
+	email := username + "@hackerspace.pl"
 
-	csrPEM, keyPEM, err := p.makeKubernetesCSR(username+"@hackerspace.pl", o)
+	keyRaw, certBytes, err := p.makeKubernetesCertificate(email, o, time.Now().Add(13*time.Hour))
 	if err != nil {
 		return nil, err
 	}
 
-	certPEM, err := p.makeKubernetesCertificate(csrPEM, time.Now().Add(13*time.Hour))
-	if err != nil {
-		return nil, err
-	}
-
-	caCert, _ := p.sign.Certificate("", "")
-	caPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert.Raw})
-
 	// Build certificate chain from new cert and intermediate CA.
-	chainPEM := append(certPEM, caPEM...)
+	chainPEM := append(serializeCert(certBytes), serializeCert(p.intermediateCACert.Raw)...)
 
 	glog.Infof("Generated k8s certificate for %q", username)
 	return &pb.KubernetesKeys{
 		Cluster: "k0.hswaw.net",
 		// APIServerCA
-		Ca: p.kubeCAPEM,
+		Ca: serializeCert(p.kubeCACert.Raw),
 		// Chain of new cert + intermediate CA
 		Cert: chainPEM,
-		Key:  keyPEM,
+		Key:  serializeKey(keyRaw),
 	}, nil
 }
 
 func (p *prodvider) kubernetesConnect() error {
-	csrPEM, keyPEM, err := p.makeKubernetesCSR("prodvider", "system:masters")
+	keyRaw, certBytes, err := p.makeKubernetesCertificate("prodvider", "system:masters", time.Now().Add(30*24*time.Hour))
 	if err != nil {
 		return err
 	}
 
-	certPEM, err := p.makeKubernetesCertificate(csrPEM, time.Now().Add(30*24*time.Hour))
-	if err != nil {
-		return err
-	}
-
-	caCert, _ := p.sign.Certificate("", "")
-
-	caPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert.Raw})
-
 	glog.Infof("Generated k8s certificate for self (system:masters)")
 
 	// Build certificate chain from our cert and intermediate CA.
-	chainPEM := append(certPEM, caPEM...)
+	chainPEM := append(serializeCert(certBytes), serializeCert(p.intermediateCACert.Raw)...)
 
 	config := &rest.Config{
 		Host: flagKubernetesHost,
 		TLSClientConfig: rest.TLSClientConfig{
 			// Chain to authenticate ourselves (us + intermediate CA).
 			CertData: chainPEM,
-			KeyData:  keyPEM,
+			KeyData:  serializeKey(keyRaw),
 			// APIServer CA for verification.
-			CAData: p.kubeCAPEM,
+			CAData: serializeCert(p.kubeCACert.Raw),
 		},
 	}
 
diff --git a/cluster/prodvider/main.go b/cluster/prodvider/main.go
index 7222a86..ca114f0 100644
--- a/cluster/prodvider/main.go
+++ b/cluster/prodvider/main.go
@@ -1,15 +1,16 @@
 package main
 
 import (
+	"crypto/ed25519"
+	"crypto/x509"
+	"encoding/pem"
 	"flag"
-	"io/ioutil"
+	"fmt"
 	"math/rand"
 	"net"
 	"os"
 	"time"
 
-	"github.com/cloudflare/cfssl/config"
-	"github.com/cloudflare/cfssl/signer/local"
 	"github.com/golang/glog"
 	"google.golang.org/grpc"
 	"k8s.io/client-go/kubernetes"
@@ -36,44 +37,66 @@
 }
 
 type prodvider struct {
-	sign      *local.Signer
-	k8s       *kubernetes.Clientset
-	srv       *grpc.Server
-	kubeCAPEM []byte
+	k8s *kubernetes.Clientset
+	srv *grpc.Server
+
+	intermediateCAKey  ed25519.PrivateKey
+	intermediateCACert *x509.Certificate
+	kubeCACert         *x509.Certificate
+}
+
+func loadCert(path string) (*x509.Certificate, error) {
+	b, err := os.ReadFile(path)
+	if err != nil {
+		return nil, err
+	}
+
+	block, _ := pem.Decode(b)
+	if block == nil {
+		return nil, fmt.Errorf("no PEM block found")
+	}
+	if block.Type != "CERTIFICATE" {
+		return nil, fmt.Errorf("unexpected PEM block: %q", block.Type)
+	}
+	return x509.ParseCertificate(block.Bytes)
+}
+
+func loadKey(path string) (ed25519.PrivateKey, error) {
+	bytes, err := os.ReadFile(path)
+	if err != nil {
+		return nil, err
+	}
+	block, _ := pem.Decode(bytes)
+	if block == nil {
+		return nil, fmt.Errorf("no PEM block found")
+	}
+	if block.Type != "PRIVATE KEY" {
+		return nil, fmt.Errorf("unexpected PEM block: %q", block.Type)
+	}
+	key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
+	if err != nil {
+		return nil, err
+	}
+	if k, ok := key.(ed25519.PrivateKey); ok {
+		return k, nil
+	}
+	return nil, fmt.Errorf("not an ED25519 key")
 }
 
 func newProdvider() *prodvider {
-	policy := &config.Signing{
-		Profiles: map[string]*config.SigningProfile{
-			"server": &config.SigningProfile{
-				Usage:        []string{"signing", "key encipherment", "server auth"},
-				ExpiryString: "30d",
-			},
-			"client": &config.SigningProfile{
-				Usage:        []string{"signing", "key encipherment", "client auth"},
-				ExpiryString: "30d",
-			},
-			"client-server": &config.SigningProfile{
-				Usage:        []string{"signing", "key encipherment", "server auth", "client auth"},
-				ExpiryString: "30d",
-			},
-		},
-		Default: config.DefaultConfig(),
-	}
-
-	sign, err := local.NewSignerFromFile(flagCACertificatePath, flagCAKeyPath, policy)
+	kubeCACert, err := loadCert(flagKubeCACertificatePath)
 	if err != nil {
-		glog.Exitf("Could not create signer: %v", err)
+		glog.Exitf("Loading kube CA certificate failed: %v", err)
 	}
-
-	kubeCAPEM, err := ioutil.ReadFile(flagKubeCACertificatePath)
+	intermediateCACert, err := loadCert(flagCACertificatePath)
 	if err != nil {
-		glog.Exitf("Could not read kube CA cert path: %v")
+		glog.Exitf("Loading intermediate CAcertificate failed: %v", err)
 	}
-
+	intermediateCAKey, err := loadKey(flagCAKeyPath)
 	return &prodvider{
-		sign:      sign,
-		kubeCAPEM: kubeCAPEM,
+		intermediateCAKey:  intermediateCAKey,
+		intermediateCACert: intermediateCACert,
+		kubeCACert:         kubeCACert,
 	}
 }