Deriving shared key using:
- C#
ECDiffieHellmanCng.DeriveKeyMaterial(ECDiffieHellmanPublicKey otherPartyPublicKey)
- Kotlin
KeyAgreement.generateSecret()
followed byKeyAgreement.doPhase(key: Key!, lastPhase: Boolean)
Yields different results using curve "secp384r1".
Kotlin related links point to Kotlin for Android docs due to readability.
Simplified driver code to demonstrate the problem, assuming that C# .NET 7.0.1 console application is "Server" and Kotlin OpenJDK 19.0.1 application is "Client":
C#:
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
var listener = new TcpListener(IPAddress.Any, 13000);
listener.Start();
using var client = await listener.AcceptTcpClientAsync();
var sharedKey = await GetSharedKey(client, CancellationToken.None);
async Task<byte[]> GetSharedKey(TcpClient client, CancellationToken token)
{
//Generate ECDH key pair using secp384r1 curve
var ecdh = new ECDiffieHellmanCng(ECCurve.CreateFromFriendlyName("secp384r1"));
var publicKeyBytes = ecdh.ExportSubjectPublicKeyInfo();
Console.WriteLine($"Server Public Key: {Convert.ToBase64String(publicKeyBytes)}, " +
$"Length: {publicKeyBytes.Length}");
//Send the generated public key encoded in X.509 to client.
var stream = client.GetStream();
await stream.WriteAsync(publicKeyBytes, token);
//Receive client's public key bytes (X.509 encoding).
var otherPublicKeyBytes = new byte[publicKeyBytes.Length];
await stream.ReadExactlyAsync(otherPublicKeyBytes, 0, otherPublicKeyBytes.Length, token);
//Decode client's public key bytes.
var otherEcdh = new ECDiffieHellmanCng(ECCurve.CreateFromFriendlyName("secp384r1"));
otherEcdh.ImportSubjectPublicKeyInfo(otherPublicKeyBytes, out _);
Console.WriteLine($"Client Public Key: {Convert.ToBase64String(otherEcdh.ExportSubjectPublicKeyInfo())}, " +
$"Length: {otherEcdh.ExportSubjectPublicKeyInfo().Length}");
//Derive shared key.
var sharedKey = ecdh.DeriveKeyMaterial(otherEcdh.PublicKey);
Console.WriteLine($"Shared key: {Convert.ToBase64String(sharedKey)}, " +
$"Length: {sharedKey.Length}");
return sharedKey;
}
Kotlin:
import java.net.Socket
import java.security.KeyFactory
import java.security.KeyPairGenerator
import java.security.spec.ECGenParameterSpec
import java.security.spec.X509EncodedKeySpec
import java.util.*
import javax.crypto.KeyAgreement
fun main(args: Array<String>) {
val socket = Socket("127.0.0.1", 13000)
val sharedKey = getSharedKey(socket)
}
private fun getSharedKey(socket: Socket): ByteArray {
//Generate ECDH key pair using secp384r1 curve
val keyGen = KeyPairGenerator.getInstance("EC")
keyGen.initialize(ECGenParameterSpec("secp384r1"))
val keyPair = keyGen.generateKeyPair()
println("Client Public Key: ${Base64.getEncoder().encodeToString(keyPair.public.encoded)}, Length: ${keyPair.public.encoded.size}")
//Receive server's public key bytes (encoded in X.509)
val input = socket.getInputStream()
val publicKeyBytes = input.readNBytes(keyPair.public.encoded.size)
//Send the generated public key encoded in X.509 to server
val output = socket.getOutputStream()
output.write(keyPair.public.encoded)
// Decode the server's public key
val keySpec = X509EncodedKeySpec(publicKeyBytes)
val keyFactory = KeyFactory.getInstance("EC")
val otherPublicKey = keyFactory.generatePublic(keySpec)
println("Server Public Key: ${Base64.getEncoder().encodeToString(otherPublicKey.encoded)}, Length: ${otherPublicKey.encoded.size}")
// Use KeyAgreement to generate the shared key
val keyAgreement = KeyAgreement.getInstance("ECDH")
keyAgreement.init(keyPair.private)
keyAgreement.doPhase(otherPublicKey, true)
val sharedKey = keyAgreement.generateSecret()
println("Shared key: ${Base64.getEncoder().encodeToString(sharedKey)}, Length: ${sharedKey.size}")
return sharedKey
}
C# output:
Server Public Key: MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEqza/eiK23hQIEW5mVdqOc0hAP3tPqittlcvPa6bGdyJK9n64sg0qYyDoPsxJ4pf7ROLz0ACrDS7n/e5Z0J1SMsWpBDViS8NRBvKwa1rQjWdFR0wzRaeVg09LIjnGs4Mj, Length: 120
Client Public Key: MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE30zvqkljT4STiE6XfLtoN147WRGA92rz9BLZfbRkOjz7uNbQ3az46DdoyQi6+eON7QVjIf2H5LKBANSk+C5zRX6u8jjrbhURDHYBKgijOddy6mOaEwiADijD/NX72O2L, Length: 120
Shared key: /u+tZYHar4MxXfrn2oqPZAqhiB2pkSTRBZ12rUxdnII=, Length: 32
Kotlin output:
Client Public Key: MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE30zvqkljT4STiE6XfLtoN147WRGA92rz9BLZfbRkOjz7uNbQ3az46DdoyQi6+eON7QVjIf2H5LKBANSk+C5zRX6u8jjrbhURDHYBKgijOddy6mOaEwiADijD/NX72O2L, Length: 120
Server Public Key: MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEqza/eiK23hQIEW5mVdqOc0hAP3tPqittlcvPa6bGdyJK9n64sg0qYyDoPsxJ4pf7ROLz0ACrDS7n/e5Z0J1SMsWpBDViS8NRBvKwa1rQjWdFR0wzRaeVg09LIjnGs4Mj, Length: 120
Shared key: lErK9DJAutaJ4af7EYWvtEXicAwfSuadtQhlZxug26wGkgB/ce7hF6ihLL87Sqc3, Length: 48
It seems there are no problems with public key import/export, but C# side fails to even produce key of correct length (384 / 8 = 48).
Edit: Somebody noticed that curiously enough C# "shared key" is Kotlin's shared key's SHA256 hash instead of the actual key.
I strongly suspect it's because of default key derivation function mismatch, but am not completely sure.
I would like to know what am I doing wrong and how to fix the issue.
Edit#2 - Solution: As the accepted answer suggests - my suspicion is not entirely wrong. ECDiffieHellmanCng.DeriveKeyMaterial does a bit extra unnecessary work - namely returning derived key's SHA256 hash (by default) instead of the actual key and does not provide any means of returning the actual key.
For anyone that is interested in getting 48 byte shared key you will have to be content with it's SHA384 (or some other hashing algorithm) hash instead (or use BouncyCastle):
C# changes:
//Generate ECDH key pair using secp384r1 curve and change default key's hashing algorithm SHA256 to SHA384
var ecdh = new ECDiffieHellmanCng(ECCurve.CreateFromFriendlyName("secp384r1"))
{
HashAlgorithm = CngAlgorithm.Sha384
};
Kotlin changes:
val sharedKey = keyAgreement.generateSecret()
val sharedKeyHash = MessageDigest.getInstance("SHA384").digest(sharedKey)
println("Shared key SHA384 hash: ${Base64.getEncoder().encodeToString(sharedKeyHash)}, Length: ${sharedKeyHash.size}")
return sharedKeyHash
I also suggest to rename the GetSharedKey method to what it actually is - GetSharedKeysSHA384Hash.