Search code examples
rustecdhsecp256k1

Rust ECDH does not produce the same shared secret as NodeJS/Javascript and C implementations


I have ECDH shared secrets (sec1 and sec2, below) working in NodeJS/Javascript:

let sk1 = crypto.createECDH('secp256k1')
sk1.setPrivateKey(Buffer.from("71179b991d7693de813aeaa5bfa241a2ac9e0535867ebf8f6b1b0884472ef4a7", "hex"));
let pk1 = crypto.createECDH('secp256k1')
pk1.setPublicKey(Buffer.from("02de4cba976ab77795c46c1c3b95afc077b17afe1bca02d28963a3bcdd9c082168", "hex"));
let sk2 = crypto.createECDH('secp256k1')
sk2.setPrivateKey(Buffer.from("1e114f237e3c59ba2b92aedf213f1127c9169c039752495c1ffb649cb9b90598", "hex"));
let pk2 = crypto.createECDH('secp256k1')
pk2.setPublicKey(Buffer.from("033415a4e45739e8f003450392793a15d3ad6cb49ff5b1943695f2cea92703aa64", "hex"));

console.log('sk1', sk1.getPrivateKey());
console.log('pk1', pk1.getPublicKey('hex', 'compressed'));
console.log('sk2', sk2.getPrivateKey());
console.log('pk2', pk2.getPublicKey('hex', 'compressed'));

let sec1 = sk2.computeSecret(pk1.getPublicKey());
let sec2 = sk1.computeSecret(pk2.getPublicKey());

console.log('sec1', sec1);
console.log('sec2', sec2);

assert.equal(sec1, sec2);

which produces:

sk1 <Buffer 71 17 9b 99 1d 76 93 de 81 3a ea a5 bf a2 41 a2 ac 9e 05 35 86 7e bf 8f 6b 1b 08 84 47 2e f4 a7>
pk1 02de4cba976ab77795c46c1c3b95afc077b17afe1bca02d28963a3bcdd9c082168
sk2 <Buffer 1e 11 4f 23 7e 3c 59 ba 2b 92 ae df 21 3f 11 27 c9 16 9c 03 97 52 49 5c 1f fb 64 9c b9 b9 05 98>
pk2 033415a4e45739e8f003450392793a15d3ad6cb49ff5b1943695f2cea92703aa64
sec1 <Buffer 23 55 7d 44 6a 48 23 d6 a3 2f b0 87 58 82 26 d1 e8 ef 4f 6b 7b 6d 26 09 13 13 84 0a 74 ed 0b 4d>
sec2 <Buffer 23 55 7d 44 6a 48 23 d6 a3 2f b0 87 58 82 26 d1 e8 ef 4f 6b 7b 6d 26 09 13 13 84 0a 74 ed 0b 4d>

I'm trying to do the same in Rust. The Rust code's shared secrets match each other, but do not match the shared secrets I get from NodeJS/Javascript (and C).

From Rust, I get:

running 1 test
sk1: [71, 17, 9b, 99, 1d, 76, 93, de, 81, 3a, ea, a5, bf, a2, 41, a2, ac, 9e, 5, 35, 86, 7e, bf, 8f, 6b, 1b, 8, 84, 47, 2e, f4, a7]
pk1: [2, de, 4c, ba, 97, 6a, b7, 77, 95, c4, 6c, 1c, 3b, 95, af, c0, 77, b1, 7a, fe, 1b, ca, 2, d2, 89, 63, a3, bc, dd, 9c, 8, 21, 68]
sk2: [1e, 11, 4f, 23, 7e, 3c, 59, ba, 2b, 92, ae, df, 21, 3f, 11, 27, c9, 16, 9c, 3, 97, 52, 49, 5c, 1f, fb, 64, 9c, b9, b9, 5, 98]
pk2: [3, 34, 15, a4, e4, 57, 39, e8, f0, 3, 45, 3, 92, 79, 3a, 15, d3, ad, 6c, b4, 9f, f5, b1, 94, 36, 95, f2, ce, a9, 27, 3, aa, 64]
sec1: [45, b7, 3c, 8a, ac, 8b, cf, 65, 1d, ad, 11, f7, f0, 4f, 63, b9, f0, 34, 86, d0, 28, ab, 4d, 5c, 52, bd, d5, d6, 92, d7, c2, aa]
sec2: [45, b7, 3c, 8a, ac, 8b, cf, 65, 1d, ad, 11, f7, f0, 4f, 63, b9, f0, 34, 86, d0, 28, ab, 4d, 5c, 52, bd, d5, d6, 92, d7, c2, aa]
test ecies::tests::test_shared_secret ... ok

using the code:

#[test]
fn test_shared_secret() {
    let sk1 = secp256k1::SecretKey::from_slice(&hex!("71179b991d7693de813aeaa5bfa241a2ac9e0535867ebf8f6b1b0884472ef4a7")).expect("bad key");
    let pk1 = secp256k1::PublicKey::from_slice(&hex!("02de4cba976ab77795c46c1c3b95afc077b17afe1bca02d28963a3bcdd9c082168")).expect("bad key");
    let sk2 = secp256k1::SecretKey::from_slice(&hex!("1e114f237e3c59ba2b92aedf213f1127c9169c039752495c1ffb649cb9b90598")).expect("bad key");
    let pk2 = secp256k1::PublicKey::from_slice(&hex!("033415a4e45739e8f003450392793a15d3ad6cb49ff5b1943695f2cea92703aa64")).expect("bad key");

    println!("sk1: {:x?}", sk1.secret_bytes());
    println!("pk1: {:x?}", pk1.serialize());
    println!("sk2: {:x?}", sk2.secret_bytes());
    println!("pk2: {:x?}", pk2.serialize());

    let sec1 = secp256k1::ecdh::SharedSecret::new(&pk2, &sk1);
    let sec2 = secp256k1::ecdh::SharedSecret::new(&pk1, &sk2);

    println!("sec1: {:x?}", sec1.secret_bytes());
    println!("sec2: {:x?}", sec2.secret_bytes());
    assert_eq!(sec1, sec2);
}

