#:package NSec.Cryptography@26.4.0using System.Security.Cryptography;using System.Text;using NSec.Cryptography; byte[] info = Encoding.UTF8.GetBytes("AES-256-GCM key v1"); // P-256: built into System.Security.Cryptographyusing var alice = ECDiffieHellman.Create();alice.ImportFromPem(File.ReadAllText("alice-p256.pem"));using var bob = ECDiffieHellman.Create();bob.ImportFromPem(File.ReadAllText("bob-p256.pub.pem")); // public key only byte[] p256Secret = alice.DeriveRawSecretAgreement(bob.PublicKey); // .NET 8+byte[] p256Key = HKDF.DeriveKey(HashAlgorithmName.SHA256, p256Secret, 32, salt: [], info: info);// X25519: not in .NET 10's System.Security.Cryptography; NSec (libsodium) has itvar x25519 = KeyAgreementAlgorithm.X25519;using var aliceX = NSec.Cryptography.Key.Import(x25519, File.ReadAllBytes("alice-x25519.pem"), KeyBlobFormat.PkixPrivateKeyText);var bobX = NSec.Cryptography.PublicKey.Import(x25519, File.ReadAllBytes("bob-x25519.pub.pem"), KeyBlobFormat.PkixPublicKeyText); // Agree returns null when the result is the all-zero value (a malicious or broken peer key)using var secret = x25519.Agree(aliceX, bobX, new SharedSecretCreationParameters { ExportPolicy = KeyExportPolicies.AllowPlaintextExport }) ?? throw new CryptographicException("X25519 agreement failed");byte[] x25519Secret = secret.Export(SharedSecretBlobFormat.RawSharedSecret); // only to print itbyte[] x25519Key = KeyDerivationAlgorithm.HkdfSha256.DeriveBytes(secret, ReadOnlySpan<byte>.Empty, info, 32); Console.WriteLine($"X25519 shared secret: {Convert.ToHexStringLower(x25519Secret)}");Console.WriteLine($"X25519 derived key: {Convert.ToHexStringLower(x25519Key)}");Console.WriteLine($"P-256 shared secret: {Convert.ToHexStringLower(p256Secret)}");Console.WriteLine($"P-256 derived key: {Convert.ToHexStringLower(p256Key)}");How it works
ECDiffieHellman.Create()plusImportFromPemloads either the private key or, for the peer, just the public key.DeriveRawSecretAgreement(peer.PublicKey)(.NET 8+) returns the raw shared secret, andHKDF.DeriveKey(HashAlgorithmName.SHA256, secret, 32, salt, info)derives the key.- With NSec,
KeyAgreementAlgorithm.X25519.Agreereturns aSharedSecretthat stays in unmanaged memory;KeyDerivationAlgorithm.HkdfSha256.DeriveBytesderives the key from it directly. - The secret is exported here only so the output can be compared with the RFCs; production code passes the
SharedSecretstraight to the KDF.
Gotchas
DeriveKeyMaterial(peer.PublicKey)does not return the shared secret: by default it returns SHA-256 of it (0519dc09…instead ofd6840f6b…for the RFC 5903 keys). Other platforms will not derive the same key — useDeriveRawSecretAgreementwith HKDF.- NSec's
Agreereturnsnullwhen the result is all zeros, which is what a malicious low-order X25519 key produces. The?? throwis the check; without it, the next line throws aNullReferenceException. - A
SharedSecretcannot be exported by default (InvalidOperationException: The key cannot be exported.); the export policy inSharedSecretCreationParametersallows it. ECDiffieHellmanis a front end for the platform's cryptography: OpenSSL on Linux (3.0.13 in the image used here), CNG on Windows, Apple's frameworks on macOS. The derived bytes are the same everywhere; exception messages are not, so do not match on them.