Chapter 05: Dilithium (ML-DSA) Digital Signatures
While Kyber is for establishing secret keys, Dilithium (standardized as ML-DSA - Module-Lattice-Based Digital Signature Algorithm) is the NIST standard for digital signatures, ensuring data authenticity and non-repudiation in a post-quantum world.
1. The Concept (ELI5)
Imagine you want to sign a check, and you want to prove to the bank that you signed it, without showing anyone your actual signature technique. In classical cryptography (like ECDSA), you use mathematical curves. A quantum computer can reverse engineer the curve to forge your signature. Dilithium uses a technique called "Fiat-Shamir with Aborts." Think of it as a complex dance. To sign the check, you perform a random, intricate 500-step dance (lattice math). If the dance gets too close to revealing your secret signature style, you immediately stop ("Abort") and start a slightly different dance from scratch. When you finally finish a safe dance, you record the steps. The bank can easily verify the steps match your public profile, but a quantum computer can't calculate a fake set of steps because of the deliberate randomness and "aborts."
2. The Visual
3. The Code
How to securely sign and verify data using ML-DSA.
Go
Vulnerable Code ❌ (Classical ECDSA)
package main
import (
"crypto/ecdsa"
"crypto/rand"
"crypto/sha256"
)
// Quantum computers can derive the private key from the public key and signature
func signData(priv *ecdsa.PrivateKey, msg []byte) {
hash := sha256.Sum256(msg)
r, s, _ := ecdsa.Sign(rand.Reader, priv, hash[:])
_ = r
_ = s
}
Production-Ready Secure Code ✅ (Dilithium / ML-DSA)
package main
import (
"fmt"
"github.com/cloudflare/circl/sign/dilithium/mode3"
)
func main() {
// Generate Dilithium Keypair
pk, sk, _ := mode3.GenerateKey(nil)
msg := []byte("AppSec Atlas PQC Masterclass")
// Sign Message
signature := mode3.Sign(sk, msg)
// Verify Message
isValid := mode3.Verify(pk, msg, signature)
fmt.Printf("Signature Valid: %t\n", isValid)
}
Python
Vulnerable Code ❌
from cryptography.hazmat.primitives.asymmetric import ec
# Classical ECC signing
Production-Ready Secure Code ✅
import oqs
# Use Dilithium3 (NIST Security Level 3)
sig = oqs.Signature("Dilithium3")
public_key = sig.generate_keypair()
message = b"Strictly confidential data"
# Sign
signature = sig.sign(message)
# Verify
is_valid = sig.verify(message, signature, public_key)
print("Verified:", is_valid)
TypeScript / Node.js
Vulnerable Code ❌
import { sign } from 'crypto';
// Classic RSA/ECC sign
Production-Ready Secure Code ✅
import { Signature } from 'node-oqs';
const signer = new Signature('Dilithium3');
const pubKey = signer.generateKeypair();
const msg = Buffer.from("Secure Payload");
const signature = signer.sign(msg);
const isValid = signer.verify(msg, signature, pubKey);
console.log("PQC Signature valid:", isValid);
4. The Guardrail
Rego Rule (OPA): If you are defining JWT signing algorithms in your API Gateway or Kubernetes configurations, strictly deny the use of 'none' and eventually enforce transitioning to PQC-enabled algorithms or hybrid setups.
package api.jwt_validation
deny[msg] {
input.jwt_config.algorithm == "none"
msg := "JWT algorithm 'none' is completely insecure."
}
warn[msg] {
# Transition warning for classical algos
classical_algs := {"RS256", "ES256"}
classical_algs[input.jwt_config.algorithm]
msg := "Consider preparing infrastructure for PQC signature algorithms (e.g., ML-DSA) in future iterations."
}