Go 1.27 shipped crypto/mldsa, which is a standard-library implementation of ML-DSA, the lattice-based signature scheme standardized in FIPS 204 . Paired with the ML-KEM support that landed in Go 1.24, the standard library now covers both halves of the post-quantum story: key establishment and authentication.

I’ve been treating this as a personal R&D track, and I’m working through what a PQC migration actually looks like at the code level, not just the whitepaper level. So, this is a small but concrete milestone: generating a self-signed X.509 certificate signed with ML-DSA, using nothing but the standard library.

What this does

The code below generates an ML-DSA-65 key pair (NIST Security Level 3, roughly comparable to 192-bit classical security), builds an X.509 certificate template, self-signs it, and writes out the certificate, private key, and public key as PEM files. It then verifies the self-signature to confirm the whole chain actually works.

// Package main demonstrates generating a self-signed X.509 certificate using
// the ML-DSA (Module-Lattice Digital Signature Algorithm) post-quantum
// signature scheme specified in FIPS 204.
//
// Requirements: Go 1.27+ (crypto/mldsa standard library package).
package main

import (
	"crypto/mldsa"
	"crypto/rand"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/hex"
	"encoding/pem"
	"fmt"
	"log"
	"math/big"
	"net"
	"os"
	"time"
)

// GenerateQuantumSafeCertificate generates a self-signed X.509 certificate
// using ML-DSA-65 (FIPS 204).
//
// ML-DSA-65 provides NIST Security Level 3 (192-bit classical security)
// and is resistant to attacks by both classical and quantum computers.
//
// Returns the parsed certificate and the ML-DSA private key, or an error.
func GenerateQuantumSafeCertificate() (*x509.Certificate, *mldsa.PrivateKey, error) {
	fmt.Println("=== Generating ML-DSA-65 Quantum-Safe Certificate ===")
	fmt.Println()

	// Step 1: Generate ML-DSA-65 key pair
	fmt.Println("[1] Generating ML-DSA-65 key pair (FIPS 204, NIST Level 3)...")
	privateKey, err := mldsa.GenerateKey(mldsa.MLDSA65())
	if err != nil {
		return nil, nil, fmt.Errorf("failed to generate ML-DSA-65 key: %w", err)
	}
	fmt.Println("Key pair generated successfully.")
	fmt.Println()

	// Step 2: Build certificate template
	fmt.Println("[2] Building X.509 certificate template...")
	domain := "Example"
	template := &x509.Certificate{
		SerialNumber: big.NewInt(1),
		Subject: pkix.Name{
			CommonName:         "Example-PQC",
			OrganizationalUnit: []string{"Engineering"},
			Organization:       []string{"TestOrg"},
			Country:            []string{"KN"},
			Province:           []string{"Kandor"},
			Locality:           []string{"Krypton"},
		},
		DNSNames:    []string{domain, "localhost"},
		IPAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")},
		NotBefore:   time.Now(),
		NotAfter:    time.Now().Add(3 * 365 * 24 * time.Hour),
		KeyUsage:    x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
		BasicConstraintsValid: true,
		IsCA:                  false,
	}
	fmt.Printf("Subject:   %s\n", template.Subject.CommonName)
	fmt.Printf("NotBefore: %s\n", template.NotBefore.Format(time.RFC3339))
	fmt.Printf("NotAfter:  %s\n", template.NotAfter.Format(time.RFC3339))
	fmt.Println()

	// Step 3: Create self-signed certificate using ML-DSA-65
	fmt.Println("[3] Creating self-signed certificate with ML-DSA-65...")
	certDER, err := x509.CreateCertificate(rand.Reader, template, template, privateKey.PublicKey(), privateKey)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to create certificate: %w", err)
	}
	fmt.Println("Certificate created successfully (DER encoded).")
	fmt.Println()

	// Step 4: Parse the certificate back to verify it
	fmt.Println("[4] Parsing certificate back from DER...")
	cert, err := x509.ParseCertificate(certDER)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to parse certificate: %w", err)
	}
	fmt.Println("Certificate parsed successfully.")
	fmt.Println()

	return cert, privateKey, nil
}

