3

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.

sauza
  • 33
  • 4
  • You may want to file a bug against Mbed TLS because their example program is misleading. It's correct (since it only interoperates with itself, it can use a nonstandard format), but it's not very good as a sample program. – Gilles 'SO- stop being evil' Jan 04 '21 at 20:37
  • I fully agree that this behavior is really misleading. Indeed, this code from Google does some trickery to "fix" the endianness: https://github.com/google/eddystone/blob/bb8738d7ddac0ddd3dfa70e594d011a0475e763d/implementations/mbed/source/EIDFrame.cpp#L144 – sauza Jan 05 '21 at 00:26
  • In your revision, you seem to have used F# instead of C# for the .NET examples. It would be better if you would post the new code consistently in C# or at least replace the C# tag accordingly. On the .NET side, the keys are derived incorrectly. If the little endian order is also taken into account (see [answer from Gilles 'SO- stop being evil'](https://stackoverflow.com/a/65569434/9014097)), both codes return the same shared secret, for details see my answer. – Topaco Jan 05 '21 at 09:36

2 Answers2

3

Curve25519 represents keys in little-endian order. X25519 (ECDH with Curve25519) represents the shared secret in little-endian order. This is unlike most standard formats used in cryptography, in particular keys on SECP/NIST and Brainpool curves and the shared secret from ECDH with Weierstrass curves, which represent numbers in big-endian numbers. So change both calls to mbedtls_mpi_write_binary to mbedtls_mpi_write_binary_le.

Alternatively, use mbedtls_ecp_point_write_binary to export the public key and mbedtls_ecdh_calc_secret to calculate the shared secret: they take care of formatting the numbers with the correct endianness for each curve.

I haven't verified that this is the only problem.

Gilles 'SO- stop being evil'
  • 104,111
  • 38
  • 209
  • 254
  • I tried to use the *_le* version, but no luck so far. I updated the code to provide a simpler test case. – sauza Jan 05 '21 at 00:24
3

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.

Topaco
  • 40,594
  • 4
  • 35
  • 62