ECDH encryption using sr25519 keys
Elliptic-curve Diffie-Hellman (ECDH) is a key agreement protocol that allows two parties, each having an elliptic public-private key pair to establish a shared secret over an insecure channel. The shared secret can then be used to encrypt subsequent communications using a symmetric-key cipher. SR25519 is a Schnorr Signature protocol on top of the Ristretto compressed point format of the Ed25519 curve. This tutorial will explain in detail how the ECDH works and has also some code examples in golang.
TL;DR
You can find the full source code and examples at github.com/gabihodoroaga/ecdh.
How ECDH encryption works
ECDH is based on the following property of EC points:
(a * G) * b = (b * G) * a
If we have two secret numbers a and b (two private keys, belonging to Alice and Bob) and an ECC elliptic curve with generator point G, we can exchange over an insecure channel the values (a * G) and (b * G) (the public keys of Alice and Bob) and then we can derive a shared secret: secret = (a * G) * b = (b * G) * a. Pretty simple. The above equation takes the following form:
alicePubKey * bobPrivKey = bobPubKey * alicePrivKey = secret
The ECDH algorithm (Elliptic Curve Diffie-Hellman Key Exchange) goes like this:
- Alice generates a random ECC key pair: alicePrivKey, alicePubKey
- Bob generates a random ECC key pair: bobPrivKey, bobPubKey
- Alice and Bob exchange their public keys through the insecure channel (e.g. over Internet)
- Alice calculates sharedKey = bobPubKey * alicePrivKey
- Bob calculates sharedKey = alicePubKey * bobPrivKey
- Now both Alice and Bob have the same sharedKey == bobPubKey * alicePrivKey == alicePubKey * bobPrivKey
How to get shared secret using sr25519 keys
In order to generate the shared secret we will use the package github.com/gtank/ristretto255
created by
George Tankersley
import (
"crypto/sha512"
r255 "github.com/gtank/ristretto255"
)
func SharedSecret(pub, priv []byte) ([]byte, error) {
pre := &r255.Element{}
err := pre.Decode(pub)
if err != nil {
return nil, err
}
sc := &r255.Scalar{}
err = sc.Decode(expandKey(priv))
if err != nil {
return nil, err
}
e := r255.Element{}
secret := e.ScalarMult(sc, pre).Encode([]byte{})
return secret, nil
}
This function is doing a simple scalar multiplication of the public key of Alice with the private key of Bob.
In order to be able to do the multiplication we need fist to expand the private key using the following function:
// expandKey expands a secret key using ed25519-style bit clamping
// https://ristretto.group/formulas/decoding.html
// https://github.com/w3f/schnorrkel/blob/43f7fc00724edd1ef53d5ae13d82d240ed6202d5/src/keys.rs#L196
func expandKey(key []byte) []byte {
newKey := [32]byte{}
h := sha512.Sum512(key)
copy(newKey[:], h[:32])
newKey[0] &= 248
newKey[31] &= 63
newKey[31] |= 64
l := len(newKey) - 1
low := byte(0)
for i := range newKey {
r := newKey[l-i] & 0x07
newKey[l-i] >>= 3
newKey[l-i] += low
low = r << 5
}
return newKey[:]
}
Next we should test if our implementation works as expected.
First let’s generate some key pairs and for this we can use a tool from Parity called subkey.
docker run -it --rm parity/subkey generate --scheme Sr25519
amd the output should be like this
Secret phrase: seed cream flash destroy pistol twin fabric trend decrease hint leg describe
Secret seed: 0x4371abacb33d4819e69ae66cb120a10a2403645926f9def758577278b39b43d3
Public key (hex): 0x32bc4936c7925bfc8f7305b1672235254cdf9f0589474e06351365e461558d31
Account ID: 0x32bc4936c7925bfc8f7305b1672235254cdf9f0589474e06351365e461558d31
Public key (SS58): 4iGvtfaq4zF9ZvNRBpsPN3YE2KrVz8bidgZyR9GBVVBFenkn
SS58 Address: 4iGvtfaq4zF9ZvNRBpsPN3YE2KrVz8bidgZyR9GBVVBFenkn
and another one for Bob
Secret phrase: clarify battle shoot shy matrix erupt note often expect pull increase scale
Secret seed: 0xb38d9ec4180312196f474fd36cc2919b121bafe9bb5185d86a3f97021e665177
Public key (hex): 0xde372a0e94cc1f2c3e5ec27742c9a6011f3823c2e98f091d04c2899586d00d47
Account ID: 0xde372a0e94cc1f2c3e5ec27742c9a6011f3823c2e98f091d04c2899586d00d47
Public key (SS58): 4n9majJnhYE6tr8jkQ9csrdbdop7KnbBMxUJxSNDiKwyxGN3
SS58 Address: 4n9majJnhYE6tr8jkQ9csrdbdop7KnbBMxUJxSNDiKwyxGN3
Nex we need to create our test function
import (
"encoding/hex"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSharedSecret(t *testing.T) {
alicePrivKey,_ := hex.DecodeString("4371abacb33d4819e69ae66cb120a10a2403645926f9def758577278b39b43d3")
alicePubKey,_ := hex.DecodeString("32bc4936c7925bfc8f7305b1672235254cdf9f0589474e06351365e461558d31")
bobPrivKey,_ := hex.DecodeString("b38d9ec4180312196f474fd36cc2919b121bafe9bb5185d86a3f97021e665177")
bobPubKey,_ := hex.DecodeString("de372a0e94cc1f2c3e5ec27742c9a6011f3823c2e98f091d04c2899586d00d47")
aliceSecret, err := SharedSecret(bobPubKey, alicePrivKey)
require.Nil(t, err)
bobSecret, err := SharedSecret(alicePubKey, bobPrivKey)
require.Nil(t, err)
assert.Equal(t, encryptSecret, decryptSecret)
}
This is it. Now this shared secret can be used as a key to encrypt any message using a symmetric-key cipher.
In reality when you need to encrypt a message for Bob you don’t need to use the sender’s public key and it is more practical to use an ephemeral key pair and include the ephemeral public key into the final message.
- Bob provides a public key: bobPubKey
- A new ephemeral key is generated: ephemeralPubKey, ephemeralPrivKey
- A shared secret is calculated: bobPubKey * ephemeralPrivKey
- A message is encrypted with the shared secret: encryptedMessage
- Bob will receive a message containing both ephemeralPubKey + encryptedMessage
- When Bob wants to decrypt the message read the ephemeralPubKey and the encryptedMessage
- Calculates the shared secret: ephemeralPubKey * bobPrivKey
- Decrypts the message using the shared secret
For a complete example of how to do ECDHE with AES-128-CBC-HMAC-SHA1 check the example in this repository github.com/gabihodoroaga/ecdh.
References
I’m not an expert in cryptography. I’m not the actual author of all this. All I did is to copy pieces of code from the following source and put them together in a way that makes sense to me and I hope it will make sense for someone else too.
ECDH Key Exchange by Svetlin Nakov
Ristretto by The Ristretto Group