VPN: move certificate signing logit to api service to which controller delegates ops
diff --git a/core/nebula/api/main.go b/core/nebula/api/main.go
index 906a674..9127630 100644
--- a/core/nebula/api/main.go
+++ b/core/nebula/api/main.go
@@ -15,6 +15,7 @@
 	"io"
 	"io/ioutil"
 	"log"
+	"net"
 	"net/http"
 	"time"
 
@@ -262,8 +263,6 @@
 		http.Error(w, err.Error(), http.StatusBadRequest)
 		return
 	}
-	fmt.Println("---- APPROVE")
-	fmt.Printf("%#v\n", req)
 	_, _, err := h.mgr.CreateNode(
 		*namespace,
 		req.Name,
@@ -280,6 +279,59 @@
 	}
 }
 
+type processCAReq struct {
+	Name string `json:"name"`
+}
+
+func (h *Handler) processCA(w http.ResponseWriter, r *http.Request) {
+	var req processCAReq
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+	ca, err := CreateCertificateAuthority(req.Name)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	w.Header().Set("Content-Type", "application/json")
+	if err := json.NewEncoder(w).Encode(ca); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+}
+
+type processNodeReq struct {
+	CAPrivateKey  []byte `json:"ca_private_key"`
+	CACert        []byte `json:"ca_certificate"`
+	NodeName      string `json:"node_name"`
+	NodePublicKey []byte `json:"node_public_key,omitempty"`
+	NodeIPCidr    string `json:"node_ip_cidr"`
+}
+
+func (h *Handler) processNode(w http.ResponseWriter, r *http.Request) {
+	var req processNodeReq
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+	_, ipNet, err := net.ParseCIDR(req.NodeIPCidr)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+	node, err := SignNebulaNode(req.CAPrivateKey, req.CACert, req.NodeName, req.NodePublicKey, ipNet)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+	w.Header().Set("Content-Type", "application/json")
+	if err := json.NewEncoder(w).Encode(node); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+}
+
 func loadConfigTemplate(path string) (map[string]interface{}, error) {
 	tmpl, err := ioutil.ReadFile(path)
 	if err != nil {
@@ -327,6 +379,8 @@
 	r.HandleFunc("/api/join", handler.join)
 	r.HandleFunc("/api/approve", handler.approve)
 	r.HandleFunc("/api/get/{name:[a-zA-z0-9-]+}", handler.get)
+	r.HandleFunc("/api/process/authority", handler.processCA)
+	r.HandleFunc("/api/process/node", handler.processNode)
 	r.HandleFunc("/node/{namespace:[a-zA-z0-9-]+}/{name:[a-zA-z0-9-]+}", handler.handleNode)
 	r.HandleFunc("/ca/{namespace:[a-zA-z0-9-]+}/{name:[a-zA-z0-9-]+}", handler.handleCA)
 	r.HandleFunc("/", handler.handleIndex)
diff --git a/core/nebula/api/nebula.go b/core/nebula/api/nebula.go
new file mode 100644
index 0000000..b4649f1
--- /dev/null
+++ b/core/nebula/api/nebula.go
@@ -0,0 +1,125 @@
+package main
+
+import (
+	"crypto/ed25519"
+	"crypto/rand"
+	"errors"
+	"io"
+	"net"
+	"time"
+
+	"github.com/slackhq/nebula/cert"
+	"golang.org/x/crypto/curve25519"
+)
+
+type CertificateAuthority struct {
+	PrivateKey  []byte `json:"private_key"`
+	Certificate []byte `json:"certificate"`
+}
+
+func CreateCertificateAuthority(name string) (*CertificateAuthority, error) {
+	t := time.Now().Add(time.Duration(-1 * time.Second))
+	rawPub, rawPriv, err := ed25519.GenerateKey(rand.Reader)
+	if err != nil {
+		return nil, err
+	}
+	nc := cert.NebulaCertificate{
+		Details: cert.NebulaCertificateDetails{
+			Name:      name,
+			NotBefore: t,
+			NotAfter:  t.Add(time.Duration(8760 * time.Hour)),
+			PublicKey: rawPub,
+			IsCA:      true,
+		},
+	}
+	if err := nc.Sign(rawPriv); err != nil {
+		return nil, err
+	}
+	certSerialized, err := nc.MarshalToPEM()
+	if err != nil {
+		return nil, err
+	}
+	privKeySerialzied := cert.MarshalEd25519PrivateKey(rawPriv)
+	return &CertificateAuthority{
+		privKeySerialzied,
+		certSerialized,
+	}, nil
+}
+
+type NebulaNode struct {
+	PrivateKey  []byte `json:"private_key,omitempty"`
+	Certificate []byte `json:"certificate"`
+}
+
+func SignNebulaNode(rawCAPrivateKey []byte, rawCACert []byte, nodeName string, nodePublicKey []byte, ip *net.IPNet) (*NebulaNode, error) {
+	caKey, _, err := cert.UnmarshalEd25519PrivateKey(rawCAPrivateKey)
+	if err != nil {
+		return nil, err
+	}
+	caCert, _, err := cert.UnmarshalNebulaCertificateFromPEM(rawCACert)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := caCert.VerifyPrivateKey(caKey); err != nil {
+		return nil, err
+	}
+	issuer, err := caCert.Sha256Sum()
+	if err != nil {
+		return nil, err
+	}
+	if caCert.Expired(time.Now()) {
+		return nil, errors.New("ca certificate is expired")
+	}
+	var pub, priv []byte
+	if nodePublicKey != nil {
+		var err error
+		pub, _, err = cert.UnmarshalX25519PublicKey(nodePublicKey)
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		var rawPriv []byte
+		var err error
+		pub, rawPriv, err = x25519Keypair()
+		if err != nil {
+			return nil, err
+		}
+		priv = cert.MarshalX25519PrivateKey(rawPriv)
+	}
+	t := time.Now().Add(time.Duration(-1 * time.Second))
+	nc := cert.NebulaCertificate{
+		Details: cert.NebulaCertificateDetails{
+			Name:      nodeName,
+			Ips:       []*net.IPNet{ip},
+			NotBefore: t,
+			NotAfter:  caCert.Details.NotAfter.Add(time.Duration(-1 * time.Second)),
+			PublicKey: pub,
+			IsCA:      false,
+			Issuer:    issuer,
+		},
+	}
+	if err := nc.CheckRootConstrains(caCert); err != nil {
+		return nil, err
+	}
+	if err := nc.Sign(caKey); err != nil {
+		return nil, err
+	}
+	certSerialized, err := nc.MarshalToPEM()
+	if err != nil {
+		return nil, err
+	}
+	return &NebulaNode{
+		PrivateKey:  priv,
+		Certificate: certSerialized,
+	}, nil
+}
+
+func x25519Keypair() ([]byte, []byte, error) {
+	var pubkey, privkey [32]byte
+	if _, err := io.ReadFull(rand.Reader, privkey[:]); err != nil {
+		return nil, nil, err
+	}
+	curve25519.ScalarBaseMult(&pubkey, &privkey)
+	return pubkey[:], privkey[:], nil
+}
diff --git a/core/nebula/controller/Dockerfile b/core/nebula/controller/Dockerfile
index 82586e4..79b4809 100644
--- a/core/nebula/controller/Dockerfile
+++ b/core/nebula/controller/Dockerfile
@@ -5,7 +5,7 @@
 COPY controller_${TARGETARCH} /usr/bin/nebula-controller
 RUN chmod +x /usr/bin/nebula-controller
 
-RUN wget https://github.com/slackhq/nebula/releases/download/v1.4.0/nebula-linux-${TARGETARCH}.tar.gz -O nebula.tar.gz
-RUN tar -xvf nebula.tar.gz
-RUN mv nebula-cert /usr/bin
-RUN chmod +x /usr/bin/nebula-cert
+# RUN wget https://github.com/slackhq/nebula/releases/download/v1.4.0/nebula-linux-${TARGETARCH}.tar.gz -O nebula.tar.gz
+# RUN tar -xvf nebula.tar.gz
+# RUN mv nebula-cert /usr/bin
+# RUN chmod +x /usr/bin/nebula-cert
diff --git a/core/nebula/controller/controllers/ca.go b/core/nebula/controller/controllers/ca.go
index 37b3856..9cc284b 100644
--- a/core/nebula/controller/controllers/ca.go
+++ b/core/nebula/controller/controllers/ca.go
@@ -3,10 +3,6 @@
 import (
 	"context"
 	"fmt"
-	"io/ioutil"
-	"os"
-	"os/exec"
-	"path/filepath"
 	"time"
 
 	corev1 "k8s.io/api/core/v1"
@@ -47,16 +43,13 @@
 	secretLister corev1listers.SecretLister
 	secretSynced cache.InformerSynced
 	workqueue    workqueue.RateLimitingInterface
-
-	nebulaCert string
 }
 
 func NewNebulaController(kubeClient kubernetes.Interface,
 	nebulaClient clientset.Interface,
 	caInformer informers.NebulaCAInformer,
 	nodeInformer informers.NebulaNodeInformer,
-	secretInformer corev1informers.SecretInformer,
-	nebulaCert string) *NebulaController {
+	secretInformer corev1informers.SecretInformer) *NebulaController {
 	c := &NebulaController{
 		kubeClient:   kubeClient,
 		nebulaClient: nebulaClient,
@@ -67,7 +60,6 @@
 		secretLister: secretInformer.Lister(),
 		secretSynced: secretInformer.Informer().HasSynced,
 		workqueue:    workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "Nebula"),
-		nebulaCert:   nebulaCert,
 	}
 
 	caInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
@@ -186,17 +178,20 @@
 		fmt.Printf("%s CA is already in Ready state\n", ca.Name)
 		return nil
 	}
-	keyDir, err := generateCAKey(ca.Name, c.nebulaCert)
+	privKey, cert, err := CreateCertificateAuthority(apiAddr(ca.Name), ca.Name)
 	if err != nil {
 		return err
 	}
-	defer os.RemoveAll(keyDir)
-	secret, err := createSecretFromDir(keyDir)
-	if err != nil {
-		return err
+	secret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: ca.Spec.SecretName,
+		},
+		Immutable: &secretImmutable,
+		Data: map[string][]byte{
+			"ca.key": privKey,
+			"ca.crt": cert,
+		},
 	}
