package main import ( "crypto/mldsa" "fmt" "log") func signAndVerify() { key, err := mldsa.GenerateKey(mldsa.MLDSA65()) if err != nil { log.Fatal(err) } message := []byte("hello, post-quantum world") // Sign is randomized (hedged); the reader argument is ignored, opts may carry a context signature, err := key.Sign(nil, message, nil) if err != nil { log.Fatal(err) } pub := key.PublicKey() fmt.Println("public key", len(pub.Bytes()), "bytes, signature", len(signature), "bytes") fmt.Println("verified:", mldsa.Verify(pub, message, signature, nil) == nil) fmt.Println("tampered message:", mldsa.Verify(pub, []byte("hello, post-quantum world!"), signature, nil)) opts := &mldsa.Options{Context: "invoice-v1"} // must match on both sides invoiceSig, err := key.Sign(nil, message, opts) if err != nil { log.Fatal(err) } fmt.Println("with the context:", mldsa.Verify(pub, message, invoiceSig, opts) == nil, "without:", mldsa.Verify(pub, message, invoiceSig, nil) == nil)} package main import ( "crypto/mldsa" "crypto/x509" "encoding/pem" "fmt" "os") // verifyWithPEM checks a signature with an SPKI "PUBLIC KEY" file, e.g. one written by OpenSSL 3.5.func verifyWithPEM(path string, message, signature []byte) (bool, error) { data, err := os.ReadFile(path) if err != nil { return false, err } block, _ := pem.Decode(data) if block == nil || block.Type != "PUBLIC KEY" { return false, fmt.Errorf("%s: no PUBLIC KEY block", path) } parsed, err := x509.ParsePKIXPublicKey(block.Bytes) // *mldsa.PublicKey since Go 1.26 if err != nil { return false, err } pub, ok := parsed.(*mldsa.PublicKey) if !ok || pub.Parameters() != mldsa.MLDSA65() { return false, fmt.Errorf("%s: not an ML-DSA-65 key", path) } return mldsa.Verify(pub, message, signature, nil) == nil, nil} How it works
mldsa.GenerateKey(mldsa.MLDSA65())creates a key;PublicKey().Bytes()is the 1952-byte public key andSignreturns a 3309-byte signature.key.Sign(nil, message, opts)signs the message itself (no pre-hashing); theio.Readerargument is ignored andoptsmay carry aContext.mldsa.Verify(pub, message, signature, opts)returnsnilfor a valid signature andmldsa: invalid signatureotherwise.x509.ParsePKIXPublicKeyreturns*mldsa.PublicKeyfor an ML-DSA SPKI; the run verifies OpenSSL's signature with it, andNewPrivateKeyplusSignDeterministicon the shared seed reproduce OpenSSL's deterministic signature byte for byte.
Gotchas
x509.ParsePKCS8PrivateKeyreads seed-only keys but refuses OpenSSL's default file:x509: ML-DSA private keys with both seed and expanded key are not supported, use e.g. "openssl pkey -provparam ml-dsa.output_formats=seed-only" to convert to a seed-only key.- A context longer than 255 bytes fails with
mldsa: context too long; and a signature made with a context does not verify without it. Signis randomized andSignDeterministicis not: use the deterministic variant only when you need reproducible signatures, for example in test vectors.crypto/mldsais unavailable when the program is built against the FIPS 140-3 Go Cryptographic Module v1.0.0;GenerateKey,NewPrivateKey,NewPublicKeyandVerifythen return an error.