How do I make the ECDH shared secrets, calculated in Rust, match those I already have working in NodeJS/Javascript (and C)?


Update, after reading @Topaco's answer.

I changed the Rust code to:

    let sec1 = secp256k1::ecdh::shared_secret_point(&pk2, &sk1);
    let sec2 = secp256k1::ecdh::shared_secret_point(&pk1, &sk2);

    println!("sec1: {:x?}", sec1);
    println!("sec2: {:x?}", sec2);

which now produced:

sk1: [71, 17, 9b, 99, 1d, 76, 93, de, 81, 3a, ea, a5, bf, a2, 41, a2, ac, 9e, 5, 35, 86, 7e, bf, 8f, 6b, 1b, 8, 84, 47, 2e, f4, a7]
pk1: [2, de, 4c, ba, 97, 6a, b7, 77, 95, c4, 6c, 1c, 3b, 95, af, c0, 77, b1, 7a, fe, 1b, ca, 2, d2, 89, 63, a3, bc, dd, 9c, 8, 21, 68]
sk2: [1e, 11, 4f, 23, 7e, 3c, 59, ba, 2b, 92, ae, df, 21, 3f, 11, 27, c9, 16, 9c, 3, 97, 52, 49, 5c, 1f, fb, 64, 9c, b9, b9, 5, 98]
pk2: [3, 34, 15, a4, e4, 57, 39, e8, f0, 3, 45, 3, 92, 79, 3a, 15, d3, ad, 6c, b4, 9f, f5, b1, 94, 36, 95, f2, ce, a9, 27, 3, aa, 64]
sec1: [23, 55, 7d, 44, 6a, 48, 23, d6, a3, 2f, b0, 87, 58, 82, 26, d1, e8, ef, 4f, 6b, 7b, 6d, 26, 9, 13, 13, 84, a, 74, ed, b, 4d, a4, 8e, f4, 7b, 67, 68, 61, 2c, 33, d1, 1e, e7, 8e, 80, 4a, 71, ef, 79, dd, 3c, b0, 18, 15, f5, b3, 80, 3f, 93, e1, 9e, 64, 4e]
sec2: [23, 55, 7d, 44, 6a, 48, 23, d6, a3, 2f, b0, 87, 58, 82, 26, d1, e8, ef, 4f, 6b, 7b, 6d, 26, 9, 13, 13, 84, a, 74, ed, b, 4d, a4, 8e, f4, 7b, 67, 68, 61, 2c, 33, d1, 1e, e7, 8e, 80, 4a, 71, ef, 79, dd, 3c, b0, 18, 15, f5, b3, 80, 3f, 93, e1, 9e, 64, 4e]

NodeJS's:

sec1 <Buffer 23 55 7d 44 6a 48 23 d6 a3 2f b0 87 58 82 26 d1 e8 ef 4f 6b 7b 6d 26 09 13 13 84 0a 74 ed 0b 4d>
sec2 <Buffer 23 55 7d 44 6a 48 23 d6 a3 2f b0 87 58 82 26 d1 e8 ef 4f 6b 7b 6d 26 09 13 13 84 0a 74 ed 0b 4d>

The Rust shared secret is 64 bytes long, but the NodeJS shared secret is only the first 32 bytes of that. As they share the first 32 bytes (probably the X-coordinated) and that is all I need, it works for me.


Solution

  • The shared secret is the x coordinate of the EC point (x, y) resulting from the multiplication of the private and public key. For secp256k1, x and y are each 32 bytes in size. With Rust, the shared secret can be determined as follows:

    ...
    let ec_point_1 = secp256k1::ecdh::shared_secret_point(&pk2, &sk1);
    let sec1 = &ec_point_1[0..32];
    println!("sec1: {:02x?}", sec1); // sec1: [23, 55, 7d, 44, 6a, 48, 23, d6, a3, 2f, b0, 87, 58, 82, 26, d1, e8, ef, 4f, 6b, 7b, 6d, 26, 09, 13, 13, 84, 0a, 74, ed, 0b, 4d]
    

    shared_secret_point() returns the EC point as concatenation of its x and y coordinate x|y, which is why the first 32 bytes equal the shared secret.

    On the shared secret determined in this way, a key derivation function (agreed upon by both sides) is used to derive the final key.


    In contrast to shared_secret_point(), SharedSecret() already applies a key derivation function whose logic can be found here:
    First a version is determined (here 0x02), this is concatenated with the raw shared secret (here 0x23557d446a4823d6a32fb087588226d1e8ef4f6b7b6d26091313840a74ed0b4d) and from the result (0x0223557d446a4823d6a32fb087588226d1e8ef4f6b7b6d26091313840a74ed0b4d) the SHA56 hash is generated (0x45b73c8aac8bcf651dad11f7f04f63b9f03486d028ab4d5c52bdd5d692d7c2aa) in accordance with the output of the Rust code.
    Note that this key derivation is not a standard and is therefore generally not implemented by other libraries.