Background and motivation
The Proposal
.NET today does not in a consistent way expose DiffieHellman with Curve25519, or X25519. There have been a couple of requests for this in the past.
- Various SSH implementations such as Tmds.Ssh have asked for X25519 since this is a popular key agreement protocol. SSH uses RFC 7748
- X25519 is still relevant in a PQC context: X25519 is commonly used with ML-KEM in composite key agreement algorithms.
- XWing (Draft, but gaining popularity)
- Many SSH KEX algorithms that are PQC hybrids use X25519 (
sntrup761x25519-sha512, mlkem768x25519-sha256)
- Random social media posts over the years.
We should expose X25519DiffieHellman as specified by RFC 7748.
Considerations
-
Why not make this work in ECDiffieHellman?
-
ECDiffieHellman is complex in a way that is not helpful. It has a separate class for a public key, ECDiffieHellmanPublicKey, it has many different APIs for actually doing the key agreement. There are different Derive* methods that may run the key through a KDF.
-
X25519 uses a different key form. That makes it somewhat awkward to use with ECParameters. X25519 (because it is a Montgomery ladder) does not expose a "point" as a public key. It only exposes u, which the RFC says is a 32-byte string. The current API shape for ECDiffieHellman does not lend itself well to working with an X25519 key.
-
The MLKem proposal re-considered many things, eliminating unneeded APIs, ditching AsymmetricAlgorithm etc. We should use our new asymmetric algorithm shape that used for other PQC algorithms.
-
What platforms can we implement this in?
-
What about X448?
There is significantly less demand for X448. It does not appear frequently as a composite algorithm with PQC, and nor is it commonly used as a key agreement in protocols. Further, Only Android and OpenSSL support it.
-
What about PublicKey, X509Certificate2, etc?
X25519 in PKI is exceedingly rare. Windows, macOS, and Android only expose it as a primitive. Only OpenSSL lets you have the notion of a leaf certificate with an attached X25519 private key.
PublicKey can also work with any algorithm without APIs, so dedicated APIs are for convenience.
API Proposal
namespace System.Security.Cryptography;
public abstract partial class X25519DiffieHellman : IDisposable
{
// RFC 7748 Section 5:
// the inputs and outputs are 32-byte strings (for X25519)
public const int SecretAgreementSizeInBytes = 32;
public const int PrivateKeySizeInBytes = 32;
public const int PublicKeySizeInBytes = 32;
// Downlevel Windows and Android may return false
public static bool IsSupported { get; }
// No public instantiation, derived classes can.
protected X25519DiffieHellman();
public static X25519DiffieHellman GenerateKey();
public static X25519DiffieHellman ImportPublicKey(byte[] source);
public static X25519DiffieHellman ImportPublicKey(ReadOnlySpan<byte> source);
public static X25519DiffieHellman ImportPrivateKey(byte[] source);
public static X25519DiffieHellman ImportPrivateKey(ReadOnlySpan<byte> source);
public byte[] ExportPublicKey();
public void ExportPublicKey(Span<byte> destination);
protected abstract void ExportPublicKeyCore(Span<byte> destination);
public byte[] ExportPrivateKey();
public void ExportPrivateKey(Span<byte> destination);
protected abstract void ExportPrivateKeyCore(Span<byte> destination);
public byte[] DeriveRawSecretAgreement(X25519DiffieHellman otherParty);
public void DeriveRawSecretAgreement(X25519DiffieHellman otherParty, Span<byte> destination);
protected abstract void DeriveRawSecretAgreementCore(X25519DiffieHellman otherParty, Span<byte> destination);
public bool TryExportEncryptedPkcs8PrivateKey(ReadOnlySpan<byte> passwordBytes, PbeParameters pbeParameters, Span<byte> destination, out int bytesWritten);
public bool TryExportEncryptedPkcs8PrivateKey(ReadOnlySpan<char> password, PbeParameters pbeParameters, Span<byte> destination, out int bytesWritten);
public bool TryExportEncryptedPkcs8PrivateKey(string password, PbeParameters pbeParameters, Span<byte> destination, out int bytesWritten);
public byte[] ExportEncryptedPkcs8PrivateKey(ReadOnlySpan<byte> passwordBytes, PbeParameters pbeParameters);
public byte[] ExportEncryptedPkcs8PrivateKey(ReadOnlySpan<char> password, PbeParameters pbeParameters);
public byte[] ExportEncryptedPkcs8PrivateKey(string password, PbeParameters pbeParameters);
public string ExportEncryptedPkcs8PrivateKeyPem(ReadOnlySpan<byte> passwordBytes, PbeParameters pbeParameters);
public string ExportEncryptedPkcs8PrivateKeyPem(ReadOnlySpan<char> password, PbeParameters pbeParameters);
public string ExportEncryptedPkcs8PrivateKeyPem(string password, PbeParameters pbeParameters);
public byte[] ExportPkcs8PrivateKey();
public string ExportPkcs8PrivateKeyPem();
public bool TryExportPkcs8PrivateKey(Span<byte> destination, out int bytesWritten);
protected abstract bool TryExportPkcs8PrivateKeyCore(Span<byte> destination, out int bytesWritten);
public byte[] ExportSubjectPublicKeyInfo();
public bool TryExportSubjectPublicKeyInfo(Span<byte> destination, out int bytesWritten);
public string ExportSubjectPublicKeyInfoPem();
public static X25519DiffieHellman ImportEncryptedPkcs8PrivateKey(ReadOnlySpan<byte> passwordBytes, ReadOnlySpan<byte> source);
public static X25519DiffieHellman ImportEncryptedPkcs8PrivateKey(ReadOnlySpan<char> password, ReadOnlySpan<byte> source);
public static X25519DiffieHellman ImportEncryptedPkcs8PrivateKey(string password, byte[] source);
public static X25519DiffieHellman ImportFromEncryptedPem(ReadOnlySpan<char> source, ReadOnlySpan<byte> passwordBytes);
public static X25519DiffieHellman ImportFromEncryptedPem(string source, byte[] passwordBytes);
public static X25519DiffieHellman ImportFromEncryptedPem(ReadOnlySpan<char> source, ReadOnlySpan<char> password);
public static X25519DiffieHellman ImportFromEncryptedPem(string source, string password);
public static X25519DiffieHellman ImportFromPem(ReadOnlySpan<char> source);
public static X25519DiffieHellman ImportFromPem(string source);
public static X25519DiffieHellman ImportPkcs8PrivateKey(byte[] source);
public static X25519DiffieHellman ImportPkcs8PrivateKey(ReadOnlySpan<byte> source);
public static X25519DiffieHellman ImportSubjectPublicKeyInfo(byte[] source);
public static X25519DiffieHellman ImportSubjectPublicKeyInfo(ReadOnlySpan<byte> source);
public void Dispose();
protected virtual void Dispose(bool disposing);
}
public sealed class X25519DiffieHellmanOpenSsl : X25519DiffieHellman
{
public X25519DiffieHellmanOpenSsl(SafeEvpPKeyHandle pkeyHandle);
public SafeEvpPKeyHandle DuplicateKeyHandle();
}
// Consider:
// We should not add CngAlgorithm or related identifiers.
public sealed class X25519DiffieHellmanCng : X25519DiffieHellman
{
[SupportedOSPlatformAttribute("windows")]
public X25519DiffieHellmanCng(CngKey key);
public CngKey GetKey();
}
API Usage
using X25519DiffieHellman vince = X25519DiffieHellman.GenerateKey();
using X25519DiffieHellman penny = X25519DiffieHellman.GenerateKey();
byte[] key1 = PerformSecretAgreement(vince, penny.ExportPublicKey());
byte[] key2 = PerformSecretAgreement(penny, vince.ExportPublicKey());
static byte[] PerformSecretAgreement(X25519DiffieHellman xdh, byte[] otherPartyPublicKey)
{
using X25519DiffieHellman otherParty = X25519DiffieHellman.ImportPublicKey(otherPartyPublicKey);
return xdh.DeriveRawSecretAgreement(otherParty);
}
Alternative Designs
No response
Risks
No response
Background and motivation
The Proposal
.NET today does not in a consistent way expose DiffieHellman with Curve25519, or X25519. There have been a couple of requests for this in the past.
sntrup761x25519-sha512,mlkem768x25519-sha256)We should expose
X25519DiffieHellmanas specified by RFC 7748.Considerations
Why not make this work in
ECDiffieHellman?ECDiffieHellmanis complex in a way that is not helpful. It has a separate class for a public key,ECDiffieHellmanPublicKey, it has many different APIs for actually doing the key agreement. There are differentDerive*methods that may run the key through a KDF.X25519 uses a different key form. That makes it somewhat awkward to use with
ECParameters. X25519 (because it is a Montgomery ladder) does not expose a "point" as a public key. It only exposesu, which the RFC says is a 32-byte string. The current API shape for ECDiffieHellman does not lend itself well to working with an X25519 key.The
MLKemproposal re-considered many things, eliminating unneeded APIs, ditchingAsymmetricAlgorithmetc. We should use our new asymmetric algorithm shape that used for other PQC algorithms.What platforms can we implement this in?
(Unconfirmed, only looked at documentation so far)Confirmed everything works E2E on Android.What about X448?
There is significantly less demand for X448. It does not appear frequently as a composite algorithm with PQC, and nor is it commonly used as a key agreement in protocols. Further, Only Android and OpenSSL support it.
What about
PublicKey,X509Certificate2, etc?X25519 in PKI is exceedingly rare. Windows, macOS, and Android only expose it as a primitive. Only OpenSSL lets you have the notion of a leaf certificate with an attached X25519 private key.
PublicKeycan also work with any algorithm without APIs, so dedicated APIs are for convenience.API Proposal
API Usage
Alternative Designs
No response
Risks
No response