-	secret.Immutable = &secretImmutable
-	secret.Name = ca.Spec.SecretName
 	_, err = c.kubeClient.CoreV1().Secrets(namespace).Create(context.TODO(), secret, metav1.CreateOptions{})
 	if err != nil {
 		return err
@@ -239,32 +234,25 @@
 		}
 		return err
 	}
-	dir, err := extractSecret(caSecret)
+	var pubKey []byte
+	if node.Spec.PubKey != "" {
+		pubKey = []byte(node.Spec.PubKey)
+	}
+	privKey, nodeCert, err := SignNebulaNode(apiAddr(ca.Name), caSecret.Data["ca.key"], caSecret.Data["ca.crt"], node.Name, pubKey, node.Spec.IPCidr)
 	if err != nil {
 		return err
 	}
-	if node.Spec.PubKey == "" {
-		if err := generateNodeKey(node.Name, node.Spec.IPCidr, dir, c.nebulaCert); err != nil {
-			return err
-		}
-	} else {
-		if err := generateNodeKeyFromPub(node.Name, node.Spec.IPCidr, node.Spec.PubKey, dir, c.nebulaCert); err != nil {
-			return err
-		}
+	secret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: node.Spec.SecretName,
+		},
+		Immutable: &secretImmutable,
+		Data: map[string][]byte{
+			"ca.crt":   caSecret.Data["ca.crt"],
+			"host.crt": nodeCert,
+			"host.key": privKey,
+		},
 	}