func main() {
	cert, privateKey, err := GenerateQuantumSafeCertificate()
	if err != nil {
		log.Fatalf("FATAL: %v", err)
	}

	// Print certificate details
	fmt.Println("============================================================")
	fmt.Println("              QUANTUM-SAFE CERTIFICATE DETAILS              ")
	fmt.Println("============================================================")
	fmt.Println()

	fmt.Printf("Version:            %d (X.509 v%d)\n", cert.Version, cert.Version)
	fmt.Printf("Serial Number:      %s\n", cert.SerialNumber.String())
	fmt.Printf("Signature Algo:     %s\n", cert.SignatureAlgorithm)
	fmt.Printf("Public Key Algo:    %s\n", cert.PublicKeyAlgorithm)
	fmt.Printf("Issuer:             %s\n", cert.Issuer.String())
	fmt.Printf("Subject:            %s\n", cert.Subject.String())
	fmt.Printf("Not Before:         %s\n", cert.NotBefore.Format(time.RFC3339))
	fmt.Printf("Not After:          %s\n", cert.NotAfter.Format(time.RFC3339))
	fmt.Printf("Is CA:              %t\n", cert.IsCA)
	fmt.Printf("DNS Names:          %v\n", cert.DNSNames)
	fmt.Printf("IP Addresses:       %v\n", cert.IPAddresses)
	fmt.Println()

	// Print public key info
	fmt.Println("============================================================")
	fmt.Println("                   ML-DSA-65 PUBLIC KEY                     ")
	fmt.Println("============================================================")
	fmt.Println()
	pubKey := privateKey.PublicKey()
	pubKeyBytes := pubKey.Bytes()
	fmt.Printf("Algorithm:          ML-DSA-65 (FIPS 204)\n")
	fmt.Printf("Security Level:     NIST Level 3 (192-bit)\n")
	fmt.Printf("Public Key Size:    %d bytes\n", len(pubKeyBytes))
	fmt.Printf("Public Key (hex, first 64 bytes):\n")
	fmt.Printf("    %s...\n", hex.EncodeToString(pubKeyBytes[:64]))
	fmt.Println()

	// Print private key seed info
	fmt.Println("============================================================")
	fmt.Println("                  ML-DSA-65 PRIVATE KEY SEED                ")
	fmt.Println("============================================================")
	fmt.Println()
	privKeyBytes := privateKey.Bytes()
	fmt.Printf("Seed Size:          %d bytes\n", len(privKeyBytes))
	fmt.Printf("Seed (hex):         %s\n", hex.EncodeToString(privKeyBytes))
	fmt.Println()

	// Print signature info from the certificate
	fmt.Println("============================================================")
	fmt.Println("                 CERTIFICATE SIGNATURE                      ")
	fmt.Println("============================================================")
	fmt.Println()
	fmt.Printf("Signature Algo:     %s\n", cert.SignatureAlgorithm)
	fmt.Printf("Signature Size:     %d bytes\n", len(cert.Signature))
	fmt.Printf("Signature (hex, first 64 bytes):\n")
	if len(cert.Signature) >= 64 {
		fmt.Printf("    %s...\n", hex.EncodeToString(cert.Signature[:64]))
	}
	fmt.Println()

	// Marshal and print the private key in PKCS#8 PEM format
	fmt.Println("============================================================")
	fmt.Println("            PRIVATE KEY (PKCS#8 PEM FORMAT)                 ")
	fmt.Println("============================================================")
	fmt.Println()
	pkcs8DER, err := x509.MarshalPKCS8PrivateKey(privateKey)
	if err != nil {
		log.Fatalf("Failed to marshal private key to PKCS#8: %v", err)
	}
	privPEM := pem.EncodeToMemory(&pem.Block{
		Type:  "PRIVATE KEY",
		Bytes: pkcs8DER,
	})
	fmt.Printf("%s\n", privPEM)

	// Marshal and print the public key in PKIX PEM format
	fmt.Println("============================================================")
	fmt.Println("            PUBLIC KEY (PKIX PEM FORMAT)                    ")
	fmt.Println("============================================================")
	fmt.Println()
	pkixDER, err := x509.MarshalPKIXPublicKey(pubKey)
	if err != nil {
		log.Fatalf("Failed to marshal public key to PKIX: %v", err)
	}
	pubPEM := pem.EncodeToMemory(&pem.Block{
		Type:  "PUBLIC KEY",
		Bytes: pkixDER,
	})
	fmt.Printf("%s\n", pubPEM)

	// Print the certificate in PEM format
	fmt.Println("============================================================")
	fmt.Println("         QUANTUM-SAFE CERTIFICATE (PEM FORMAT)             ")
	fmt.Println("============================================================")
	fmt.Println()
	certPEM := pem.EncodeToMemory(&pem.Block{
		Type:  "CERTIFICATE",
		Bytes: cert.Raw,
	})
	fmt.Printf("%s\n", certPEM)

	// Verify the certificate signature against itself (self-signed)
	fmt.Println("============================================================")
	fmt.Println("               SELF-SIGNATURE VERIFICATION                 ")
	fmt.Println("============================================================")
	fmt.Println()
	err = cert.CheckSignatureFrom(cert)
	if err != nil {
		fmt.Printf("  Verification FAILED: %v\n", err)
	} else {
		fmt.Println("  Verification PASSED: Self-signed certificate signature is valid.")
	}
	fmt.Println()

	// Write certificate and keys to files for inspection
	fmt.Println("============================================================")
	fmt.Println("                  WRITING FILES TO DISK                     ")
	fmt.Println("============================================================")
	fmt.Println()

	if err := os.WriteFile("certificate.pem", certPEM, 0644); err != nil {
		log.Fatalf("Failed to write certificate: %v", err)
	}
	fmt.Println("  Written: certificate.pem")

	if err := os.WriteFile("private_key.pem", privPEM, 0600); err != nil {
		log.Fatalf("Failed to write private key: %v", err)
	}
	fmt.Println("  Written: private_key.pem")

	if err := os.WriteFile("public_key.pem", pubPEM, 0644); err != nil {
		log.Fatalf("Failed to write public key: %v", err)
	}
	fmt.Println("  Written: public_key.pem")
}

What’s actually happening

  • mldsa.GenerateKey(mldsa.MLDSA65()) generates a key pair for the ML-DSA-65 parameter set.
  • x509.CreateCertificate doesn’t care that the key is ML-DSA instead of RSA/ECDSA. As of Go 1.27, crypto/x509 recognizes ML-DSA public keys and signs accordingly. That’s the whole point of the standard-library integration: no adapter code, no third-party crate.
  • cert.CheckSignatureFrom(cert) verifies the self-signature, confirming the certificate is internally consistent, and it’s the same check a TLS stack would run on the chain.

Setup

Requires Go 1.27 or later. The code above has been tested against go1.27.0.linux-amd64.tar.gz . crypto/mldsa is brand new in this release, so expect the surrounding tooling (linters, static analysis, third-party libraries that shell out to openssl) to lag behind for a while.

This is early, as Go 1.27 only shipped a few weeks ago, but that’s exactly the point of working through it now: PQC in Go is moving from “bring your own implementation” to “standard library primitive,” and getting hands-on with it while it’s still new is worth more than reading about it after the fact, but that’s just my humble opinion.