{"id":58921,"date":"2025-11-18T08:00:00","date_gmt":"2025-11-18T16:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=58921"},"modified":"2025-11-18T08:00:00","modified_gmt":"2025-11-18T16:00:00","slug":"post-quantum-cryptography-in-dotnet","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/post-quantum-cryptography-in-dotnet\/","title":{"rendered":"Post-Quantum Cryptography in .NET"},"content":{"rendered":"<p>The main focus of .NET Cryptography for .NET 10 was adding support for Post-Quantum Cryptography (PQC).\nCryptography work tends to be in the &#8220;it&#8217;s important, but not really worth talking about&#8221; camp,\nbut since there&#8217;s been a fair amount of buzz this year regarding PQC,\nthis seems like a good time to talk about PQC in .NET.<\/p>\n<p>First, a note about nomenclature.\nThe &#8220;Post&#8221; in &#8220;Post-Quantum&#8221; doesn&#8217;t mean &#8220;quantum computers are here,&#8221;\nit mostly means &#8220;algorithms that won&#8217;t be compromised by the existence of a sufficiently powerful quantum computer.&#8221;\nEven that is overreaching, because a &#8220;cryptographically-relevant quantum computer&#8221; (CRQC) won&#8217;t have as significant an impact\non AES, the SHA-2 family of hash algorithms, or the SHA-3 family of hash algorithms as it will for ECC (EC-DSA, EC-Diffie-Hellman, etc) or RSA.\nSo, mainly, &#8220;PQC&#8221; just means &#8220;some new algorithms we&#8217;re adding because quantum computers are a threat to RSA and ECC.&#8221;<\/p>\n<p>Strategies like &#8220;<a href=\"https:\/\/en.wikipedia.org\/wiki\/Harvest_now,_decrypt_later\">Harvest now, decrypt later<\/a>&#8221; mean that the transition\nfrom &#8220;traditional&#8221; asymmetric cryptography to PQC should be done <em>before<\/em> CRQCs exist.\nDepending on the futurist you ask (and how important your data is),\nthe time to switch is anywhere from &#8220;years ago&#8221; to &#8220;maybe never&#8221;.\nWe don&#8217;t have a time machine, so we can&#8217;t solve it for &#8220;years ago&#8221;,\nbut we achieved &#8220;in our first release after the first specifications were standardized&#8221;,\nso &#8220;now&#8221; is about as good as it gets!<\/p>\n<p>In .NET 10 we&#8217;re focusing on 4 PQC algorithms:<\/p>\n<table>\n<thead>\n<tr>\n<th>Algorithm<\/th>\n<th>Kind<\/th>\n<th>Specification<\/th>\n<th>.NET Class<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>ML-KEM<\/td>\n<td>Key Encapsulation<\/td>\n<td>NIST <a href=\"https:\/\/nvlpubs.nist.gov\/nistpubs\/FIPS\/NIST.FIPS.203.pdf\">FIPS 203<\/a><\/td>\n<td><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.security.cryptography.mlkem\">MLKem<\/a><\/td>\n<\/tr>\n<tr>\n<td>ML-DSA<\/td>\n<td>Signature<\/td>\n<td>NIST <a href=\"https:\/\/nvlpubs.nist.gov\/nistpubs\/FIPS\/NIST.FIPS.204.pdf\">FIPS 204<\/a><\/td>\n<td><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.security.cryptography.mldsa\">MLDsa<\/a><\/td>\n<\/tr>\n<tr>\n<td>SLH-DSA<\/td>\n<td>Signature<\/td>\n<td>NIST <a href=\"https:\/\/nvlpubs.nist.gov\/nistpubs\/FIPS\/NIST.FIPS.205.pdf\">FIPS 205<\/a><\/td>\n<td><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.security.cryptography.slhdsa\">SlhDsa<\/a><\/td>\n<\/tr>\n<tr>\n<td>Composite ML-DSA<\/td>\n<td>Signature<\/td>\n<td>IETF Draft &#8220;<a href=\"https:\/\/datatracker.ietf.org\/doc\/draft-ietf-lamps-pq-composite-sigs\/\">Composite ML-DSA for use in X.509 Public Key Infrastructure<\/a>&#8220;<\/td>\n<td><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.security.cryptography.compositemldsa\">CompositeMLDsa<\/a><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>ML-DSA, SLH-DSA, and Composite ML-DSA are all replacements for signatures by RSA and EC-DSA.\nML-KEM logically replaces both RSA &#8220;Key Transport&#8221; and EC-Diffie-Hellman &#8220;Key Agreement&#8221;, though the actual usage isn&#8217;t close to being a drop-in replacement for either of them.\nThere is no direct replacement for RSA &#8220;Data Encryption&#8221;, but that&#8217;s because that&#8217;s not a recommended use of RSA in the first place.<\/p>\n<h2>The Way We&#8217;ve Always Done It<\/h2>\n<p>Generally speaking, when you&#8217;re doing &#8220;a thing&#8221; that&#8217;s &#8220;like some other thing&#8221;, you should do it in the same way.\nIn .NET Cryptography, we have an established pattern for keys of asymmetric algorithms:<\/p>\n<ul>\n<li>Algorithm types derive from <code>AsymmetricAlgorithm<\/code><\/li>\n<li>Implementation types derive from the algorithm types<\/li>\n<li>Algorithm types have a static <code>Create()<\/code> method that works no matter what OS you&#8217;re on (unless the algorithm just isn&#8217;t supported on your OS).<\/li>\n<li>Keys can then be imported, explicitly generated, or implicitly generated.<\/li>\n<\/ul>\n<pre><code class=\"language-csharp\">namespace System.Security.Cryptography;\n\npublic partial class AsymmetricAlgorithm : IDisposable { }\npublic partial class RSA : AsymmetricAlgorithm\n{\n    public static RSA Create();\n}\npublic partial class DSA : AsymmetricAlgorithm\n{\n    public static DSA Create();\n}\npublic partial class RSACng : RSA { }\npublic partial class RSAOpenSsl : RSA { }\n\/\/ etc<\/code><\/pre>\n<h2>It Starts To Go Wrong<\/h2>\n<p>When we started the PQC project, the obvious answer was that we should continue to extend <code>AsymmetricAlgorithm<\/code>,\nbut there was sort of a hint that was a bad answer&#8230; and that&#8217;s the <code>KeySize<\/code> property on <code>AsymmetricAlgorithm<\/code>:<\/p>\n<pre><code class=\"language-csharp\">public partial class AsymmetricAlgorithm\n{\n    public virtual int KeySize { get; set; }\n}<\/code><\/pre>\n<p>This property was introduced when .NET only supported RSA and DSA, and it mostly made sense:\nwhen creating an RSA key or a DSA key, pretty much the only parameter is the RSA modulus (n) size or the DSA prime modulus (p) size.\nRSA&#8217;s <code>KeySize<\/code> values and DSA&#8217;s <code>KeySize<\/code> values shouldn&#8217;t be compared across the algorithms, but they are a property of any given key.<\/p>\n<p>Then we introduced EC-DSA and EC-DiffieHellman.\nECC keys have a simple integer value as a private key, a number in the range <code>[1, p)<\/code>, where <code>p<\/code> is the prime modulus for the &#8220;curve&#8221;.\nSo, OK, everyone agrees that the &#8220;key size&#8221; of an ECC key is &#8220;the number of bits required to represent <code>p<\/code>&#8220;.\n.NET at the time only supported 3 curves: NIST P-256, NIST P-384, and NIST P-521.\nThe number after &#8220;P-&#8221; is &#8220;how many bits are required to represent <code>p<\/code>&#8220;,\nso now we have a sensible answer for this property:\nthe getter reports the number of bits required to represent <code>p<\/code>,\nand the setter chooses from NIST P-256, NIST P-384, and NIST P-521.<\/p>\n<p>Then Windows added support for more elliptic curves, so .NET added support for more elliptic curves.\nBrainpool&#8217;s <code>brainpool384r1<\/code> and NIST&#8217;s P-384 both report <code>384<\/code> from the getter, but what should the setter do?\nThe best answer we came up with was &#8220;the setter still picks from the 3 options it had before, and we need a new way to inspect or specify the curve.&#8221;<\/p>\n<p>That was basically like hearing a creak of wood on a calm day while standing next to a dam.<\/p>\n<p>So now we&#8217;re adding more new algorithms.\nGiven an ML-DSA-65 key, what should we report as the <code>KeySize<\/code> value?\n&#8220;65&#8221; is an obvious answer, but it&#8217;s sort of meaningless (that name just means that this &#8220;parameter set&#8221; makes use of a 6&#215;5 matrix).\nThe &#8220;raw&#8221; public key for ML-DSA-65 is 1952 bytes, so maybe 1952?\nWell, this is cryptography, so it should be in bits: 15616?<\/p>\n<p>This was the start of a journey where we decided to &#8220;break up&#8221; with <code>AsymmetricAlgorithm<\/code>.<\/p>\n<h2>&#8230; Anything Else?<\/h2>\n<p>Many moons ago I saw a poster that said something like &#8220;Change is terrible&#8230; unless it&#8217;s great!&#8221;.\nBased on where it was, I think the target audience was UX designers and the poster was saying\n&#8220;users hate it when you move buttons around, so if you&#8217;re going to move it, you better have a great reason&#8221;.\nRegardless of the intended audience, the message has resonated with me all these years,\nand so I knew that we needed something other than &#8220;AsymmetricAlgorithm, but without the KeySize property.&#8221;\nSo, we took a look at what things we like about <code>AsymmetricAlgorithm<\/code>, and what parts we don&#8217;t.<\/p>\n<p>The bad parts:<\/p>\n<ul>\n<li>Heavy use of <code>public virtual<\/code> means that we have to repeat state and argument validation in every derived type.\n<ul>\n<li>And sometimes we didn&#8217;t repeat it correctly.<\/li>\n<\/ul>\n<\/li>\n<li>You have to create an instance to ask about its capabilities (e.g. <code>public virtual LegalKeySizes[] { get; }<\/code>)<\/li>\n<li><code>Create()<\/code> doesn&#8217;t generate a key, in case you do import.  As a result, key generation happens when the key is first needed, making for some perf surprises.<\/li>\n<li><code>Dispose()<\/code> doesn&#8217;t always mean &#8220;the object is unusable&#8221;, often it meant &#8220;I&#8217;ve abandoned this key, but I can generate another one!&#8221;<\/li>\n<li>You can&#8217;t really use it as-is. If you accept one you need to cast it to an algorithm type.<\/li>\n<li><code>KeySize<\/code> doesn&#8217;t seem to make sense for these new algorithms.<\/li>\n<li><code>KeyExchangeAlgorithm<\/code>, <code>SignatureAlgorithm<\/code>, <code>ToXmlString(bool)<\/code>, <code>FromXmlString(string)<\/code> are intrusions from <code>SignedXml<\/code> and <code>EncryptedXml<\/code>, they&#8217;re at the wrong layer.<\/li>\n<li><code>ExportParameters(bool)<\/code> makes it hard to write a consistent flow analyzer for when you have private key data or public key data.<\/li>\n<\/ul>\n<p>The good parts:<\/p>\n<ul>\n<li>There&#8217;s a consistent way to import\/export keys.<\/li>\n<\/ul>\n<p>Clearly, once we started making the &#8220;breakup&#8221; list, it was pretty obvious.<\/p>\n<p>Sorry, <code>AsymmetricAlgorithm<\/code>, it&#8217;s not you, it&#8217;s me (P.S.: it&#8217;s totally you).<\/p>\n<h2>The New Design&#8217;s Goals<\/h2>\n<ol>\n<li>Instances represent a key\/keypair.<\/li>\n<li>Once disposed, always disposed.<\/li>\n<li>Don&#8217;t have a &#8220;common base class&#8221; when two things don&#8217;t really have anything in common.<\/li>\n<li>Minimize code for derived types, so that we can minimize the room for mistakes.<\/li>\n<li>Use existing terminology when it means the same thing.<\/li>\n<li>Use new terminology when the existing terminology means something else.<\/li>\n<li>Design for Span<\/li>\n<\/ol>\n<h2>The New Design<\/h2>\n<p>Here&#8217;s a view of the class for ML-DSA, with most of the overloads removed for brevity:<\/p>\n<pre><code class=\"language-csharp\">namespace System.Security.Cryptography;\n\npublic abstract partial class MLDsa : System.IDisposable\n{\n    public static bool IsSupported { get; }\n\n    protected MLDsa(MLDsaAlgorithm algorithm);\n    public MLDsaAlgorithm Algorithm { get; }\n\n    public void Dispose();\n    protected virtual void Dispose(bool disposing);\n\n    \/\/ Generate a new key\n    public static MLDsa GenerateKey(MLDsaAlgorithm algorithm);\n\n    \/\/ Algorithm-specific key format imports\n    public static MLDsa ImportMLDsaPublicKey(MLDsaAlgorithm algorithm, ReadOnlySpan&lt;byte&gt; source);\n    public static MLDsa ImportMLDsaPrivateKey(MLDsaAlgorithm algorithm, ReadOnlySpan&lt;byte&gt; source);\n    public static MLDsa ImportMLDsaPrivateSeed(MLDsaAlgorithm algorithm, ReadOnlySpan&lt;byte&gt; source);\n\n    \/\/ Standard key container format imports\n    public static MLDsa ImportSubjectPublicKeyInfo(ReadOnlySpan&lt;byte&gt; source);\n    public static MLDsa ImportPkcs8PrivateKey(ReadOnlySpan&lt;byte&gt; source);\n    public static MLDsa ImportEncryptedPkcs8PrivateKey(ReadOnlySpan&lt;char&gt; password, ReadOnlySpan&lt;byte&gt; source);\n    public static MLDsa ImportFromPem(ReadOnlySpan&lt;char&gt; source);\n    public static MLDsa ImportFromEncryptedPem(ReadOnlySpan&lt;char&gt; source, ReadOnlySpan&lt;char&gt; password);\n\n    \/\/ Algorithm-specific key format exports\n    public void ExportMLDsaPublicKey(Span&lt;byte&gt; destination);\n    public void ExportMLDsaPrivateKey(Span&lt;byte&gt; destination);\n    public void ExportMLDsaPrivateSeed(Span&lt;byte&gt; destination);\n\n    \/\/ Standard key container format exports\n    public byte[] ExportSubjectPublicKeyInfo();\n    public byte[] ExportPkcs8PrivateKey();\n    public byte[] ExportEncryptedPkcs8PrivateKey(ReadOnlySpan&lt;byte&gt; passwordBytes, PbeParameters pbeParameters);\n    public string ExportSubjectPublicKeyInfoPem();\n    public string ExportEncryptedPkcs8PrivateKeyPem(ReadOnlySpan&lt;char&gt; password, PbeParameters pbeParameters);\n\n    \/\/ Operations the algorithm can perform\n    public void SignData(ReadOnlySpan&lt;byte&gt; data, Span&lt;byte&gt; destination, ReadOnlySpan&lt;byte&gt; context = default);\n    public void SignMu(ReadOnlySpan&lt;byte&gt; externalMu, Span&lt;byte&gt; destination);\n    public void SignPreHash(ReadOnlySpan&lt;byte&gt; hash, Span&lt;byte&gt; destination, string hashAlgorithmOid, ReadOnlySpan&lt;byte&gt; context = default);\n    public bool VerifyData(ReadOnlySpan&lt;byte&gt; data, ReadOnlySpan&lt;byte&gt; signature, ReadOnlySpan&lt;byte&gt; context = default);\n    public bool VerifyMu(ReadOnlySpan&lt;byte&gt; externalMu, ReadOnlySpan&lt;byte&gt; signature);\n    public bool VerifyPreHash(ReadOnlySpan&lt;byte&gt; hash, ReadOnlySpan&lt;byte&gt; signature, string hashAlgorithmOid, ReadOnlySpan&lt;byte&gt; context = default);\n\n    \/\/ Key exports, implementation-specific.    \n    protected abstract void ExportMLDsaPrivateSeedCore(Span&lt;byte&gt; destination);\n    protected abstract void ExportMLDsaPublicKeyCore(Span&lt;byte&gt; destination);\n    protected abstract void ExportMLDsaPrivateKeyCore(Span&lt;byte&gt; destination);\n    protected abstract bool TryExportPkcs8PrivateKeyCore(Span&lt;byte&gt; destination, out int bytesWritten);\n\n    \/\/ Algorithm operations, implementation-specific.\n    protected abstract void SignDataCore(ReadOnlySpan&lt;byte&gt; data, ReadOnlySpan&lt;byte&gt; context, Span&lt;byte&gt; destination);\n    protected abstract void SignMuCore(ReadOnlySpan&lt;byte&gt; externalMu, Span&lt;byte&gt; destination);\n    protected abstract void SignPreHashCore(ReadOnlySpan&lt;byte&gt; hash, ReadOnlySpan&lt;byte&gt; context, string hashAlgorithmOid, Span&lt;byte&gt; destination);\n    protected abstract bool VerifyDataCore(ReadOnlySpan&lt;byte&gt; data, ReadOnlySpan&lt;byte&gt; context, ReadOnlySpan&lt;byte&gt; signature);\n    protected abstract bool VerifyMuCore(ReadOnlySpan&lt;byte&gt; externalMu, ReadOnlySpan&lt;byte&gt; signature);\n    protected abstract bool VerifyPreHashCore(ReadOnlySpan&lt;byte&gt; hash, ReadOnlySpan&lt;byte&gt; context, string hashAlgorithmOid, ReadOnlySpan&lt;byte&gt; signature);\n}<\/code><\/pre>\n<p>Let&#8217;s first see how this stacks up against our goals:<\/p>\n<ol>\n<li>\u2705 All of the instance methods are about &#8220;the key\/keypair&#8221;, none are about &#8220;the algorithm&#8221;.\n<ul>\n<li>Generating and importing keys are <code>static<\/code> methods.<\/li>\n<\/ul>\n<\/li>\n<li>\u2705 You can&#8217;t tell from the class shape, but the base class tracks disposal and won&#8217;t call any virtual members once the key is disposed.<\/li>\n<li>\u2705 It turns out ML-DSA and ML-KEM have very little in common. And while ML-DSA and Composite ML-DSA sound similar, they differ in important ways.\n<ul>\n<li>So all of the new algorithms directly extend <code>object<\/code>.<\/li>\n<\/ul>\n<\/li>\n<li>\u2705 The class extensively uses the Template Method Pattern. All argument and state validation is done in the base class&#8217;s <code>public<\/code> methods, the <code>protected abstract<\/code> methods only have to do the last step.<\/li>\n<li>\u2705 <code>RSA<\/code> and <code>ECDsa<\/code> both have a method named <code>SignData<\/code> that takes the full data to sign and produces a signature.  <code>MLDsa<\/code> matches that.\n<ul>\n<li><code>MLDsa<\/code>&#8216;s version gains an extra <code>context<\/code> parameter from the specification, but that doesn&#8217;t fundamentally change the terminology.<\/li>\n<li>Additionally, all of the Export methods from <code>AsymmetricAlgorithm<\/code> are here, with the same parameters, in the same order.<\/li>\n<\/ul>\n<\/li>\n<li>\u2705 <code>RSA<\/code> and <code>ECDsa<\/code> both have a method named <code>SignHash<\/code>, it produces a signature that is compatible with <code>SignData<\/code>.  ML-DSA&#8217;s <code>HashML-DSA<\/code> variant produces an intentionally <em>incompatible<\/em> signature, so instead of <code>SignHash<\/code> it&#8217;s called <code>SignPreHash<\/code>.\n<ul>\n<li><code>SignMu<\/code> is closer to <code>SignHash<\/code>, but it&#8217;s still different. And it&#8217;s very different from <code>SignPreHash<\/code>, so it needed an even more unique name.<\/li>\n<\/ul>\n<\/li>\n<li>\u2705 There are no <code>abstract<\/code> or <code>virtual<\/code> methods that operate on arrays.  The <code>MLDsa<\/code> base class does have a lot of overloads that accept (or return) arrays, but those are just for caller convenience.<\/li>\n<\/ol>\n<p>One thing that may stand out is the prevalence of <code>void<\/code> methods writing to spans.\nFor ML-DSA, ML-KEM, and SLH-DSA, all of the algorithm operations have fixed-size responses;\nthat means there was a strong case made for &#8220;if you pass a buffer that&#8217;s not exactly the correct size, you&#8217;re holding it wrong.&#8221;\nOkay, so how do you know how to hold it right?\nClearly, just open <a href=\"https:\/\/nvlpubs.nist.gov\/nistpubs\/FIPS\/NIST.FIPS.204.pdf\">FIPS 204 (Module-Lattice-Based Digital Signature Standard)<\/a>,\njump down to section 4 (Parameter Sets), and read Table 2 (Sizes (in bytes) of keys and signatures of ML-DSA)<\/p>\n<table>\n<thead>\n<tr>\n<th><\/th>\n<th>Private Key<\/th>\n<th>Public Key<\/th>\n<th>Signature Size<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>ML-DSA-44<\/td>\n<td>2560<\/td>\n<td>1312<\/td>\n<td>2420<\/td>\n<\/tr>\n<tr>\n<td>ML-DSA-65<\/td>\n<td>4032<\/td>\n<td>1952<\/td>\n<td>3309<\/td>\n<\/tr>\n<tr>\n<td>ML-DSA-87<\/td>\n<td>4896<\/td>\n<td>2592<\/td>\n<td>4627<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Just kidding.<\/p>\n<p>The data from this table (as well as other data) is in the <code>Algorithm<\/code> property:<\/p>\n<pre><code class=\"language-csharp\">namespace System.Security.Cryptography;\n\npublic sealed partial class MLDsaAlgorithm : IEquatable&lt;MLDsaAlgorithm&gt;\n{\n    public static MLDsaAlgorithm MLDsa44 { get; }\n    public static MLDsaAlgorithm MLDsa65 { get; }\n    public static MLDsaAlgorithm MLDsa87 { get; }\n\n    public string Name { get; }\n\n    public int MuSizeInBytes { get; }\n    public int PrivateKeySizeInBytes { get; }\n    public int PrivateSeedSizeInBytes { get; }\n    public int PublicKeySizeInBytes { get; }\n    public int SignatureSizeInBytes { get; }\n}<\/code><\/pre>\n<p>Every now and then, someone wants\/needs to do interop with the underlying provider.\nSo, we still have the Cng and OpenSsl derived types, but they&#8217;re much, much smaller.<\/p>\n<pre><code class=\"language-csharp\">namespace System.Security.Cryptography;\n\npublic sealed partial class MLDsaCng : MLDsa\n{\n    public MLDsaCng(CngKey key) : base (GetMLDsaAlgorithm(key)) { }\n\n    public CngKey GetKey();\n\n    protected override void Dispose(bool disposing);\n    protected override void ExportMLDsaPrivateKeyCore(Span&lt;byte&gt; destination);\n    protected override void ExportMLDsaPrivateSeedCore(Span&lt;byte&gt; destination);\n    protected override void ExportMLDsaPublicKeyCore(Span&lt;byte&gt; destination);\n    protected override void SignDataCore(ReadOnlySpan&lt;byte&gt; data, ReadOnlySpan&lt;byte&gt; context, Span&lt;byte&gt; destination);\n    protected override void SignMuCore(ReadOnlySpan&lt;byte&gt; externalMu, Span&lt;byte&gt; destination);\n    protected override void SignPreHashCore(ReadOnlySpan&lt;byte&gt; hash, ReadOnlySpan&lt;byte&gt; context, string hashAlgorithmOid, Span&lt;byte&gt; destination);\n    protected override bool TryExportPkcs8PrivateKeyCore(Span&lt;byte&gt; destination, out int bytesWritten);\n    protected override bool VerifyDataCore(ReadOnlySpan&lt;byte&gt; data, ReadOnlySpan&lt;byte&gt; context, ReadOnlySpan&lt;byte&gt; signature);\n    protected override bool VerifyMuCore(ReadOnlySpan&lt;byte&gt; externalMu, ReadOnlySpan&lt;byte&gt; signature);\n    protected override bool VerifyPreHashCore(ReadOnlySpan&lt;byte&gt; hash, ReadOnlySpan&lt;byte&gt; context, string hashAlgorithmOid, ReadOnlySpan&lt;byte&gt; signature);\n}\n\npublic sealed partial class MLDsaOpenSsl : MLDsa\n{\n    public MLDsaOpenSsl(SafeEvpPKeyHandle pkeyHandle) : base (GetMLDsaAlgorithm(pkeyHandle)) { }\n\n    public SafeEvpPKeyHandle DuplicateKeyHandle();\n\n    protected override void Dispose(bool disposing);\n    protected override void ExportMLDsaPrivateKeyCore(Span&lt;byte&gt; destination);\n    protected override void ExportMLDsaPrivateSeedCore(Span&lt;byte&gt; destination);\n    protected override void ExportMLDsaPublicKeyCore(Span&lt;byte&gt; destination);\n    protected override void SignDataCore(ReadOnlySpan&lt;byte&gt; data, ReadOnlySpan&lt;byte&gt; context, Span&lt;byte&gt; destination);\n    protected override void SignMuCore(ReadOnlySpan&lt;byte&gt; externalMu, Span&lt;byte&gt; destination);\n    protected override void SignPreHashCore(ReadOnlySpan&lt;byte&gt; hash, ReadOnlySpan&lt;byte&gt; context, string hashAlgorithmOid, Span&lt;byte&gt; destination);\n    protected override bool TryExportPkcs8PrivateKeyCore(Span&lt;byte&gt; destination, out int bytesWritten);\n    protected override bool VerifyDataCore(ReadOnlySpan&lt;byte&gt; data, ReadOnlySpan&lt;byte&gt; context, ReadOnlySpan&lt;byte&gt; signature);\n    protected override bool VerifyMuCore(ReadOnlySpan&lt;byte&gt; externalMu, ReadOnlySpan&lt;byte&gt; signature);\n    protected override bool VerifyPreHashCore(ReadOnlySpan&lt;byte&gt; hash, ReadOnlySpan&lt;byte&gt; context, string hashAlgorithmOid, ReadOnlySpan&lt;byte&gt; signature);\n}<\/code><\/pre>\n<p>And that is our last change from the existing types: there&#8217;s no &#8220;import a key into MLDsaCng&#8221;, or &#8220;generate a key with MLDsaCng&#8221;.\nWhy? The primary reason is that you shouldn&#8217;t care.\nMLDsaCng doesn&#8217;t work on Linux, MLDsaOpenSsl doesn&#8217;t work on Windows;\nso if you&#8217;re writing a run-anywhere app or library, you want to stick to just using the base class.\nIf you&#8217;re trying to work with the underlying provider, that work should be done using the provider classes, like <code>CngKey.Create(...)<\/code>.<\/p>\n<h2>Does This Help Me As An Implementer?<\/h2>\n<p>OK, it&#8217;s pretty unusual that someone other than us extends cryptographic key types, but it happens.\nThe answer is, emphatically, &#8220;yes!&#8221;<\/p>\n<p>For <code>RSAOpenSsl<\/code> (and the hidden class used by <code>RSA.Create()<\/code> on Linux), signing looks like this:<\/p>\n<pre><code class=\"language-csharp\">public override bool TrySignHash(\n    ReadOnlySpan&lt;byte&gt; hash,\n    Span&lt;byte&gt; destination,\n    HashAlgorithmName hashAlgorithm,\n    RSASignaturePadding padding,\n    out int bytesWritten)\n{\n    ArgumentException.ThrowIfNullOrEmpty(hashAlgorithm.Name, nameof(hashAlgorithm));\n    ArgumentNullException.ThrowIfNull(padding);\n    ThrowIfDisposed();\n\n    SafeEvpPKeyHandle key = GetKey();\n    int bytesRequired = Interop.Crypto.GetEvpPKeySizeBytes(key);\n\n    if (destination.Length &lt; bytesRequired)\n    {\n        bytesWritten = 0;\n        return false;\n    }\n\n    bytesWritten = Interop.Crypto.RsaSignHash(key, padding.Mode, hashAlgorithm, hash, destination);\n    Debug.Assert(bytesWritten == bytesRequired);\n    return true;\n}<\/code><\/pre>\n<p>Argument validation, a disposed state check, a precondition on the destination size to prevent an out-of-bounds write when calling the provider implementation, and then finally the call to the provider.<\/p>\n<p>Here&#8217;s the same for <code>MLDsaOpenSsl<\/code>:<\/p>\n<pre><code class=\"language-csharp\">protected override void SignDataCore(ReadOnlySpan&lt;byte&gt; data, ReadOnlySpan&lt;byte&gt; context, Span&lt;byte&gt; destination) =&gt;\n    Interop.Crypto.MLDsaSignPure(_key, data, context, destination);<\/code><\/pre>\n<p>&#8220;But you can hide all manner of sins behind a one-line call&#8221;.  OK, here&#8217;s <code>MLDsaSignPure<\/code>:<\/p>\n<pre><code class=\"language-csharp\">internal static void MLDsaSignPure(\n    SafeEvpPKeyHandle pkey,\n    ReadOnlySpan&lt;byte&gt; msg,\n    ReadOnlySpan&lt;byte&gt; context,\n    Span&lt;byte&gt; destination)\n{\n    int ret = CryptoNative_MLDsaSignPure(\n        pkey, GetExtraHandle(pkey),\n        msg, msg.Length,\n        context, context.Length,\n        destination, destination.Length);\n\n    if (ret != 1)\n    {\n        throw Interop.Crypto.CreateOpenSslCryptographicException();\n    }\n}<\/code><\/pre>\n<p>The only thing <code>MLDsaOpenSsl.SignDataCore<\/code> needs to do is <em>call OpenSSL<\/em>,\neverything else was done in the base class.<\/p>\n<p>For <code>RSA<\/code>, every single derived type gets independently tested for<\/p>\n<ul>\n<li>Argument validation<\/li>\n<li>Disposed state<\/li>\n<li>Argument validation vs Disposed state ordering<\/li>\n<li>Buffer too small<\/li>\n<\/ul>\n<p>just to make sure they&#8217;re consistent.<\/p>\n<p>For <code>MLDsa<\/code>, it&#8217;s impossible for them to be inconsistent, so we only need to test the base class.<\/p>\n<p><code>RSA<\/code>-derived types also get tested with a correct buffer, and an overly large buffer, when doing &#8220;algorithm correctness&#8221; tests.\nFor <code>MLDsa<\/code>-derived types, that&#8217;s almost the entirety of the tests we run.<\/p>\n<p>So, overall it&#8217;s less code to write, is therefore less error-prone, and by eliminating categories of tests it makes the overall testing phase faster (with no loss of coverage).\nSounds like a win.<\/p>\n<p>There is one drawback in testing, and that&#8217;s that we want a separation between &#8220;testing the <code>MLDsa<\/code> base class behaviors&#8221; and &#8220;testing <code>MLDsa<\/code> implementations&#8221; (so we can actually run fewer tests overall),\nbut the most obvious name for each of those halves is <code>MLDsaTests<\/code>.\nWhile all of the algorithms use the same strategy of a <code>static class<\/code> to test things like &#8220;no abstract or virtual methods are called once the instance is disposed&#8221;,\nand an <code>abstract class<\/code> to make sure that the implementation types are performing the algorithms correctly,\nwe ended up with four different naming patterns for four algorithms (none of which are clearly superior to the others):<\/p>\n<table>\n<thead>\n<tr>\n<th>Algorithm<\/th>\n<th>static test class<\/th>\n<th>instance test class<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>ML-DSA<\/td>\n<td>MLDsaTests<\/td>\n<td>MLDsaTestsBase<\/td>\n<\/tr>\n<tr>\n<td>ML-KEM<\/td>\n<td>MLKemTests<\/td>\n<td>MLKemBaseTests<\/td>\n<\/tr>\n<tr>\n<td>SLH-DSA<\/td>\n<td>SlhDsaContractTests<\/td>\n<td>SlhDsaTests<\/td>\n<\/tr>\n<tr>\n<td>Composite ML-DSA<\/td>\n<td>CompositeMLDsaContractTests<\/td>\n<td>CompositeMLDsaTestsBase<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2>What&#8217;s Up With <code>[Experimental]<\/code>?<\/h2>\n<p>In .NET Cryptography, we observe a modified &#8220;rule-of-two&#8221;:\nwe don&#8217;t (usually) add an algorithm unless two (or more) of our supported OSes offer it.\nThis helps us to reduce situations where we&#8217;ve designed something that can&#8217;t be fulfilled by a new OS, or an OS that added the feature later.\nWhen I was in college, math students used the phrase &#8220;engineering induction&#8221; for the notion of &#8220;if it works three times, it&#8217;ll work forever&#8221; (versus the much more rigid mathematical induction we had to use in formal proofs).\nOur &#8220;rule-of-two&#8221; is like that&#8230; except with two instead of three: if an algorithm feature is exposed by any two of Windows, OpenSSL, or macOS, it&#8217;ll <em>probably<\/em> work on the third.<\/p>\n<p>Since Windows hasn&#8217;t yet (as of this writing) added support for SLH-DSA,\nand neither Windows nor OpenSSL have added Composite ML-DSA as a first-class algorithm,\nwe&#8217;ve decided to release the <code>SlhDsa<\/code> and <code>CompositeMLDsa<\/code> classes with <code>[Experimental]<\/code> on the classes themselves.\nIt&#8217;s possible (though not expected) that we&#8217;ll have to make breaking structural changes to these classes when the OS support arrives.<\/p>\n<p>For <code>MLKem<\/code> and <code>MLDsa<\/code> we&#8217;ve removed <code>[Experimental]<\/code> from the classes,\nbut it remains on a few methods:<\/p>\n<pre><code class=\"language-csharp\">using System.Security.Cryptography;\n\npublic abstract partial class MLKem : System.IDisposable\n{\n    \/\/ These are [Experimental]\n    [Experimental(\"SYSLIB5006\", UrlFormat=\"https:\/\/aka.ms\/dotnet-warnings\/{0}\")]\n    public byte[] ExportEncryptedPkcs8PrivateKey(ReadOnlySpan&lt;byte&gt; passwordBytes, PbeParameters pbeParameters);\n    [Experimental(\"SYSLIB5006\", UrlFormat=\"https:\/\/aka.ms\/dotnet-warnings\/{0}\")]\n    public byte[] ExportPkcs8PrivateKey();\n    [Experimental(\"SYSLIB5006\", UrlFormat=\"https:\/\/aka.ms\/dotnet-warnings\/{0}\")]\n    public byte[] ExportSubjectPublicKeyInfo();\n    [Experimental(\"SYSLIB5006\", UrlFormat=\"https:\/\/aka.ms\/dotnet-warnings\/{0}\")]\n    public static MLKem ImportFromPem(ReadOnlySpan&lt;char&gt; source);\n    ...\n\n    \/\/ These are not\n    public byte[] ExportPrivateSeed();\n    public static MLKem GenerateKey(MLKemAlgorithm algorithm);\n    public static MLKem ImportDecapsulationKey(MLKemAlgorithm algorithm, byte[] source);\n    public void Encapsulate(Span&lt;byte&gt; ciphertext, Span&lt;byte&gt; sharedSecret);\n    public void Decapsulate(ReadOnlySpan&lt;byte&gt; ciphertext, Span&lt;byte&gt; sharedSecret);\n    ...\n}<\/code><\/pre>\n<p>&#8220;What&#8217;s the difference?&#8221; you ask? Mainly spec ownership.<\/p>\n<p>All of the parts of the class that come from <a href=\"https:\/\/nvlpubs.nist.gov\/nistpubs\/FIPS\/NIST.FIPS.203.pdf\">FIPS 203<\/a> are in a published spec,\nhave been written for both Windows and OpenSSL, and we&#8217;ve integrated with them.\nTherefore, there are no surprises left there, and so no need for <code>[Experimental]<\/code>.<\/p>\n<p>The PKCS#8 PrivateKeyInfo and X.509 SubjectPublicKeyInfo formats of the key, however,\ncome from a different spec (in this case, &#8220;draft-ietf-lamps-kyber-certificates&#8221;).\nWhen our last possible day for changes came up, the specification had not yet been published.\nWhile draft-11 looks like it will be published as the finished RFC,\nwe still needed to make callers aware that these formats were still susceptible to both breaking changes and interoperability concerns.\nIf draft-11 is published as the RFC (or was by the time you read this), you can just suppress the diagnostic.<\/p>\n<p>It is very similar for <code>MLDsa<\/code>, except that, additionally, <code>SignPreHash<\/code> and <code>VerifyPreHash<\/code> are <code>[Experimental]<\/code> even though they come from <a href=\"https:\/\/nvlpubs.nist.gov\/nistpubs\/FIPS\/NIST.FIPS.204.pdf\">NIST FIPS 204<\/a>.\nThat&#8217;s mainly because we feel that representing the hash algorithm by name isn&#8217;t <em>quite<\/em> right, and representing it by OID is not very user friendly\n(&#8220;SHAKE-128&#8221; is a XOF, which means it doesn&#8217;t have a fixed output length; &#8220;2.16.840.1.101.3.4.2.11&#8221; means &#8220;a 256-bit extraction from SHAKE-128&#8221;).\nWe&#8217;re also not certain if we should be validating that the pre-hash length matches the hash algorithm output,\nor if &#8220;garbage in, garbage out&#8221; is the correct design.\nUltimately, these both tie back to the &#8220;rule-of-two&#8221;, because while OpenSSL 3.5 supports HashML-DSA,\nit&#8217;s not as polished as pure ML-DSA,\nand we&#8217;re waiting on them (and the rest of the ecosystem) for some future guidance.<\/p>\n<h2>Where Does .NET Use These Algorithms?<\/h2>\n<p>These new algorithms can be used a few places within the <code>System.Security.Cryptography<\/code> namespaces:<\/p>\n<ul>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.security.cryptography.x509certificates.certificaterequest\"><code>CertificateRequest<\/code><\/a>: <code>MLDsa<\/code>, <code>SlhDsa<\/code>, <code>CompositeMLDsa<\/code><\/li>\n<li><code>SignedCms<\/code>&#8216; <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.security.cryptography.pkcs.cmssigner\"><code>CmsSigner<\/code><\/a>: <code>MLDsa<\/code>, <code>SlhDsa<\/code><\/li>\n<li>COSE&#8217;s <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.security.cryptography.cose.cosesigner\"><code>CoseSigner<\/code><\/a>: <code>MLDsa<\/code>\n<ul>\n<li><code>CoseSigner<\/code> doesn&#8217;t accept an <code>MLDsa<\/code> directly.  The COSE library added a new <code>CoseKey<\/code> type to reduce the number of overloads required for key-specific verification, and <code>CoseKey<\/code> can accept an <code>MLDsa<\/code> instance.<\/li>\n<li><a href=\"https:\/\/datatracker.ietf.org\/doc\/draft-ietf-cose-sphincs-plus\/\">COSE with SLH-DSA<\/a> is an expired draft specification, so we left it out.  Based on current trends, it&#8217;ll probably turn up for .NET 11, but that&#8217;s not a promise.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p><code>CertificateRequest<\/code> accepting PQC keys may have already been a hint,\nbut just like .NET&#8217;s <code>X509Certificate2<\/code> instances can know about <code>RSA<\/code>, <code>DSA<\/code>, <code>ECDsa<\/code>, and <code>ECDiffieHellman<\/code> private keys,\nthey can also know about <code>MLDsa<\/code>, <code>SlhDsa<\/code>, and <code>CompositeMLDsa<\/code> private keys.\n<code>X509Certificate2<\/code> can also track <code>MLKem<\/code> private keys,\nbut since <code>MLKem<\/code> can&#8217;t self-sign it isn&#8217;t tightly integrated with <code>CertificateRequest<\/code>.<\/p>\n<p>Most .NET APIs that accept an <code>X509Certificate2<\/code> (or even <code>X509Certificate<\/code>) are interested in signing,\nso ML-KEM subject keys inside a certificate can&#8217;t be used with things like TLS.\nCertificates with ML-DSA public keys, though, should generally work.\nThe two most prominent places are <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.security.sslstreamcertificatecontext\"><code>SslStreamCertificateContext<\/code><\/a> (and <code>SslStream<\/code> directly) and <code>SignedCms<\/code>&#8216; <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.security.cryptography.pkcs.cmssigner\"><code>CmsSigner<\/code><\/a> (which works either with attached keys, or detached keys, which is why it was also mentioned above).<\/p>\n<p>Sometimes layering requires explicit support at each layer for a new algorithm.\nFor example, Kestrel&#8217;s <code>CertificateConfigLoader<\/code> required <a href=\"https:\/\/github.com\/dotnet\/aspnetcore\/pull\/62866\">a change to support ML-DSA and SLH-DSA<\/a>.\nWe fixed everywhere that we noticed, but if we missed somewhere (for an algorithm that makes sense) we&#8217;ll (probably) fix it in a servicing update.<\/p>\n<p>For SslStream to work with ML-DSA or SLH-DSA certificates you need to be using TLS 1.3 (or a newer future version),\nand the OS needs to support it,\nand so does the other half of the connection.<\/p>\n<h2>Great, How Do I Get Started?<\/h2>\n<ul>\n<li>Go grab a version of <a href=\"https:\/\/dotnet.microsoft.com\/download\/dotnet\/10.0\">.NET 10<\/a><\/li>\n<li>Ensure you&#8217;re on a computer where the OS supports the algorithms.\n<ul>\n<li>We&#8217;ll tell you if yours is via <code>System.Security.Cryptography.MLDsa.IsSupported<\/code> (or similar for <code>MLKem<\/code>, <code>SlhDsa<\/code>, et al).<\/li>\n<li>For Linux, you need OpenSSL 3.5 or newer<\/li>\n<li>Windows support <a href=\"https:\/\/aka.ms\/PQCnowGAIgniteBlog\">arrived this month<\/a>, so if you&#8217;re running Windows 11 and have rebooted for Patch Tuesday, you should be good to go.<\/li>\n<\/ul>\n<\/li>\n<li>If you&#8217;re targeting .NET Standard 2.0, you will need to reference a 10.0 version of <a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.Bcl.Cryptography\/\">Microsoft.Bcl.Cryptography<\/a><\/li>\n<\/ul>\n<pre><code class=\"language-csharp\">using System.Security.Cryptography;\n\nif (!MLKem.IsSupported)\n{\n    Console.WriteLine(\"ML-KEM isn't supported :(\");\n    return;\n}\n\nMLKemAlgorithm alg = MLKemAlgorithm.MLKem768;\n\nusing (MLKem privateKey = MLKem.GenerateKey(alg))\nusing (MLKem publicKey = MLKem.ImportEncapsulationKey(alg, privateKey.ExportEncapsulationKey()))\n{\n    publicKey.Encapsulate(out byte[] ciphertext, out byte[] sharedSecret1);\n    byte[] sharedSecret2 = privateKey.Decapsulate(ciphertext);\n\n    if (sharedSecret1.AsSpan().SequenceEqual(sharedSecret2))\n    {\n        Console.WriteLine($\"Same answer, yay math! {Convert.ToHexString(sharedSecret1)}\");\n    }\n    else\n    {\n        Console.WriteLine(\"You just got the one in 2^165 failure. There's probably a prize for that.\");\n        Console.WriteLine($\"sharedSecret1: {Convert.ToHexString(sharedSecret1)}\");\n        Console.WriteLine($\"sharedSecret2: {Convert.ToHexString(sharedSecret2)}\");\n        Console.WriteLine($\"MLKEM768 seed: {Convert.ToHexString(privateKey.ExportPrivateSeed())}\");\n    }\n}<\/code><\/pre>\n<p>If you run into any surprises, <a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\">let us know<\/a>!<\/p>\n<h2>Special Thanks<\/h2>\n<p>As the saying goes, it takes a village (to raise a child).\nWe wouldn&#8217;t have made it as far as we did, at the quality we did, without help.<\/p>\n<ul>\n<li>GitHub Security Services: Participating in the class design journey, doing all of the work for ML-KEM, and setting up a private CI leg to get the project off to a good start.<\/li>\n<li>OpenSSL, Debian (13), CentOS (10): The timing of the OpenSSL 3.5 release, and how rapidly Debian and CentOS adopted it, meant we had stable CI coverage way earlier than we expected.<\/li>\n<li>Windows Cryptography: For putting PQC into the Windows Insider builds, and being responsive to our feedback.<\/li>\n<li>IETF LAMPS-WG: For quickly responding to our questions and feedback for the composite signatures project.<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>What we&#8217;ve added for PQC, and how we got there.<\/p>\n","protected":false},"author":23779,"featured_media":58922,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685,326],"tags":[7417,8090,8089],"class_list":["post-58921","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","category-security","tag-cryptography","tag-pqc","tag-quantum"],"acf":[],"blog_post_summary":"<p>What we&#8217;ve added for PQC, and how we got there.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/58921","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/users\/23779"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=58921"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/58921\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/58922"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=58921"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=58921"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=58921"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}