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.CreateCertificatedoesn’t care that the key is ML-DSA instead of RSA/ECDSA. As of Go 1.27,crypto/x509recognizes 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.