m/test/swptm/swtpm_cert: init

This is a Go reimplementaiton of swtpm_cert from upstream swtpm.

Change-Id: I5738709fbe9512cfb3c853622f0ff6655506e9a9
Reviewed-on: https://review.monogon.dev/c/monogon/+/3129
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
Tested-by: Jenkins CI
diff --git a/metropolis/test/swtpm/README.md b/metropolis/test/swtpm/README.md
index e00892e..6aab10f 100644
--- a/metropolis/test/swtpm/README.md
+++ b/metropolis/test/swtpm/README.md
@@ -24,6 +24,14 @@
 some tools to rip out GnuTLS support, and by replacing other with native Go
 reimplementations.
 
+swtpm_cert
+----------
+
+This is a reimplementation of swtpm_cert in Go. The upstream swtpm_cert is implemented in C and has a hard dependency on
+GnuTLS and libtasn1. Rewriting it in Go and using plain stdlib functions seems like the correct solution here (the
+alternative being either bringing in GnuTLS/libtasn1 into `third_party`, or rewriting swtpm_cert to use
+OpenSSL/BoringSSL).
+
 certtool
 --------
 
diff --git a/metropolis/test/swtpm/swtpm_cert/BUILD.bazel b/metropolis/test/swtpm/swtpm_cert/BUILD.bazel
new file mode 100644
index 0000000..dce12e6
--- /dev/null
+++ b/metropolis/test/swtpm/swtpm_cert/BUILD.bazel
@@ -0,0 +1,21 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+    name = "swtpm_cert_lib",
+    srcs = [
+        "asn1.go",
+        "main.go",
+    ],
+    importpath = "source.monogon.dev/metropolis/test/swtpm/swtpm_cert",
+    visibility = ["//visibility:private"],
+    deps = [
+        "//metropolis/pkg/pki",
+        "@com_github_spf13_pflag//:pflag",
+    ],
+)
+
+go_binary(
+    name = "swtpm_cert",
+    embed = [":swtpm_cert_lib"],
+    visibility = ["//visibility:public"],
+)
diff --git a/metropolis/test/swtpm/swtpm_cert/asn1.go b/metropolis/test/swtpm/swtpm_cert/asn1.go
new file mode 100644
index 0000000..4136a25
--- /dev/null
+++ b/metropolis/test/swtpm/swtpm_cert/asn1.go
@@ -0,0 +1,129 @@
+package main
+
+import (
+	"encoding/asn1"
+	"log"
+)
+
+type manufacturerInfo struct {
+	Manufacturer struct {
+		Sequence struct {
+			OID  asn1.ObjectIdentifier
+			Data string `asn1:"utf8"`
+		}
+	} `asn1:"set"`
+	Model struct {
+		Sequence struct {
+			OID  asn1.ObjectIdentifier
+			Data string `asn1:"utf8"`
+		}
+	} `asn1:"set"`
+	Version struct {
+		Sequence struct {
+			OID  asn1.ObjectIdentifier
+			Data string `asn1:"utf8"`
+		}
+	} `asn1:"set"`
+}
+
+// buildManufacturerInfo marshals TPM manufacturer info (TPMManufacturer
+// structure from TCG EK Credential Profile For TPM Family 2.0; Level 0; Version
+// 2.4; Revision 3; 16 July 2021).
+//
+// This is embedded as a directoryName GeneralName SubjectAltName in the
+// generated X509 certificate for an EK.
+func buildManufacturerInfo(manufacturer, model, version string) []byte {
+	var v manufacturerInfo
+	v.Manufacturer.Sequence.OID = asn1.ObjectIdentifier{2, 23, 133, 2, 1}
+	v.Manufacturer.Sequence.Data = manufacturer
+	v.Model.Sequence.OID = asn1.ObjectIdentifier{2, 23, 133, 2, 2}
+	v.Model.Sequence.Data = model
+	v.Version.Sequence.OID = asn1.ObjectIdentifier{2, 23, 133, 2, 3}
+	v.Version.Sequence.Data = version
+
+	res, err := asn1.Marshal(v)
+	if err != nil {
+		log.Fatalf("Failed to marshal manufacturer info: %v", err)
+	}
+	return res
+}
+
+type platformManufacturerInfo struct {
+	Manufacturer struct {
+		Sequence struct {
+			OID  asn1.ObjectIdentifier
+			Data string `asn1:"utf8"`
+		}
+	} `asn1:"set"`
+	Model struct {
+		Sequence struct {
+			OID  asn1.ObjectIdentifier
+			Data string `asn1:"utf8"`
+		}
+	} `asn1:"set"`
+	Version struct {
+		Sequence struct {
+			OID  asn1.ObjectIdentifier
+			Data string `asn1:"utf8"`
+		}
+	} `asn1:"set"`
+}
+
+// buildPlatformManufacturerInfo marshals TPM platform manufacturer info.
+//
+// See: TCG Platform Certificate Profile; Specification Version 1.1; Revision 19;
+// 10 April 2020: Section 3.1.2 (Name Attributes
+// Platform{ManufacturerStr,Model,Version}) and Section 3.2 (Platform
+// Certificate, Extensions Subject Alternative Names).
+//
+// This is embedded as a directoryName GeneralName SubjectAltName in the
+// generated X509 certificate for a Platform.
+//
+// The spec seems to have missing ASN.1 definitions to tie together the strings
+// into a structure that's embedded into the SAN. This corresponds to whatever
+// upstream swtpm_cert is doing.
+func buildPlatformManufacturerInfo(manufacturer, model, version string) []byte {
+	var v platformManufacturerInfo
+	v.Manufacturer.Sequence.OID = asn1.ObjectIdentifier{2, 23, 133, 5, 1, 1}
+	v.Manufacturer.Sequence.Data = manufacturer
+	v.Model.Sequence.OID = asn1.ObjectIdentifier{2, 23, 133, 5, 1, 4}
+	v.Model.Sequence.Data = model
+	v.Version.Sequence.OID = asn1.ObjectIdentifier{2, 23, 133, 5, 1, 5}
+	v.Version.Sequence.Data = version
+
+	res, err := asn1.Marshal(v)
+	if err != nil {
+		log.Fatalf("Failed to marshal platform manufacturer info: %v", err)
+	}
+	return res
+}
+
+type specificationInfo struct {
+	OID asn1.ObjectIdentifier
+	Set struct {
+		Sequence struct {
+			Family   string
+			Level    int
+			Revision int
+		}
+	} `asn1:"set"`
+}
+
+// buildSpecificationInfo marshals TPM manufacturer info (tPMSpecification
+// structure from TCG EK Credential Profile For TPM Family 2.0; Level 0; Version
+// 2.4; Revision 3; 16 July 2021).
+//
+// This is embedded as a directoryName SAN or extension in the generated X509
+// certificate for an EK.
+func buildSpecificationInfo(family string, level, revision int) []byte {
+	var v specificationInfo
+	v.OID = asn1.ObjectIdentifier{2, 23, 133, 2, 16}
+	v.Set.Sequence.Family = family
+	v.Set.Sequence.Level = level
+	v.Set.Sequence.Revision = revision
+	res, err := asn1.Marshal(v)
+	if err != nil {
+		log.Fatalf("Failed to marshal specification info: %v", err)
+	}
+	return res
+}
diff --git a/metropolis/test/swtpm/swtpm_cert/main.go b/metropolis/test/swtpm/swtpm_cert/main.go
new file mode 100644
index 0000000..7b14f3a
--- /dev/null
+++ b/metropolis/test/swtpm/swtpm_cert/main.go
@@ -0,0 +1,299 @@
+package main
+
+// swtpm_cert (from swtpm project) reimplemented in Go.
+//
+// This tool generates a TPM EK or Platform certificate. These certificates have
+// to be in a very specific format and include non-standard extensions and
+// subjectAltNames.
+
+import (
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/asn1"
+	"encoding/hex"
+	"encoding/pem"
+	"log"
+	"math/big"
+	"os"
+	"strings"
+	"time"
+
+	"github.com/spf13/pflag"
+
+	"source.monogon.dev/metropolis/pkg/pki"
+)
+
+func getSignkey() *rsa.PrivateKey {
+	if strings.HasPrefix(flagSignKey, "tpmkey:") || strings.HasPrefix(flagSignKey, "pkcs11:") {
+		log.Fatalf("Loading tpmkey: and pkcs11: sign keys unimplemented")
+	}
+	bytes, err := os.ReadFile(flagSignKey)
+	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 getPubkey() any {
+	if flagModulus != "" {
+		if flagECCX != "" || flagECCY != "" || flagECCCurveID != "" {
+			log.Fatalf("--modulus and --ecc* cannot be set simultaneously")
+		}
+		var modulus big.Int
+		modulusBytes, err := hex.DecodeString(flagModulus)
+		if err != nil {
+			log.Fatalf("Could not decode modulus: %v", err)
+		}
+		modulus.SetBytes(modulusBytes)
+		return &rsa.PublicKey{
+			N: &modulus,
+			E: flagExponent,
+		}
+	}
+	if flagECCX != "" && flagECCY != "" && flagECCCurveID != "" {
+		if flagModulus != "" {
+			log.Fatalf("--modulus and --ecc* cannot be set simultaneously")
+		}
+		var x, y big.Int
+		xBytes, err := hex.DecodeString(flagECCX)
+		if err != nil {
+			log.Fatalf("Could not decode ECC X: %v", err)
+		}
+		x.SetBytes(xBytes)
+		yBytes, err := hex.DecodeString(flagECCY)
+		if err != nil {
+			log.Fatalf("Could not decode ECC Y: %v", err)
+		}
+		y.SetBytes(yBytes)
+		res := ecdsa.PublicKey{X: &x, Y: &y}
+		switch flagECCCurveID {
+		case "secp256r1":
+			res.Curve = elliptic.P256()
+		case "secp384r1":
+			res.Curve = elliptic.P384()
+		default:
+			log.Fatalf("Unknown ECC curve ID %q", flagECCCurveID)
+		}
+		return &res
+	}
+	log.Fatalf("--modulus or --ecc* must be set")
+	panic("unreachable")
+}
+
+func getIssuerCert() *x509.Certificate {
+	bytes, err := os.ReadFile(flagIssuerCert)
+	if err != nil {
+		log.Fatalf("Could not read issuer certificate: %v", err)
+	}
+	block, _ := pem.Decode(bytes)
+	if block.Type != "CERTIFICATE" {
+		log.Fatalf("Issuer certificate contains invalid PEM data")
+	}
+	res, err := x509.ParseCertificate(block.Bytes)
+	if err != nil {
+		log.Fatalf("Could not parse issuer certificate: %v", err)
+	}
+	return res
+}
+
+type certType string
+
+const (
+	certTypeEK       certType = "ek"
+	certTypePlatform certType = "platform"
+)
+
+var (
+	flagType                 string
+	flagSubject              string
+	flagPlatformManufacturer string
+	flagPlatformVersion      string
+	flagPlatformModel        string
+	flagTPM2                 bool
+	flagTPMSpecFamily        string
+	flagTPMSpecLevel         int
+	flagTPMSpecRevision      int
+	flagTPMManufacturer      string
+	flagTPMModel             string
+	flagTPMVersion           string
+	flagOutCert              string
+	flagExponent             int
+	flagSignKey              string
+	flagIssuerCert           string
+	flagDays                 int
+	flagSerial               string
+
+	flagModulus string
+
+	flagECCX       string
+	flagECCY       string
+	flagECCCurveID string
+)
+
+func main() {
+	pflag.BoolVar(&flagTPM2, "tpm2", false, "Enable TPM2 mode (no-op, only mode supported)")
+	pflag.StringVar(&flagType, "type", "ek", "Type of certificate to create, ek or platform")
+
+	pflag.StringVar(&flagPlatformManufacturer, "platform-manufacturer", "", "TPM platform manufacturer")
+	pflag.StringVar(&flagPlatformVersion, "platform-version", "", "TPM platform version")
+	pflag.StringVar(&flagPlatformModel, "platform-model", "", "TPM platform model")
+
+	pflag.StringVar(&flagTPMSpecFamily, "tpm-spec-family", "", "TPM Specification family")
+	pflag.IntVar(&flagTPMSpecLevel, "tpm-spec-level", -1, "TPM Specification level")
+	pflag.IntVar(&flagTPMSpecRevision, "tpm-spec-revision", -1, "TPM Specification revision")
+
+	pflag.StringVar(&flagTPMManufacturer, "tpm-manufacturer", "", "TPM device manufacturer")
+	pflag.StringVar(&flagTPMModel, "tpm-model", "", "TPM device model")
+	pflag.StringVar(&flagTPMVersion, "tpm-version", "", "TPM device version")
+
+	pflag.StringVar(&flagSubject, "subject", "", "Certificate subject (only cn=... is implemented)")
+	pflag.IntVar(&flagDays, "days", 0, "")
+	pflag.StringVar(&flagSerial, "serial", "", "")
+
+	pflag.StringVar(&flagOutCert, "out-cert", "", "Path to generated certificate (.pem)")
+	pflag.StringVar(&flagIssuerCert, "issuercert", "", "Path to issuer certificate (.pem)")
+	pflag.StringVar(&flagSignKey, "signkey", "", "Path to private key used to sign certificate")
+
+	pflag.IntVar(&flagExponent, "exponent", 0x10001, "RSA key exponent")
+	pflag.StringVar(&flagModulus, "modulus", "", "RSA key modulus")
+
+	pflag.StringVar(&flagECCX, "ecc-x", "", "ECC key x component")
+	pflag.StringVar(&flagECCY, "ecc-y", "", "ECC key y component")
+	pflag.StringVar(&flagECCCurveID, "ecc-curveid", "", "ECC curve id (one of secp256r1, secp384r1)")
+	pflag.Parse()
+
+	var ty certType
+	switch flagType {
+	case "ek":
+		ty = certTypeEK
+	case "platform":
+		ty = certTypePlatform
+	default:
+		log.Fatalf("Unknown type %q (must be ek or platform)", flagType)
+	}
+	if ty == certTypeEK || ty == certTypePlatform {
+		if flagTPMManufacturer == "" {
+			log.Fatalf("--tpm-manufacturer must be set")
+		}
+		if flagTPMModel == "" {
+			log.Fatalf("--tpm-model must be set")
+		}
+		if flagTPMVersion == "" {
+			log.Fatalf("--tpm-version must be set")
+		}
+	}
+	if ty == certTypeEK {
+		if flagTPMSpecFamily == "" {
+			log.Fatalf("--tpm-spec-family must be set")
+		}
+		if flagTPMSpecLevel == -1 {
+			log.Fatalf("--tpm-spec-level must be set")
+		}
+		if flagTPMSpecRevision == -1 {
+			log.Fatalf("--tpm-spec-revision must be set")
+		}
+	}
+	if ty == certTypePlatform {
+		if flagPlatformManufacturer == "" {
+			log.Fatalf("--platform-manufacturer must be set")
+		}
+		if flagPlatformModel == "" {
+			log.Fatalf("--platform-model must be set")
+		}
+		if flagPlatformVersion == "" {
+			log.Fatalf("--platform-version must be set")
+		}
+	}
+
+	pubkey := getPubkey()
+	signkey := getSignkey()
+	issuercert := getIssuerCert()
+
+	var cert x509.Certificate
+	cert.Version = 3
+	cert.SerialNumber = big.NewInt(0)
+	if _, ok := cert.SerialNumber.SetString(flagSerial, 10); !ok {
+		log.Fatalf("Could not parse serial %q", flagSerial)
+	}
+	cert.NotBefore = time.Now()
+	if flagDays > 0 {
+		cert.NotAfter = time.Now().Add(time.Hour * 24 * time.Duration(flagDays))
+	} else {
+		cert.NotAfter = pki.UnknownNotAfter
+	}
+	if flagSubject != "" {
+		parts := strings.Split(flagSubject, ",")
+		for _, part := range parts {
+			part = strings.TrimSpace(part)
+			els := strings.SplitN(part, "=", 2)
+			k := strings.ToLower(els[0])
+			switch k {
+			case "cn":
+				cert.Subject.CommonName = els[1]
+			default:
+				log.Fatalf("Unparseable subject: %q", flagSubject)
+			}
+		}
+	}
+	var sanValues = []asn1.RawValue{}
+	switch ty {
+	case certTypeEK:
+		sanValues = append(sanValues, asn1.RawValue{
+			Tag: 4, Class: 2, Bytes: buildManufacturerInfo(flagTPMManufacturer, flagTPMModel, flagTPMVersion),
+		})
+	case certTypePlatform:
+		sanValues = append(sanValues, asn1.RawValue{
+			Tag: 4, Class: 2, Bytes: buildPlatformManufacturerInfo(flagPlatformManufacturer, flagPlatformModel, flagPlatformVersion),
+		})
+	}
+	sanBytes, err := asn1.Marshal(sanValues)
+	if err != nil {
+		log.Fatalf("Failed to marshal SAN values: %v", err)
+	}
+	cert.ExtraExtensions = []pkix.Extension{
+		{
+			Id:    asn1.ObjectIdentifier{2, 5, 29, 17}, // subjectAltName
+			Value: sanBytes,
+		},
+	}
+	if ty == certTypeEK {
+		cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{
+			Id:    asn1.ObjectIdentifier{2, 5, 29, 9}, // directoryName
+			Value: buildSpecificationInfo(flagTPMSpecFamily, flagTPMSpecLevel, flagTPMSpecRevision),
+		})
+	}
+	cert.BasicConstraintsValid = true
+	cert.IsCA = false
+	switch ty {
+	case certTypeEK:
+		// tcg-kp-EKCertificate
+		cert.UnknownExtKeyUsage = []asn1.ObjectIdentifier{{2, 23, 133, 8, 1}}
+	case certTypePlatform:
+		// tcg-kp-PlatformAttributeCertificate
+		cert.UnknownExtKeyUsage = []asn1.ObjectIdentifier{{2, 23, 133, 8, 2}}
+	}
+
+	derBytes, err := x509.CreateCertificate(rand.Reader, &cert, issuercert, pubkey, signkey)
+	if err != nil {
+		log.Fatalf("Generating certificate failed: %v", err)
+	}
+	block := pem.Block{
+		Type:  "CERTIFICATE",
+		Bytes: derBytes,
+	}
+	if err := os.WriteFile(flagOutCert, pem.EncodeToMemory(&block), 0644); err != nil {
+		log.Fatalf("Writing certificate failed: %v", err)
+	}
+}