using System.Security.Cryptography; // .NET 10: on Linux this needs OpenSSL 3.5 or later underneathif (!MLDsa.IsSupported) throw new PlatformNotSupportedException("ML-DSA is not available on this platform"); using MLDsa key = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65);byte[] message = "hello, post-quantum world"u8.ToArray(); byte[] signature = key.SignData(message); // randomized (hedged) signingConsole.WriteLine($"public key {key.ExportMLDsaPublicKey().Length} bytes, signature {signature.Length} bytes");Console.WriteLine($"verified: {key.VerifyData(message, signature)}");Console.WriteLine($"tampered message: {key.VerifyData("hello, post-quantum world!"u8.ToArray(), signature)}"); // A context string (up to 255 bytes) binds the signature to one purposebyte[] context = "invoice-v1"u8.ToArray();byte[] invoiceSignature = key.SignData(message, context);Console.WriteLine($"with the context: {key.VerifyData(message, invoiceSignature, context)}, without: {key.VerifyData(message, invoiceSignature)}"); using System.Security.Cryptography; byte[] message = "hello, post-quantum world"u8.ToArray(); // SPKI / PKCS#8 / PEM methods are marked experimental (SYSLIB5006) in .NET 10#pragma warning disable SYSLIB5006using MLDsa openssl = MLDsa.ImportFromPem(File.ReadAllText("openssl-pub.pem"));#pragma warning restore SYSLIB5006byte[] opensslSignature = Convert.FromBase64String(File.ReadAllText("openssl-sig.b64"));Console.WriteLine($"OpenSSL signature verifies: {openssl.VerifyData(message, opensslSignature)}"); // The 32-byte seed is the whole private key: the same seed gives the same key everywherebyte[] seed = Convert.FromHexString(Environment.GetEnvironmentVariable("SEED_HEX")!);using MLDsa seeded = MLDsa.ImportMLDsaPrivateSeed(MLDsaAlgorithm.MLDsa65, seed);Console.WriteLine($"key from the shared seed equals OpenSSL's: {seeded.ExportMLDsaPublicKey().AsSpan().SequenceEqual(openssl.ExportMLDsaPublicKey())}");Console.WriteLine($"two signatures equal: {seeded.SignData(message).AsSpan().SequenceEqual(seeded.SignData(message))}"); How it works
MLDsa.IsSupportedtells whether the platform provides ML-DSA; check it before generating keys.SignData(message)returns a 3309-byte signature andVerifyData(message, signature)abool; both take an optional context (up to 255 bytes).ExportMLDsaPublicKey()returns the raw 1952-byte public key;ImportMLDsaPrivateSeedbuilds the key from the 32-byte seed — the shared seed gives OpenSSL's exact public key.MLDsa.ImportFromPemreads OpenSSL'sBEGIN PUBLIC KEYfile; the run verifies OpenSSL's signature with it.
Gotchas
- On the Ubuntu 24.04 SDK image (OpenSSL 3.0.13)
MLDsa.IsSupportedisfalse; the Alpine 3.24 image ships OpenSSL 3.5 and supports it. The same .NET version behaves differently depending on the OpenSSL it finds. - The SPKI, PKCS#8 and PEM methods (
ImportFromPem,ExportSubjectPublicKeyInfoPem,ExportPkcs8PrivateKey…) are marked experimental in .NET 10: in a file-based app the build fails witherror SYSLIB5006unless you suppress the diagnostic, asinterop.csdoes. - Signing is randomized: two signatures of the same message differ.
- A signature made with a context does not verify without it (
Falsein the run).