cf#cryptographybouncycastlembedtls

MbedTLS and .NET BouncyCastle interoperability issue with Curve25519


EDIT: Code changed to provide a simpler test case

I'm creating a simple client/server application that uses Curve25519 for key exchange. The client is implemented in C with mbedtls and the server is implemented in .NET with BouncyCastle.

Unfortunately, the generated shared secret is not the same on the client and on the server. Below is shown an excerpt of the code that generates the public/private keys (I hardcoded some value to easy the debugging).

Client Keys Generation (mbedtls code, mostly copied from https://github.com/ARMmbed/mbedtls/blob/development/programs/pkey/ecdh_curve25519.c and https://github.com/google/eddystone/blob/bb8738d7ddac0ddd3dfa70e594d011a0475e763d/implementations/mbed/source/EIDFrame.cpp#L144)

void generate_curve25519_keys() {
    uint8_t my_pubkey[32] = { 0 };
    uint8_t my_privkey[32] = { 0 };

    mbedtls_ecdh_context ctx;
    mbedtls_entropy_context entropy;
    mbedtls_ctr_drbg_context ctr_drbg;

    // generate the keys and save to buffer
    mbedtls_ctr_drbg_init(&ctr_drbg);
    mbedtls_entropy_init(&entropy);

    mbedtls_ecdh_init(&ctx);
    mbedtls_ctr_drbg_seed(
        &ctr_drbg, 
        mbedtls_entropy_func, 
        &entropy, 
        0, 
        0
    );
        
    mbedtls_ecp_group_load(&ctx.grp, MBEDTLS_ECP_DP_CURVE25519);        
    mbedtls_ecdh_gen_public(
        &ctx.grp, 
        &ctx.d, 
        &ctx.Q,
        mbedtls_ctr_drbg_random, 
        &ctr_drbg
    );

    mbedtls_mpi_write_binary(&ctx.Q.X, my_pubkey, sizeof(my_pubkey));   
    printf("Pub: ");
    for (size_t i = 0; i < sizeof(my_pubkey); i++)
        printf("0x%02X, ", my_pubkey[i]);
    printf("\n");
        
    mbedtls_mpi_write_binary(&ctx.d, my_privkey, sizeof(my_privkey));
    printf("Priv: ");
    for (size_t i = 0; i < sizeof(my_privkey); i++)
        printf("0x%02X, ", my_privkey[i]);
    printf("\n");
}

The output of the execution is:

Pub: 0x36, 0x4B, 0x8E, 0x89, 0x31, 0x18, 0xA4, 0x32, 0xE3, 0x5B, 0xB1, 0x70, 0x69, 0x55, 0xFE, 0x42, 0x8C, 0x48, 0x8C, 0xC9, 0x0E, 0x2C, 0xA2, 0x1A, 0x66, 0x6A, 0x26, 0x7B, 0xD0, 0xDA, 0x88, 0x5C,
Priv: 0x6E, 0xCF, 0x6C, 0xBD, 0x9C, 0xDE, 0xDC, 0xBF, 0xD3, 0xB3, 0x82, 0x9A, 0x7D, 0xA7, 0x27, 0x50, 0xA2, 0xA0, 0x47, 0x64, 0x14, 0xC7, 0xD8, 0x90, 0xFC, 0xCD, 0x11, 0xC3, 0x5C, 0x37, 0xFB, 0xB0,

Server Key Generation (BouncyCastle code)

// generate public and private key
let keyGenerator = new X25519KeyPairGenerator()
keyGenerator.Init(new X25519KeyGenerationParameters(new SecureRandom()))
let keys = keyGenerator.GenerateKeyPair()

let publicKey = SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(keys.Public)
let x25519PublicKey = new X25519PublicKeyParameters(publicKey.GetEncoded(), 0)
Console.WriteLine("PUB: {{0x{0}}}", BitConverter.ToString(x25519PublicKey.GetEncoded()).Replace("-", ", 0x"))

let privateKey = ECPrivateKeyStructure.GetInstance(PrivateKeyInfoFactory.CreatePrivateKeyInfo(keys.Private))
let x25519PrivateKey = new X25519PrivateKeyParameters(privateKey.GetEncoded(), 0)
Console.WriteLine("PRIV: {{0x{0}}}", BitConverter.ToString(x25519PrivateKey.GetEncoded()).Replace("-", ", 0x"))

The output of the execution is:

PUB: {0x30, 0x2A, 0x30, 0x05, 0x06, 0x03, 0x2B, 0x65, 0x6E, 0x03, 0x21, 0x00, 0xD2, 0x86, 0xF9, 0x67, 0xAB, 0xF8, 0x8C, 0x8C, 0x4E, 0xC9, 0xF9, 0xFC, 0x29, 0xE2, 0xC2, 0xD2, 0x3B, 0x8E, 0x1E, 0x3D}
PRIV: {0x30, 0x51, 0x02, 0x01, 0x01, 0x30, 0x05, 0x06, 0x03, 0x2B, 0x65, 0x6E, 0x04, 0x22, 0x04, 0x20, 0x78, 0xF3, 0xC9, 0xBE, 0xB5, 0x74, 0x5A, 0x63, 0x99, 0x5C, 0xCB, 0x82, 0xD7, 0x0C, 0xBC, 0x37}

With this information I proceed to generate the shared key with the following code:

Client Shared Secret Generation (mbedtls code)

void generate_curve25519_shared_secret() {
    uint8_t my_privkey[] =      { 0x6E, 0xCF, 0x6C, 0xBD, 0x9C, 0xDE, 0xDC, 0xBF, 0xD3, 0xB3, 0x82, 0x9A, 0x7D, 0xA7, 0x27, 0x50, 0xA2, 0xA0, 0x47, 0x64, 0x14, 0xC7, 0xD8, 0x90, 0xFC, 0xCD, 0x11, 0xC3, 0x5C, 0x37, 0xFB, 0xB0 };
    uint8_t server_pubkey[] =   { 0x30, 0x2A, 0x30, 0x05, 0x06, 0x03, 0x2B, 0x65, 0x6E, 0x03, 0x21, 0x00, 0xD2, 0x86, 0xF9, 0x67, 0xAB, 0xF8, 0x8C, 0x8C, 0x4E, 0xC9, 0xF9, 0xFC, 0x29, 0xE2, 0xC2, 0xD2, 0x3B, 0x8E, 0x1E, 0x3D };
    uint8_t shared_secret[32] = { 0 };

    mbedtls_ecdh_context ctx;
    mbedtls_entropy_context entropy;
    mbedtls_ctr_drbg_context ctr_drbg;

    // generate the keys and save to buffer
    mbedtls_ctr_drbg_init(&ctr_drbg);
    mbedtls_entropy_init(&entropy);

    mbedtls_ecdh_init(&ctx);
    mbedtls_ctr_drbg_seed(
        &ctr_drbg,
        mbedtls_entropy_func,
        &entropy,
        0,
        0
    );

    mbedtls_ecp_group_load(&ctx.grp, MBEDTLS_ECP_DP_CURVE25519);    
    
    // read my private key
    mbedtls_mpi_read_binary(&ctx.d, my_privkey, sizeof(my_privkey));
    mbedtls_mpi_lset(&ctx.Qp.Z, 1);
    
    // read server key
    mbedtls_mpi_read_binary(&ctx.Qp.X, server_pubkey, sizeof(server_pubkey));

    // generate shared secret
    size_t olen;
    mbedtls_ecdh_calc_secret(
        &ctx,
        &olen,
        shared_secret,
        32,
        0,
        0
    );

    printf("Secret :");
    for (size_t i = 0; i < sizeof(shared_secret); i++)
        printf("0x%02X, ", shared_secret[i]);
    printf("\n");
}

The output of the execution on the client is:

Secret :0x3D, 0xF3, 0xD3, 0x88, 0xAB, 0xD7, 0x31, 0xA4, 0x1E, 0x52, 0xFB, 0x9A, 0x28, 0x82, 0xBF, 0x9C, 0xA9, 0x45, 0xB0, 0x6C, 0xC7, 0xD7, 0x20, 0xAC, 0x7E, 0xCB, 0x51, 0x50, 0x84, 0x2C, 0x25, 0x57,

Server Shared Secret Generation (BouncyCastle code)

// compute shared secret
let rawAgentPubKey = [|0x36uy; 0x4Buy; 0x8Euy; 0x89uy; 0x31uy; 0x18uy; 0xA4uy; 0x32uy; 0xE3uy; 0x5Buy; 0xB1uy; 0x70uy; 0x69uy; 0x55uy; 0xFEuy; 0x42uy; 0x8Cuy; 0x48uy; 0x8Cuy; 0xC9uy; 0x0Euy; 0x2Cuy; 0xA2uy; 0x1Auy; 0x66uy; 0x6Auy; 0x26uy; 0x7Buy; 0xD0uy; 0xDAuy; 0x88uy; 0x5Cuy|]
        let rawPrivKey =     [|0x30uy; 0x51uy; 0x02uy; 0x01uy; 0x01uy; 0x30uy; 0x05uy; 0x06uy; 0x03uy; 0x2Buy; 0x65uy; 0x6Euy; 0x04uy; 0x22uy; 0x04uy; 0x20uy; 0x78uy; 0xF3uy; 0xC9uy; 0xBEuy; 0xB5uy; 0x74uy; 0x5Auy; 0x63uy; 0x99uy; 0x5Cuy; 0xCBuy; 0x82uy; 0xD7uy; 0x0Cuy; 0xBCuy; 0x37uy|]
let agentPubKey = new X25519PublicKeyParameters(rawAgentPubKey, 0)
let secret = Array.zeroCreate<Byte>(32)        
let privateKey = new X25519PrivateKeyParameters(rawPrivKey, 0)
privateKey.GenerateSecret(agentPubKey, secret, 0)
Console.WriteLine("SECRET: {{0x{0}}}", BitConverter.ToString(secret).Replace("-", ", 0x"))

The output of the execution on the server is:

SECRET: {0xE2, 0x2B, 0xC6, 0x3A, 0xA0, 0x75, 0x83, 0x60, 0xB8, 0xE1, 0x47, 0xD6, 0x66, 0x24, 0x14, 0xC2, 0x99, 0x51, 0x05, 0x3C, 0xDC, 0x96, 0x2B, 0xC4, 0xE2, 0x10, 0x7C, 0x77, 0xC0, 0xA2, 0xD1, 0x77}

The two generated secrets are clearly different. By reading various examples it might due to a different byte order encoding. I tried to use the methods mbedtls_mpi_read_binary_le and mbedtls_mpi_write_binary_le, but without any luck.

As an alternative solution, I can change the .NET library and move to another one if this change can solve the problem. Unfortunately, at this time I wasn't able to find a good .NET alternative.


Solution

  • On the .NET side, the key pair is generated correctly, but the public and private keys are derived incorrectly. They must be determined as follows (in C#):

    // generate public and private key
    var keyGenerator = new X25519KeyPairGenerator();
    keyGenerator.Init(new X25519KeyGenerationParameters(new SecureRandom()));
    var keys = keyGenerator.GenerateKeyPair();
    
    var publicKey = (X25519PublicKeyParameters)keys.Public;
    Console.WriteLine("PUB: {{0x{0}}}", BitConverter.ToString(publicKey.GetEncoded()).Replace("-", ", 0x"));
    
    var privateKey = (X25519PrivateKeyParameters)keys.Private;
    Console.WriteLine("PRIV: {{0x{0}}}", BitConverter.ToString(privateKey.GetEncoded()).Replace("-", ", 0x"));
    

    And as already pointed out in the answer from Gilles 'SO- stop being evil', the little endian order must be considered, i.e. in the C/C++ code in generate_curve25519_keys() both mbedtls_mpi_write_binary() must be replaced by mbedtls_mpi_write_binary_le(). Similarly in generate_curve25519_shared_secret() both mbedtls_mpi_read_binary() must be replaced by mbedtls_mpi_read_binary_le().

    With these changes, both codes generate the same shared secret on my machine.