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)
+ }
+}