-	defer os.RemoveAll(dir)
-	if err := os.Remove(filepath.Join(dir, "ca.key")); err != nil {
-		return err
-	}
-	if err := os.Remove(filepath.Join(dir, "ca.png")); err != nil {
-		return err
-	}
-	secret, err := createSecretFromDir(dir)
-	if err != nil {
-		return err
-	}
-	secret.Immutable = &secretImmutable
-	secret.Name = node.Spec.SecretName
 	_, err = c.kubeClient.CoreV1().Secrets(namespace).Create(context.TODO(), secret, metav1.CreateOptions{})
 	if err != nil {
 		return err
@@ -292,88 +280,7 @@
 	return err
 }
 
-func createSecretFromDir(path string) (*corev1.Secret, error) {
-	all, err := ioutil.ReadDir(path)
-	if err != nil {
-		return nil, err
-	}
-	secret := &corev1.Secret{
-		Data: make(map[string][]byte),
-	}
-	for _, f := range all {
-		if f.IsDir() {
-			continue
-		}
-		d, err := ioutil.ReadFile(filepath.Join(path, f.Name()))
-		if err != nil {
-			return nil, err
-		}
-		secret.Data[f.Name()] = d
-	}
-	return secret, nil
-}
-
-func extractSecret(secret *corev1.Secret) (string, error) {
-	tmp, err := os.MkdirTemp("", secret.Name)
-	if err != nil {
-		return "", err
-	}
-	for name, data := range secret.Data {
-		if err := ioutil.WriteFile(filepath.Join(tmp, name), data, 0644); err != nil {
-			defer os.RemoveAll(tmp)
-			return "", err
-		}
-	}
-	return tmp, nil
-}
-
-func generateCAKey(name, nebulaCert string) (string, error) {
-	tmp, err := os.MkdirTemp("", name)
-	if err != nil {
-		return "", err
-	}
-	cmd := exec.Command(nebulaCert, "ca",
-		"-name", name,
-		"-out-key", filepath.Join(tmp, "ca.key"),
-		"-out-crt", filepath.Join(tmp, "ca.crt"),
-		"-out-qr", filepath.Join(tmp, "ca.png"))
-	if d, err := cmd.CombinedOutput(); err != nil {
-		return "", fmt.Errorf(string(d))
-	}
-	return tmp, nil
-}
-
-func generateNodeKeyFromPub(name, ip, pubKey, dir, nebulaCert string) error {
-	hostPub := filepath.Join(dir, "host.pub")
-	if err := ioutil.WriteFile(hostPub, []byte(pubKey), 0644); err != nil {
-		return err
-	}
-	defer os.Remove(hostPub)
-	cmd := exec.Command(nebulaCert, "sign",
-		"-ca-crt", filepath.Join(dir, "ca.crt"),
-		"-ca-key", filepath.Join(dir, "ca.key"),
-		"-name", name,
-		"-ip", ip,
-		"-in-pub", hostPub,
-		"-out-crt", filepath.Join(dir, "host.crt"),
-		"-out-qr", filepath.Join(dir, "host.png"))
-	if _, err := cmd.CombinedOutput(); err != nil {
-		return err
-	}
-	return nil
-}
-
-func generateNodeKey(name, ip, dir, nebulaCert string) error {
-	cmd := exec.Command(nebulaCert, "sign",
-		"-ca-crt", filepath.Join(dir, "ca.crt"),
-		"-ca-key", filepath.Join(dir, "ca.key"),
-		"-name", name,
-		"-ip", ip,
-		"-out-key", filepath.Join(dir, "host.key"),
-		"-out-crt", filepath.Join(dir, "host.crt"),
-		"-out-qr", filepath.Join(dir, "host.png"))
-	if _, err := cmd.CombinedOutput(); err != nil {
-		return err
-	}
-	return nil
+// TODO(giolekva): maybe pass by flag?
+func apiAddr(ca string) string {
+	return fmt.Sprintf("http://nebula-api.%s-ingress-private.svc.cluster.local", ca)
 }
diff --git a/core/nebula/controller/controllers/client.go b/core/nebula/controller/controllers/client.go
new file mode 100644
index 0000000..da5b818
--- /dev/null
+++ b/core/nebula/controller/controllers/client.go
@@ -0,0 +1,81 @@
+package controllers
+
+import (
+	"bytes"
+	"crypto/tls"
+	"encoding/json"
+	"net/http"
+)
+
+type createCAReq struct {
+	Name string `json:"name"`
+}
+
+type createCAResp struct {
+	PrivateKey  []byte `json:"private_key"`
+	Certificate []byte `json:"certificate"`
+}
+
+func CreateCertificateAuthority(apiAddr, name string) ([]byte, []byte, error) {
+	var data bytes.Buffer
+	if err := json.NewEncoder(&data).Encode(createCAReq{name}); err != nil {
+		return nil, nil, err
+	}
+	client := &http.Client{
+		// TODO(giolekva): remove, for some reason valid certificates are not accepted on gioui android.
+		Transport: &http.Transport{
+			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+		},
+	}
+	resp, err := client.Post(apiAddr+"/api/process/ca", "application/json", &data)
+	if err != nil {
+		return nil, nil, err
+	}
+	var ret createCAResp
+	if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil {
+		return nil, nil, err
+	}
+	return ret.PrivateKey, ret.Certificate, nil
+}
+
+type signNodeReq struct {
+	CAPrivateKey  []byte `json:"ca_private_key"`
+	CACert        []byte `json:"ca_certificate"`
+	NodeName      string `json:"node_name"`
+	NodePublicKey []byte `json:"node_public_key,omitempty"`
+	NodeIPCidr    string `json:"node_ip_cidr"`
+}
+
+type signNodeResp struct {
+	PrivateKey  []byte `json:"private_key,omitempty"`
+	Certificate []byte `json:"certificate"`
+}
+
+func SignNebulaNode(apiAddr string, caPrivateKey, caCert []byte, nodeName string, nodePublicKey []byte, nodeIp string) ([]byte, []byte, error) {
+	req := signNodeReq{
+		caPrivateKey,
+		caCert,
+		nodeName,
+		nodePublicKey,
+		nodeIp,
+	}
+	var data bytes.Buffer
+	if err := json.NewEncoder(&data).Encode(req); err != nil {
+		return nil, nil, err
+	}
+	client := &http.Client{
+		// TODO(giolekva): remove, for some reason valid certificates are not accepted on gioui android.
+		Transport: &http.Transport{
+			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+		},
+	}
+	resp, err := client.Post(apiAddr+"/api/process/node", "application/json", &data)
+	if err != nil {
+		return nil, nil, err
+	}
+	var ret signNodeResp
+	if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil {
+		return nil, nil, err
+	}
+	return ret.PrivateKey, ret.Certificate, nil
+}
diff --git a/core/nebula/controller/main.go b/core/nebula/controller/main.go
index e230129..7867491 100644
--- a/core/nebula/controller/main.go
+++ b/core/nebula/controller/main.go
@@ -19,7 +19,6 @@
 
 var kubeConfig = flag.String("kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.")
 var masterURL = flag.String("master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.")
-var nebulaCert = flag.String("nebula-cert", "", "Path to the nebula-cert binary.")
 
 func main() {
 	flag.Parse()
@@ -40,8 +39,7 @@
 		nebulaClient,
 		nebulaInformerFactory.Lekva().V1().NebulaCAs(),
 		nebulaInformerFactory.Lekva().V1().NebulaNodes(),
-		kubeInformerFactory.Core().V1().Secrets(),
-		*nebulaCert)
+		kubeInformerFactory.Core().V1().Secrets())
 	stopCh := make(chan struct{})
 	kubeInformerFactory.Start(stopCh)
 	nebulaInformerFactory.Start(stopCh)