MbedTLS 和 .NET BouncyCastle 与 Curve25519 的互操作性问题

问题描述

编辑:更改代码以提供更简单的测试用例

我正在创建一个简单的客户端/服务器应用程序,它使用 Curve25519 进行密钥交换。客户端使用 mbedtls 用 C 语言实现,服务器使用 BouncyCastle 用 .NET 实现。

不幸的是,生成的共享密钥在客户端和服务器上并不相同。下面显示生成公钥/私钥的代码摘录(我硬编码了一些值以方便调试)。

客户端密钥生成mbedtls 代码,大部分复制自 https://github.com/ARMmbed/mbedtls/blob/development/programs/pkey/ecdh_curve25519.chttps://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
    );
        
    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");
}

执行的输出为:

Pub: 0x36,0x4B,0x8E,0x89,0x31,0x18,0xA4,0x32,0xE3,0x5B,0xB1,0x70,0x69,0x55,0xFE,0x42,0x8C,0x48,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,0xA0,0x47,0x64,0x14,0xC7,0xD8,0x90,0xFC,0xCD,0x11,0xC3,0x37,0xFB,0xB0,

服务器密钥生成BouncyCastle 代码

// 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"))

执行的输出为:

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

根据这些信息,我继续使用以下代码生成共享密钥:

客户端共享秘密生成mbedtls 代码

void generate_curve25519_shared_secret() {
    uint8_t my_privkey[] =      { 0x6E,0xB0 };
    uint8_t server_pubkey[] =   { 0x30,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,0
    );

    mbedtls_ecp_group_load(&ctx.grp,MbedTLS_ECP_DP_CURVE25519);    
    
    // read my private key
    mbedtls_mpi_read_binary(&ctx.d,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
    );

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

在客户端执行的输出为:

Secret :0x3D,0x52,0x28,0xA9,0x45,0xAC,0x7E,0x84,0x25,0x57,

服务器共享密钥生成BouncyCastle 代码

// 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"))

在服务器上执行的输出为:

SECRET: {0xE2,0xC6,0x3A,0x75,0x83,0x60,0xB8,0xE1,0xD6,0x24,0x3C,0x96,0xC4,0x10,0x7C,0x77,0xC0,0xD1,0x77}

生成的两个秘密明显不同。通过阅读各种示例,可能是由于不同的字节顺序编码。我尝试使用 mbedtls_mpi_read_binary_lembedtls_mpi_write_binary_le 方法,但没有任何运气。

作为一种替代解决方案,我可以更改 .NET 库并转移到另一个库,如果此更改可以解决问题。不幸的是,此时我无法找到一个好的 .NET 替代方案。

解决方法

Curve25519 表示按小端顺序排列的键。 X25519(带有 Curve25519 的 ECDH)以小端顺序表示共享秘密。这与密码学中使用的大多数标准格式不同,特别是 SECP/NIST 和 Brainpool 曲线上的密钥以及来自 ECDH 的共享秘密与 Weierstrass 曲线,这些曲线表示大端数字中的数字。因此,将两个调用 mbedtls_mpi_write_binary 更改为 mbedtls_mpi_write_binary_le

或者,使用 mbedtls_ecp_point_write_binary 导出公钥并使用 mbedtls_ecdh_calc_secret 计算共享密钥:它们负责使用每条曲线的正确字节顺序格式化数字。

我还没有确认这是唯一的问题。

,

在 .NET 端,密钥对生成正确,但公钥和私钥派生不正确。它们必须按如下方式确定(在 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"));

正如在 answer from Gilles 'SO- stop being evil' 中已经指出的那样,必须考虑小端顺序,即在 generate_curve25519_keys() 的 C/C++ 代码中,mbedtls_mpi_write_binary() 都必须替换为 {{1 }}。同样,在 mbedtls_mpi_write_binary_le() 中,generate_curve25519_shared_secret() 都必须替换为 mbedtls_mpi_read_binary()

通过这些更改,两个代码在我的机器上生成相同的共享密钥。