m/test/swptm/certtool: init

This implements a minimal GnuTLS certtool replacement. It will be used
by swtpm_setup and friends when generating an emulated TPM certificate.

Change-Id: I7635ccdc50459fec9287ea790488e110c6ce3094
Reviewed-on: https://review.monogon.dev/c/monogon/+/3128
Tested-by: Jenkins CI
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/metropolis/test/swtpm/README.md b/metropolis/test/swtpm/README.md
new file mode 100644
index 0000000..e00892e
--- /dev/null
+++ b/metropolis/test/swtpm/README.md
@@ -0,0 +1,32 @@
+swtpm enhancements
+==================
+
+Metropolis uses [swtpm](https://github.com/stefanberger/swtpm) for emulating a
+TPM device when running tests in qemu, eg. end-to-end-tests.
+
+swtpm consists of a runtime emulator (`swtpm`) which runs against a state
+directory and exposes TPM functionality over the socket; and of tooling
+designed to create said state directory (`swtpm_setup`, `swtpm_localca`, etc).
+
+Getting the former to be built with Bazel is generally trivial, as it mostly
+depends on libraries we are already building (glib, openssl/boringssll, etc).
+However, the tooling is another story: it depends heavily on GnuTLS, both as a
+library to link against and as a runtime tool (`certtool`). We already have one
+C implementation of cryptographic primitives in `//third_party` (boringssl),
+dragging another one in would be shameful.
+
+The tooling is also not a single C binary, but a handful of different ones that
+call eachother based on the requested functionality (presumably as a way to
+implement modularity to allow creating swtpm secrets using a HSM, etc).
+
+This subdirectory contains bits and pieces that allow us to use the
+aforementioned tooling without depending on GnuTLS. This is done by patching
+some tools to rip out GnuTLS support, and by replacing other with native Go
+reimplementations.
+
+certtool
+--------
+
+This is a minimal GnuTLS certtool reimplementation in Go. It's used by `swtpm_localca` to generate TLS certificates. An
+alternative to this would be to rewrite `swtpm_localca` entirely to Go, but that seems like a bit too much effort for
+now.
\ No newline at end of file
diff --git a/metropolis/test/swtpm/certtool/BUILD.bazel b/metropolis/test/swtpm/certtool/BUILD.bazel
new file mode 100644
index 0000000..b017ed8
--- /dev/null
+++ b/metropolis/test/swtpm/certtool/BUILD.bazel
@@ -0,0 +1,15 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+    name = "certtool_lib",
+    srcs = ["main.go"],
+    importpath = "source.monogon.dev/metropolis/test/swtpm/certtool",
+    visibility = ["//visibility:private"],
+    deps = ["@com_github_spf13_pflag//:pflag"],
+)
+
+go_binary(
+    name = "certtool",
+    embed = [":certtool_lib"],
+    visibility = ["//visibility:public"],
+)
diff --git a/metropolis/test/swtpm/certtool/main.go b/metropolis/test/swtpm/certtool/main.go
new file mode 100644
index 0000000..5079c54
--- /dev/null
+++ b/metropolis/test/swtpm/certtool/main.go
@@ -0,0 +1,231 @@
+package main
+
+// Minimal GnuTLS certtool-like tool in Go. Implements only what's needed for
+// compatibility with `swtpm_localca`.
+
+import (
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/x509"
+	"encoding/pem"
+	"log"
+	"math/big"
+	"os"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/spf13/pflag"
+)
+
+var (
+	flagGeneratePrivkey     bool
+	flagGenerateSelfSigned  bool
+	flagGenerateCertificate bool
+
+	flagOutfile           string
+	flagTemplate          string
+	flagLoadPrivkey       string
+	flagLoadCAPrivkey     string
+	flagLoadCACertificate string
+)
+
+func main() {
+	pflag.BoolVar(&flagGeneratePrivkey, "generate-privkey", false, "Generate RSA private kay")
+	pflag.BoolVar(&flagGenerateSelfSigned, "generate-self-signed", false, "Generate self-signed certificate")
+	pflag.BoolVar(&flagGenerateCertificate, "generate-certificate", false, "Sign certificate")
+
+	pflag.StringVar(&flagOutfile, "outfile", "", "Output file for operation")
+	pflag.StringVar(&flagTemplate, "template", "", "Certificate template file (GnuTLS proprietary)")
+	pflag.StringVar(&flagLoadPrivkey, "load-privkey", "", "Path to private key")
+	pflag.StringVar(&flagLoadCAPrivkey, "load-ca-privkey", "", "Path to CA private key")
+	pflag.StringVar(&flagLoadCACertificate, "load-ca-certificate", "", "Path to CA certificate")
+	pflag.Parse()
+
+	modesActive := 0
+	for _, mode := range []bool{flagGeneratePrivkey, flagGenerateSelfSigned, flagGenerateCertificate} {
+		if mode {
+			modesActive++
+		}
+	}
+	if modesActive != 1 {
+		log.Fatalf("Exactly one of --generate-privkey, --generate-self-signed, --generate-certificate must be set")
+	}
+
+	if flagGeneratePrivkey || flagGenerateSelfSigned || flagGenerateCertificate {
+		if flagOutfile == "" {
+			log.Fatalf("--outfile must be set")
+		}
+	}
+	if flagGenerateSelfSigned || flagGenerateCertificate {
+		if flagTemplate == "" {
+			log.Fatalf("--template must be set")
+		}
+		if flagLoadPrivkey == "" {
+			log.Fatalf("--load-privkey must be set")
+		}
+		if flagOutfile == "" {
+			log.Fatalf("--outfile must be set")
+		}
+	}
+	if flagGenerateCertificate {
+		if flagLoadCAPrivkey == "" {
+			log.Fatalf("--load-ca-privkey must be set")
+		}
+		if flagLoadCACertificate == "" {
+			log.Fatalf("--load-ca-certificate must be set")
+		}
+	}
+	switch {
+	case flagGeneratePrivkey:
+		generatePrivkey(flagOutfile)
+	case flagGenerateSelfSigned:
+		generateSelfSigned(flagTemplate, flagLoadPrivkey, flagOutfile)
+	case flagGenerateCertificate:
+		generateCertificate(flagTemplate, flagLoadPrivkey, flagLoadCAPrivkey, flagLoadCACertificate, flagOutfile)
+	}
+}
+
+func generatePrivkey(outfile string) {
+	priv, err := rsa.GenerateKey(rand.Reader, 3072)
+	if err != nil {
+		log.Fatalf("Could not generate RSA key: %v", err)
+	}
+	block := pem.Block{
+		Type:  "RSA PRIVATE KEY",
+		Bytes: x509.MarshalPKCS1PrivateKey(priv),
+	}
+	if err := os.WriteFile(outfile, pem.EncodeToMemory(&block), 0600); err != nil {
+		log.Fatalf("Could not write RSA key: %v", err)
+	}
+}
+
+// certificateFromTemplate parses a GnuTLS 'template' file. This template file is
+// made up of newline-separated stanzas, with an optional 'data' part after a =
+// character.
+//
+// Supported stanzas (on conflict last stanza wins):
+//
+//	cn=data: set subject CN to data
+//	ca: mark certificate as CA
+//	cert_signing_key: enable keyCertSign KeyUsage
+//	expiration_days: number of days in which cert expires (if -1/default, no expiry date)
+func certificateFromTemplate(template string) *x509.Certificate {
+	serial, err := rand.Int(rand.Reader, big.NewInt(1).Lsh(big.NewInt(1), 128))
+	if err != nil {
+		log.Fatalf("Could not generate serial: %v", err)
+	}
+	res := x509.Certificate{
+		SerialNumber:          serial,
+		NotBefore:             time.Now().Add(-time.Minute),
+		BasicConstraintsValid: true,
+		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
+	}
+	for _, line := range strings.Split(template, "\n") {
+		line = strings.TrimSpace(line)
+		if line == "" {
+			continue
+		}
+		parts := strings.SplitN(line, "=", 2)
+		for i := range parts {
+			parts[i] = strings.TrimSpace(parts[i])
+		}
+		switch parts[0] {
+		case "cn":
+			res.Subject.CommonName = parts[1]
+		case "ca":
+			res.IsCA = true
+		case "cert_signing_key":
+			res.KeyUsage |= x509.KeyUsageCertSign
+		case "expiration_days":
+			days, err := strconv.ParseInt(parts[1], 10, 64)
+			if err != nil {
+				log.Fatalf("Invalid expiration_days: %q", err)
+			}
+			if days != -1 {
+				res.NotAfter = time.Now().Add(time.Hour * 24 * time.Duration(days))
+			} else {
+				res.NotAfter = time.Unix(253402300799, 0)
+			}
+		default:
+			log.Fatalf("Unhandled template line %q", line)
+		}
+	}
+	return &res
+}
+
+func readPrivkey(path string) *rsa.PrivateKey {
+	bytes, err := os.ReadFile(path)
+	if err != nil {
+		log.Fatalf("Could not read private key: %v", err)
+	}
+	block, _ := pem.Decode(bytes)
+	if block.Type != "RSA PRIVATE KEY" {
+		log.Fatalf("Private key contains invalid PEM data")
+	}
+	res, err := x509.ParsePKCS1PrivateKey(block.Bytes)
+	if err != nil {
+		log.Fatalf("Could not parse private key: %v", err)
+	}
+	return res
+}
+
+func readCertificate(path string) *x509.Certificate {
+	bytes, err := os.ReadFile(path)
+	if err != nil {
+		log.Fatalf("Could not read certificate: %v", err)
+	}
+	block, _ := pem.Decode(bytes)
+	if block.Type != "CERTIFICATE" {
+		log.Fatalf("Certificate contains invalid PEM data")
+	}
+	res, err := x509.ParseCertificate(block.Bytes)
+	if err != nil {
+		log.Fatalf("Could not parse certificate: %v", err)
+	}
+	return res
+}
+
+func generateSelfSigned(templatePath, privkeyPath, outfile string) {
+	template, err := os.ReadFile(templatePath)
+	if err != nil {
+		log.Fatalf("Could not read template: %v", err)
+	}
+	priv := readPrivkey(privkeyPath)
+	cert := certificateFromTemplate(string(template))
+
+	derBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, priv.Public(), priv)
+	if err != nil {
+		log.Fatalf("Could not generate self-signed certificate: %v", err)
+	}
+	block := pem.Block{
+		Type:  "CERTIFICATE",
+		Bytes: derBytes,
+	}
+	if err := os.WriteFile(outfile, pem.EncodeToMemory(&block), 0600); err != nil {
+		log.Fatalf("Could not write self-signed certificate: %v", err)
+	}
+}
+
+func generateCertificate(templatePath, privkeyPath, caPrivkeyPath, caCertificatePath, outfile string) {
+	template, err := os.ReadFile(templatePath)
+	if err != nil {
+		log.Fatalf("Could not read template: %v", err)
+	}
+	priv := readPrivkey(privkeyPath)
+	caPriv := readPrivkey(caPrivkeyPath)
+	cert := certificateFromTemplate(string(template))
+	ca := readCertificate(caCertificatePath)
+
+	derBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, priv.Public(), caPriv)
+	if err != nil {
+		log.Fatalf("Could not generate certificate: %v", err)
+	}
+	block := pem.Block{
+		Type:  "CERTIFICATE",
+		Bytes: derBytes,
+	}
+	if err := os.WriteFile(outfile, pem.EncodeToMemory(&block), 0600); err != nil {
+		log.Fatalf("Could not write certificate: %v", err)
+	}
+}