cloud: init with apigw

This adds a first component to the cloud project, the apigw (API
Gateway), which listens on a public gRPC-Web socket.

It's not truly a gateway - it will actually contain most of the
IAM/Project logic for the cloud system. A better name should be picked
later.

We implement a minimum internal/public gRPC(-Web) listener and some
boilerplate for the parts that are gonna pop up again. Notably, we add
some automation around generating developer TLS certificates for the
internal gRPC listener.

Currently the apigw serves a single, demo RPC which returns
'unimplemented'.

Change-Id: I9164ddbd9a20172154ae5a3ffad676de5fe4927d
Reviewed-on: https://review.monogon.dev/c/monogon/+/906
Reviewed-by: Leopold Schabel <leo@monogon.tech>
Tested-by: Jenkins CI
diff --git a/cloud/lib/component/BUILD.bazel b/cloud/lib/component/BUILD.bazel
new file mode 100644
index 0000000..a97d770
--- /dev/null
+++ b/cloud/lib/component/BUILD.bazel
@@ -0,0 +1,18 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "component",
+    srcs = [
+        "component.go",
+        "devcerts.go",
+    ],
+    importpath = "source.monogon.dev/cloud/lib/component",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//metropolis/pkg/pki",
+        "@com_github_adrg_xdg//:xdg",
+        "@io_k8s_klog_v2//:klog",
+        "@org_golang_google_grpc//:go_default_library",
+        "@org_golang_google_grpc//credentials",
+    ],
+)
diff --git a/cloud/lib/component/component.go b/cloud/lib/component/component.go
new file mode 100644
index 0000000..4353bdf
--- /dev/null
+++ b/cloud/lib/component/component.go
@@ -0,0 +1,105 @@
+// Package component implements reusable bits for cloud service components. Each
+// component is currently defined as being a standalone Go binary with its own
+// internal gRPC listener. Subsequent listeners (eg. public gRPC or HTTP) can be
+// defined by users of this library.
+package component
+
+import (
+	"crypto/tls"
+	"crypto/x509"
+	"flag"
+	"os"
+	"path/filepath"
+
+	"github.com/adrg/xdg"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials"
+	"k8s.io/klog/v2"
+)
+
+// Configuration is the common configuration of a component.
+type Configuration struct {
+	// GRPCKeyPath is the filesystem path of the x509 key used to serve internal
+	// gRPC traffic.
+	GRPCKeyPath string
+	// GRPCCertificatePath is the filesystem path of the x509 certificate used to
+	// serve internal gRPC traffic.
+	GRPCCertificatePath string
+	// GRPCCAPath is the filesystem path of of the x509 CA certificate used to
+	// verify incoming connections on internal gRPC traffic.
+	GRPCCAPath string
+	// GRPCListenAddress is the address on which the component should server
+	// internal gRPC traffic.
+	GRPCListenAddress string
+
+	// DevCerts, if enabled, automatically generates development CA and component
+	// certificates/keys at DevCertsPath, uses these to serve traffic.
+	DevCerts bool
+	// DevCertsPath sets the prefix in which DevCerts are generated. All components
+	// should have the same path set so that they reuse the CA certificate.
+	DevCertsPath string
+
+	// ComponentName is the name of this component, which should be [a-z0-9+]. It's
+	// used to prefix all flags set by the Configuration.
+	ComponentName string
+}
+
+// RegisterFlags registers the component configuration to be provided by flags.
+// This must be called exactly once before then calling flags.Parse().
+func (c *Configuration) RegisterFlags(componentName string) {
+	flag.StringVar(&c.GRPCKeyPath, componentName+"_grpc_key_path", "", "Path to gRPC server/client key for "+componentName)
+	flag.StringVar(&c.GRPCCertificatePath, componentName+"_grpc_certificate_path", "", "Path to gRPC server/client certificate for "+componentName)
+	flag.StringVar(&c.GRPCCAPath, componentName+"_grpc_ca_certificate_path", "", "Path to gRPC CA certificate for "+componentName)
+	flag.StringVar(&c.GRPCListenAddress, componentName+"_grpc_listen_address", ":4242", "Address to listen at for gRPC connections for "+componentName)
+
+	flag.BoolVar(&c.DevCerts, componentName+"_dev_certs", false, "Use developer certificates (autogenerated) for "+componentName)
+	flag.StringVar(&c.DevCertsPath, componentName+"_dev_certs_path", filepath.Join(xdg.ConfigHome, "monogon-dev-certs"), "Path for storing developer certificates")
+
+	c.ComponentName = componentName
+}
+
+// GRPCServerOptions returns pre-built grpc.ServerOptions that this component
+// should use to serve internal gRPC.
+func (c *Configuration) GRPCServerOptions() []grpc.ServerOption {
+	var certPath, keyPath, caPath string
+	if c.DevCerts {
+		// Use devcerts if requested.
+		certPath, keyPath, caPath = c.GetDevCerts()
+	} else {
+		// Otherwise, use data from flags.
+		if c.GRPCKeyPath == "" {
+			klog.Exitf("-grpc_key_path must be set")
+		}
+		if c.GRPCCertificatePath == "" {
+			klog.Exitf("-grpc_certificate_path must be set")
+		}
+		if c.GRPCCAPath == "" {
+			klog.Exitf("-grpc_ca_certificate_path must be set")
+		}
+		keyPath = c.GRPCKeyPath
+		certPath = c.GRPCCertificatePath
+		caPath = c.GRPCCAPath
+	}
+
+	ca, err := os.ReadFile(caPath)
+	if err != nil {
+		klog.Exitf("Could not read GRPC CA: %v", err)
+	}
+	certPool := x509.NewCertPool()
+	if !certPool.AppendCertsFromPEM(ca) {
+		klog.Exitf("Could not load GRPC CA: %v", err)
+	}
+
+	pair, err := tls.LoadX509KeyPair(certPath, keyPath)
+	if err != nil {
+		klog.Exitf("Could not load GRPC TLS keypair: %v", err)
+	}
+	tlsConf := &tls.Config{
+		Certificates: []tls.Certificate{pair},
+		ClientAuth:   tls.RequireAndVerifyClientCert,
+		RootCAs:      certPool,
+	}
+	return []grpc.ServerOption{
+		grpc.Creds(credentials.NewTLS(tlsConf)),
+	}
+}
diff --git a/cloud/lib/component/devcerts.go b/cloud/lib/component/devcerts.go
new file mode 100644
index 0000000..af1e3d3
--- /dev/null
+++ b/cloud/lib/component/devcerts.go
@@ -0,0 +1,187 @@
+package component
+
+import (
+	"crypto/ed25519"
+	"crypto/rand"
+	"crypto/tls"
+	"crypto/x509"
+	"encoding/pem"
+	"fmt"
+	"math/big"
+	"os"
+	"time"
+
+	"k8s.io/klog/v2"
+
+	"source.monogon.dev/metropolis/pkg/pki"
+)
+
+// GetDevCerts returns paths to this component's development certificate, key
+// and CA, or panics if unavailable.
+func (c *Configuration) GetDevCerts() (certPath, keyPath, caPath string) {
+	klog.Infof("Using developer certificates at %s", c.DevCertsPath)
+
+	caPath = c.ensureDevCA()
+	certPath, keyPath = c.ensureDevComponent()
+	return
+}
+
+// ensureDevComponent ensures that a development certificate/key exists for this
+// component and returns paths to them. This data is either read from disk if it
+// already exists, or is generated when this function is called. If any problem
+// occurs, the code panics.
+func (c *Configuration) ensureDevComponent() (certPath, keyPath string) {
+	caKeyPath := c.DevCertsPath + "/ca.key"
+	caCertPath := c.DevCertsPath + "/ca.cert"
+
+	// Load CA. By convention, we are always called after ensureDevCA.
+	ca, err := tls.LoadX509KeyPair(caCertPath, caKeyPath)
+	if err != nil {
+		klog.Exitf("Could not load Dev CA: %v", err)
+	}
+	caCert, err := x509.ParseCertificate(ca.Certificate[0])
+	if err != nil {
+		klog.Exitf("Could not parse Dev CA: %v", err)
+	}
+
+	// Check if we have keys already.
+	keyPath = c.DevCertsPath + fmt.Sprintf("/%s.key", c.ComponentName)
+	certPath = c.DevCertsPath + fmt.Sprintf("/%s.crt", c.ComponentName)
+	noKey := false
+	if _, err := os.Stat(keyPath); os.IsNotExist(err) {
+		noKey = true
+	}
+	noCert := false
+	if _, err := os.Stat(certPath); os.IsNotExist(err) {
+		noCert = true
+	}
+
+	if noKey || noCert {
+		klog.Infof("Generating developer %s certificate...", c.ComponentName)
+	} else {
+		return
+	}
+
+	// Generate key/certificate.
+	cert := pki.Server([]string{
+		fmt.Sprintf("%s.local", c.ComponentName),
+	}, nil)
+
+	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 127)
+	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
+	if err != nil {
+		klog.Exitf("Failed to generate %s serial number: %v", c.ComponentName, err)
+	}
+	cert.SerialNumber = serialNumber
+	cert.NotBefore = time.Now()
+	cert.NotAfter = pki.UnknownNotAfter
+	cert.BasicConstraintsValid = true
+
+	pub, priv, err := ed25519.GenerateKey(rand.Reader)
+	if err != nil {
+		klog.Exitf("Failed to generate %s key: %v", c.ComponentName, err)
+	}
+	certBytes, err := x509.CreateCertificate(rand.Reader, &cert, caCert, pub, ca.PrivateKey)
+	if err != nil {
+		klog.Exitf("Failed to generate %s certificate: %v", c.ComponentName, err)
+	}
+
+	// And marshal them to disk.
+	privPKCS, err := x509.MarshalPKCS8PrivateKey(priv)
+	if err != nil {
+		klog.Exitf("Failed to marshal %s private key: %v", c.ComponentName, err)
+	}
+	err = os.WriteFile(keyPath, pem.EncodeToMemory(&pem.Block{
+		Type:  "PRIVATE KEY",
+		Bytes: privPKCS,
+	}), 0600)
+	if err != nil {
+		klog.Exitf("Failed to write %s private key: %v", c.ComponentName, err)
+	}
+	err = os.WriteFile(certPath, pem.EncodeToMemory(&pem.Block{
+		Type:  "CERTIFICATE",
+		Bytes: certBytes,
+	}), 0644)
+	if err != nil {
+		klog.Exitf("Failed to write %s certificate: %v", c.ComponentName, err)
+	}
+
+	return
+}
+
+// ensureDevCA ensures that a development CA certificate/key exists and returns
+// paths to them. This data is either read from disk if it already exists, or is
+// generated when this function is called. If any problem occurs, the code
+// panics.
+func (c *Configuration) ensureDevCA() (caCertPath string) {
+	caKeyPath := c.DevCertsPath + "/ca.key"
+	caCertPath = c.DevCertsPath + "/ca.cert"
+
+	if err := os.MkdirAll(c.DevCertsPath, 0700); err != nil {
+		klog.Exitf("Failed to make developer certificate directory: %v", err)
+	}
+
+	// Check if we already have a key/certificate.
+	noKey := false
+	if _, err := os.Stat(caKeyPath); os.IsNotExist(err) {
+		noKey = true
+	}
+	noCert := false
+	if _, err := os.Stat(caCertPath); os.IsNotExist(err) {
+		noCert = true
+	}
+
+	if noKey || noCert {
+		klog.Infof("Generating developer CA certificate...")
+	} else {
+		return
+	}
+	hostname, err := os.Hostname()
+	if err != nil {
+		hostname = "unknown"
+	}
+
+	// No key/certificate, generate them.
+	ca := pki.CA(fmt.Sprintf("monogon dev certs CA (%s)", hostname))
+
+	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 127)
+	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
+	if err != nil {
+		klog.Exitf("Failed to generate CA serial number: %v", err)
+	}
+	ca.SerialNumber = serialNumber
+	ca.NotBefore = time.Now()
+	ca.NotAfter = pki.UnknownNotAfter
+	ca.BasicConstraintsValid = true
+
+	caPub, caPriv, err := ed25519.GenerateKey(rand.Reader)
+	if err != nil {
+		klog.Exitf("Failed to generate CA key: %v", err)
+	}
+	caBytes, err := x509.CreateCertificate(rand.Reader, &ca, &ca, caPub, caPriv)
+	if err != nil {
+		klog.Exitf("Failed to generate CA certificate: %v", err)
+	}
+
+	// And marshal them to disk.
+	caPrivPKCS, err := x509.MarshalPKCS8PrivateKey(caPriv)
+	if err != nil {
+		klog.Exitf("Failed to marshal %s private key: %v", c.ComponentName, err)
+	}
+	err = os.WriteFile(caKeyPath, pem.EncodeToMemory(&pem.Block{
+		Type:  "PRIVATE KEY",
+		Bytes: caPrivPKCS,
+	}), 0600)
+	if err != nil {
+		klog.Exitf("Failed to write CA private key: %v", err)
+	}
+	err = os.WriteFile(caCertPath, pem.EncodeToMemory(&pem.Block{
+		Type:  "CERTIFICATE",
+		Bytes: caBytes,
+	}), 0644)
+	if err != nil {
+		klog.Exitf("Failed to write CA certificate: %v", err)
+	}
+
+	return
+}