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

MbedTLS and .NET BouncyCastle interoperability issue with Curve25519

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

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

在 .NET 中实现

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

客户端密钥生成mbedtls 代码,大部分复制自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");
}

执行的输出是:

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,

服务器密钥生成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, 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}

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

客户端共享密钥生成mbedtls 代码)

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");
}

客户端执行的输出为:

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,

服务器共享密钥生成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, 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}

两次生成的密文明显不同。通过阅读各种示例,可能是由于不同的字节顺序编码。我尝试使用方法 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 导出 public 密钥并使用 mbedtls_ecdh_calc_secret 计算共享密钥:它们负责为每条曲线格式化数字,使其具有正确的字节顺序。

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

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

并且正如在 中已经指出的那样,必须考虑小端顺序,即在 generate_curve25519_keys() 中的 C/C++ 代码中 mbedtls_mpi_write_binary() 必须替换为 mbedtls_mpi_write_binary_le()。同样,在 generate_curve25519_shared_secret() 中,两个 mbedtls_mpi_read_binary() 都必须替换为 mbedtls_mpi_read_binary_le()

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