---
title: "X25519 key exchange in C#"
url: https://recursiveai.org/onecrypto/cookbook/x25519-key-exchange/
description: "X25519 key exchange in C# on .NET 10, which has no Curve25519 of its own: agree on a shared secret with a peer’s public key, turn it into an AES key with HKDF, and check the secret against OpenSSL. Runnable code with OneCrypto 0.1.0-preview.1."
version: "0.1.0-preview.1"
---

# X25519 key exchange in C#

.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.

```shell
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`:

```csharp
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);
```

```text
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:

```shell
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"
```

```text
Same 32-byte secret
```

## Limits

- **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](https://recursiveai.org/onecrypto/docs/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](https://recursiveai.org/onecrypto/cookbook/ed25519-sign-verify/)).
- **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](https://recursiveai.org/onecrypto/docs/curves/).
