Skip to main content

netget/server/devp2p/
crypto.rs

1//! The cryptography devp2p is built from: secp256k1 node keys, ECDH, recoverable signatures,
2//! keccak-256, and the ECIES variant the RLPx handshake encrypts with.
3//!
4//! ECIES here is go-ethereum's `crypto/ecies` with `ECIES_AES128_SHA256`, the only parameter set
5//! RLPx uses: an ephemeral secp256k1 key `R`, `z` = the x-coordinate of `R·pub`, a 32-byte
6//! NIST SP 800-56 concatenation KDF over SHA-256 (counter 1, no shared info), `Ke` = the first
7//! 16 bytes, `Km` = SHA-256 of the last 16, AES-128-CTR under `Ke` with a random IV, and an
8//! HMAC-SHA256 tag under `Km` over `IV ‖ ciphertext ‖ shared_mac_data`. RLPx passes the
9//! message's two-byte size prefix as `shared_mac_data`, so the prefix is authenticated too.
10//!
11//! The wire form is `0x04 ‖ R (64) ‖ IV (16) ‖ ciphertext ‖ tag (32)`: 113 bytes of overhead.
12use aes::cipher::{KeyIvInit, StreamCipher};
13use anyhow::{bail, ensure, Context, Result};
14use hmac::{Hmac, Mac};
15use rand::RngCore;
16use secp256k1::ecdsa::{RecoverableSignature, RecoveryId};
17use secp256k1::{Message, PublicKey, Secp256k1, SecretKey};
18use sha2::{Digest, Sha256};
19use sha3::Keccak256;
20use std::sync::LazyLock;
21
22/// Bytes ECIES adds to a plaintext: the ephemeral key, the IV and the tag.
23pub const ECIES_OVERHEAD: usize = 65 + 16 + 32;
24
25static SECP: LazyLock<Secp256k1<secp256k1::All>> = LazyLock::new(Secp256k1::new);
26
27type Aes128Ctr = ctr::Ctr128BE<aes::Aes128>;
28
29/// keccak-256 (the pre-standard Keccak Ethereum uses, not SHA3-256) of the concatenation.
30pub fn keccak256(parts: &[&[u8]]) -> [u8; 32] {
31    let mut h = Keccak256::new();
32    for p in parts {
33        h.update(p);
34    }
35    h.finalize().into()
36}
37
38/// A node's identity: its secp256k1 key. The node id is the 64-byte uncompressed public key
39/// without the `0x04` prefix.
40#[derive(Clone)]
41pub struct NodeKey {
42    secret: SecretKey,
43    public: PublicKey,
44}
45
46impl NodeKey {
47    pub fn random() -> NodeKey {
48        loop {
49            let mut b = [0u8; 32];
50            rand::thread_rng().fill_bytes(&mut b);
51            if let Ok(k) = NodeKey::from_bytes(&b) {
52                return k;
53            }
54        }
55    }
56
57    pub fn from_bytes(b: &[u8]) -> Result<NodeKey> {
58        let secret = SecretKey::from_slice(b).context("not a valid secp256k1 secret key")?;
59        Ok(NodeKey {
60            public: PublicKey::from_secret_key(&SECP, &secret),
61            secret,
62        })
63    }
64
65    /// A key written as 64 hex characters, with or without `0x`.
66    pub fn from_hex(s: &str) -> Result<NodeKey> {
67        let b =
68            hex::decode(s.trim().trim_start_matches("0x")).context("the node key is not hex")?;
69        ensure!(b.len() == 32, "the node key is {} bytes, not 32", b.len());
70        NodeKey::from_bytes(&b)
71    }
72
73    pub fn id(&self) -> [u8; 64] {
74        node_id(&self.public)
75    }
76
77    pub fn public(&self) -> &PublicKey {
78        &self.public
79    }
80
81    pub fn secret(&self) -> &SecretKey {
82        &self.secret
83    }
84}
85
86/// The 64-byte node id of a public key.
87pub fn node_id(pk: &PublicKey) -> [u8; 64] {
88    let full = pk.serialize_uncompressed();
89    let mut id = [0u8; 64];
90    id.copy_from_slice(&full[1..]);
91    id
92}
93
94/// The public key a 64-byte node id names, refused when it is not a point on the curve.
95pub fn public_from_id(id: &[u8]) -> Result<PublicKey> {
96    ensure!(id.len() == 64, "a node id is 64 bytes, not {}", id.len());
97    let mut full = [0u8; 65];
98    full[0] = 4;
99    full[1..].copy_from_slice(id);
100    PublicKey::from_slice(&full).context("the node id is not a secp256k1 public key")
101}
102
103/// The x-coordinate of `secret · public`: the raw ECDH shared secret devp2p uses everywhere.
104pub fn ecdh_x(secret: &SecretKey, public: &PublicKey) -> [u8; 32] {
105    let point = secp256k1::ecdh::shared_secret_point(public, secret);
106    let mut x = [0u8; 32];
107    x.copy_from_slice(&point[..32]);
108    x
109}
110
111/// A recoverable signature over a 32-byte digest, as `r ‖ s ‖ v` with `v` 0 or 1.
112pub fn sign_recoverable(digest: &[u8; 32], secret: &SecretKey) -> [u8; 65] {
113    let sig = SECP.sign_ecdsa_recoverable(&Message::from_digest(*digest), secret);
114    let (rec, rs) = sig.serialize_compact();
115    let mut out = [0u8; 65];
116    out[..64].copy_from_slice(&rs);
117    out[64] = rec.to_i32() as u8;
118    out
119}
120
121/// The public key that signed `digest`, from an `r ‖ s ‖ v` signature.
122pub fn recover(digest: &[u8; 32], sig: &[u8]) -> Result<PublicKey> {
123    ensure!(
124        sig.len() == 65,
125        "a signature is 65 bytes, not {}",
126        sig.len()
127    );
128    let rec = RecoveryId::from_i32(i32::from(sig[64])).context("bad recovery id")?;
129    let sig = RecoverableSignature::from_compact(&sig[..64], rec).context("bad signature")?;
130    SECP.recover_ecdsa(&Message::from_digest(*digest), &sig)
131        .context("the signature recovers no key")
132}
133
134/// NIST SP 800-56 concatenation KDF over SHA-256, 32 bytes, no shared info.
135fn concat_kdf(z: &[u8; 32]) -> [u8; 32] {
136    let mut h = Sha256::new();
137    h.update(1u32.to_be_bytes());
138    h.update(z);
139    h.finalize().into()
140}
141
142fn ecies_keys(z: &[u8; 32]) -> ([u8; 16], [u8; 32]) {
143    let k = concat_kdf(z);
144    let mut ke = [0u8; 16];
145    ke.copy_from_slice(&k[..16]);
146    let km: [u8; 32] = Sha256::digest(&k[16..]).into();
147    (ke, km)
148}
149
150fn tag(km: &[u8; 32], iv_and_ciphertext: &[u8], shared_mac_data: &[u8]) -> [u8; 32] {
151    let mut mac = <Hmac<Sha256> as Mac>::new_from_slice(km).expect("HMAC takes any key length");
152    mac.update(iv_and_ciphertext);
153    mac.update(shared_mac_data);
154    mac.finalize().into_bytes().into()
155}
156
157/// Encrypt `plain` to `remote`.
158pub fn ecies_encrypt(remote: &PublicKey, plain: &[u8], shared_mac_data: &[u8]) -> Vec<u8> {
159    let ephemeral = NodeKey::random();
160    let z = ecdh_x(ephemeral.secret(), remote);
161    let (ke, km) = ecies_keys(&z);
162    let mut iv = [0u8; 16];
163    rand::thread_rng().fill_bytes(&mut iv);
164    let mut body = Vec::with_capacity(16 + plain.len());
165    body.extend_from_slice(&iv);
166    body.extend_from_slice(plain);
167    Aes128Ctr::new(&ke.into(), &iv.into()).apply_keystream(&mut body[16..]);
168    let t = tag(&km, &body, shared_mac_data);
169    let mut out = Vec::with_capacity(ECIES_OVERHEAD + plain.len());
170    out.extend_from_slice(&ephemeral.public().serialize_uncompressed());
171    out.extend_from_slice(&body);
172    out.extend_from_slice(&t);
173    out
174}
175
176/// Decrypt a message sent to `secret`. The tag is checked before anything is decrypted, in
177/// constant time.
178pub fn ecies_decrypt(secret: &SecretKey, data: &[u8], shared_mac_data: &[u8]) -> Result<Vec<u8>> {
179    ensure!(
180        data.len() > ECIES_OVERHEAD,
181        "ECIES message of {} bytes is shorter than its own overhead",
182        data.len()
183    );
184    if data[0] != 4 {
185        bail!("ECIES ephemeral key is not an uncompressed point");
186    }
187    let ephemeral =
188        PublicKey::from_slice(&data[..65]).context("ECIES ephemeral key is not on the curve")?;
189    let z = ecdh_x(secret, &ephemeral);
190    let (ke, km) = ecies_keys(&z);
191    let body = &data[65..data.len() - 32];
192    let want = tag(&km, body, shared_mac_data);
193    let mut diff = 0u8;
194    for (a, b) in want.iter().zip(&data[data.len() - 32..]) {
195        diff |= a ^ b;
196    }
197    ensure!(diff == 0, "ECIES tag mismatch");
198    let (iv, ciphertext) = body.split_at(16);
199    let mut plain = ciphertext.to_vec();
200    let iv: [u8; 16] = iv.try_into().expect("16 bytes");
201    Aes128Ctr::new(&ke.into(), &iv.into()).apply_keystream(&mut plain);
202    Ok(plain)
203}