3
$\begingroup$

Rust and NodeJS implementations of ECDH on secp256k1 produce different shared secrets, when using identical keypairs:

NodeJS:

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>

Rust:

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]

Both implementations work, i.e. they produce the same shared secret from (pk1, sk2) as from (pk2, sk1).

Should I expect these ECDH/secp256k1 shared secrets, produced by different implementations, to match?

$\endgroup$

1 Answer 1

4
$\begingroup$

ECDH produces a well-defined result given a specific private key and a specific public key. ECDH is very often used to generate the same shared secret from two programs that talk the same protocol, and those programs may not be implemented using the same code, so it really has to be interoperable.

The raw shared secret from ECDH is not directly suitable for many purposes because it has a fixed length and it's biased (for example, since it's a number between $1$ and $n-1$ where $n$ is between $2^{k-1}$ and $2^k$, the values $0$ and $\ge n$ are unreachable). So it must be processed by a key derivation function. The protocol definition must specify what key derivation function to use. There are many correct ways to do the derivation there, and there isn't one that's more standard than others.

What is happening here is that Rust is applying a specific KDF which I think is specific to how secp256k1 is used in Bitcoin.

In practice, you have to watch for several things, which unfortunately may not always be caught, depending on the API design:

  • You need to pass the keys in the correct format. In principle, there are a few standard formats, but some implementations may require a non-standard format. You'd normally get a runtime error, but sometimes not, for example if an embedded implementation that's optimized for a specific processor architecture wants a non-default endianness.

    The standard format for private keys on SEC curves is a fixed-size big-endian representation of the private value. It may be encapsulated in a larger format with metadata.

    The standard format for public keys on SEC curves is a one-byte header followed by a fixed-size representation of the coordinates. It may be encapsulated in a larger format with metadata. There are two possible representations for a valid public key: compressed (the byte 0x02 or 0x03, followed by a fixed-length big-endian representation of the $x$ coordinate) or uncompressed (the byte 0x04, followed by a fixed-length big-endian representation of the $x$ and $y$ coordinates in this order). See SEC1 §2.3.3 for details.

  • Likewise, the output format may be different. Mathematically, the shared secret is a point on the curve. The de facto standard is to use the $x$ coordinate of that point as the shared secret, in a fixed-length big-endian representation.

  • There are many correct ways to do the key derivation, and there isn't one that is an ubiquitous standard. So check the documentation of your implementation — or the code, if the documentation is bad, which unfortunately is common in cryptography implementations.

$\endgroup$
2
  • 1
    $\begingroup$ Yes. I checked that the value output by NodeJS is the X coordinate of the raw ECDH point. And Reading The Fantastic Manual tells that the Rust implementation uses "EcdhHashFn Hash function to use to post-process an ECDH point to get a shared secret", There's a default such function but I can't tell which. $\endgroup$
    – fgrieu
    Commented Jul 24, 2023 at 16:32
  • 3
    $\begingroup$ @fgrieu-onstrike Yes, see the linked Stack Overflow question. Specifically, it's hashing 0x02 concatenated with the standard x coordinate, using SHA-256. The existence of this post-processing of SharedSecret is mentioned in the documentation of shared_secret_point but not in the documentation of SharedSecret. I RTFS for the exact postprocessing. $\endgroup$ Commented Jul 24, 2023 at 16:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.