.NET’s ECDiffieHellman has no Curve25519. OneCrypto adds X25519 (RFC 7748): X25519PrivateKey.Generate() makes your key, and TryDeriveSharedSecret combines it with the other side’s public key. Here the other side is a key made by OpenSSL, and OpenSSL derives the same secret.
dotnet new console -o X25519Demo && cd X25519Demo dotnet add package RecursiveAI.OneCrypto --version 0.1.0-preview.1 openssl genpkey -algorithm x25519 -out bob.pem openssl pkey -in bob.pem -pubout -out bob-public.pem
OneCrypto is a preview, so dotnet add package needs --version or --prerelease. Put this in Program.cs and run dotnet run:
using System.Security.Cryptography; using System.Text; using RecursiveAI.OneCrypto; using RecursiveAI.OneCrypto.Pkcs; using RecursiveAI.OneCrypto.Primitives; // The peer's public key, from a PEM file using LoadedItems bobFile = Crypto.Load("bob-public.pem"); PublicKeyInfo bobInfo = bobFile.PublicKeys[0]; Console.WriteLine($"Peer key: {bobInfo.KeyAlgorithm}"); X25519PublicKey bob = X25519PublicKey.FromBytes(bobInfo.KeyBits.Span); // Our key pair; send our public key to the peer using X25519PrivateKey alice = X25519PrivateKey.Generate(); using (PrivateKeyInfo pkcs8 = PrivateKeyInfo.FromX25519(alice)) File.WriteAllText("alice-public.pem", PemEncoding.WriteString("PUBLIC KEY", pkcs8.GetPublicKey().Encoded.Span)); byte[] shared = new byte[X25519.KeySize]; if (!alice.TryDeriveSharedSecret(bob, shared)) throw new CryptographicException("Low-order peer key: abandon the exchange."); File.WriteAllBytes("shared-csharp.bin", shared); Console.WriteLine($"Shared: {shared.Length} bytes"); // Never use the raw secret as a key: derive one with HKDF (built into .NET) byte[] aesKey = HKDF.DeriveKey(HashAlgorithmName.SHA256, shared, 32, info: Encoding.UTF8.GetBytes("example app v1 AES-256-GCM key")); Console.WriteLine($"AES key: {aesKey.Length * 8} bits"); CryptographicOperations.ZeroMemory(shared);
Peer key: X25519 Shared: 32 bytes AES key: 256 bits
Both sides compute the same 32 bytes from their own private key and the other’s public key. TryDeriveSharedSecret returns false when the peer’s key has low order (the all-zero check of RFC 7748 §6.1); stop the exchange then. HKDF and AES-GCM come from .NET itself.
Check it with OpenSSL
OpenSSL takes Bob’s private key and Alice’s public key and must arrive at the same secret:
openssl pkeyutl -derive -inkey bob.pem -peerkey alice-public.pem \ -out shared-openssl.bin cmp shared-csharp.bin shared-openssl.bin && echo "Same 32-byte secret"
Same 32-byte secretLimits
- Not audited. This library has not been independently audited. It is a preview: 0.1.0-preview.1, and the API can change.
- Timing. The project’s timing tests cover X25519 only in “earlier, shorter runs”, and it makes “no constant-time claim … for key handling” yet. Details: Conformance.
- Key exchange only. X25519 gives a shared secret, not an authenticated channel: confirm the peer’s public key some other way, for example with a signature (Ed25519).
- Speed. Against BouncyCastle 2.7.0, X25519 agreement takes 0.76 of its time on x64 and 0.77 on arm64, from the project’s benchmark table. The x64 run is marked load-flagged.
X448 works the same way with X448PrivateKey. How the curves are implemented and checked: Ed25519, Ed448, X25519 and X448.