Skip to main content

bee/swarm/
keys.rs

1//! `PrivateKey` / `PublicKey` and the Ethereum signed-message signer.
2//!
3//! Mirrors bee-go's `pkg/swarm/typed_bytes.go` crypto block. ECDSA is
4//! provided by [`secp256k1`] (the libsecp256k1 C bindings used by
5//! Bitcoin Core, alloy, ethers, and reth) — chosen for performance:
6//! ~4× faster signing than the previous pure-Rust `k256` backend.
7//! libsecp256k1 returns low-S signatures by default, so no manual S
8//! normalization is needed.
9//!
10//! The scheme matches bee-js:
11//!
12//! ```text
13//! digest = keccak256("\x19Ethereum Signed Message:\n32" || keccak256(data))
14//! ```
15//!
16//! and signatures are stored with `V ∈ {27, 28}` on the wire (v0
17//! / v1 are normalized on the way out, denormalized on the way in).
18
19use std::fmt;
20use std::str::FromStr;
21
22use secp256k1::ecdsa::{RecoverableSignature, RecoveryId};
23use secp256k1::{Message, PublicKey as SecpPublicKey, SECP256K1, SecretKey};
24use sha3::{Digest, Keccak256};
25use subtle::ConstantTimeEq;
26use zeroize::{Zeroize, ZeroizeOnDrop};
27
28use crate::swarm::bytes::{decode_hex, encode_hex};
29use crate::swarm::errors::Error;
30use crate::swarm::typed_bytes::{
31    ETH_ADDRESS_LENGTH, EthAddress, PRIVATE_KEY_LENGTH, PUBLIC_KEY_LENGTH, SIGNATURE_LENGTH,
32    Signature,
33};
34
35// ---- PrivateKey --------------------------------------------------------
36
37/// secp256k1 private key (32 bytes).
38///
39/// Not [`Copy`] — copies of secret material should be intentional.
40/// Implements [`ZeroizeOnDrop`] so the bytes are scrubbed from memory
41/// when the value is dropped, and [`PartialEq`] is constant-time.
42#[derive(Clone, Zeroize, ZeroizeOnDrop)]
43pub struct PrivateKey([u8; PRIVATE_KEY_LENGTH]);
44
45impl PartialEq for PrivateKey {
46    fn eq(&self, other: &Self) -> bool {
47        self.0.ct_eq(&other.0).into()
48    }
49}
50
51impl Eq for PrivateKey {}
52
53impl PrivateKey {
54    /// Length in bytes.
55    pub const LENGTH: usize = PRIVATE_KEY_LENGTH;
56
57    /// Construct from raw bytes.
58    pub fn new(b: &[u8]) -> Result<Self, Error> {
59        if b.len() != PRIVATE_KEY_LENGTH {
60            return Err(Error::LengthMismatch {
61                kind: "PrivateKey",
62                expected: &[PRIVATE_KEY_LENGTH],
63                got: b.len(),
64            });
65        }
66        let mut a = [0u8; PRIVATE_KEY_LENGTH];
67        a.copy_from_slice(b);
68        Ok(Self(a))
69    }
70
71    /// Parse from hex (with or without `0x` prefix).
72    pub fn from_hex(s: &str) -> Result<Self, Error> {
73        Self::new(&decode_hex(s)?)
74    }
75
76    /// Borrow the raw bytes.
77    pub fn as_bytes(&self) -> &[u8] {
78        &self.0
79    }
80
81    /// Lowercase hex, no `0x` prefix.
82    pub fn to_hex(&self) -> String {
83        encode_hex(&self.0)
84    }
85
86    fn secret_key(&self) -> Result<SecretKey, Error> {
87        SecretKey::from_slice(&self.0).map_err(Error::crypto)
88    }
89
90    /// Derive the uncompressed (64-byte `X || Y`) public key.
91    pub fn public_key(&self) -> Result<PublicKey, Error> {
92        let sk = self.secret_key()?;
93        let pk = SecpPublicKey::from_secret_key(SECP256K1, &sk);
94        // 0x04 || X(32) || Y(32). Strip the prefix.
95        let serialized = pk.serialize_uncompressed();
96        let mut a = [0u8; PUBLIC_KEY_LENGTH];
97        a.copy_from_slice(&serialized[1..]);
98        Ok(PublicKey(a))
99    }
100
101    /// Sign `data` using the Ethereum signed-message scheme and return
102    /// a 65-byte `R || S || V` signature with `V ∈ {27, 28}`.
103    pub fn sign(&self, data: &[u8]) -> Result<Signature, Error> {
104        let digest = eth_signed_message_digest(data);
105        let sk = self.secret_key()?;
106        let msg = Message::from_digest(digest);
107        // libsecp256k1 always returns low-S; no normalization needed.
108        let sig: RecoverableSignature = SECP256K1.sign_ecdsa_recoverable(&msg, &sk);
109        let (recid, compact) = sig.serialize_compact();
110
111        let mut out = [0u8; SIGNATURE_LENGTH];
112        out[..64].copy_from_slice(&compact);
113        out[64] = (recid as i32) as u8 + 27;
114        Signature::new(&out)
115    }
116}
117
118impl fmt::Debug for PrivateKey {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        // Don't leak private bytes in Debug output.
121        f.write_str("PrivateKey(<redacted>)")
122    }
123}
124
125impl FromStr for PrivateKey {
126    type Err = Error;
127    fn from_str(s: &str) -> Result<Self, Self::Err> {
128        Self::from_hex(s)
129    }
130}
131
132// ---- PublicKey ---------------------------------------------------------
133
134/// Uncompressed secp256k1 public key (`X || Y`, 64 bytes).
135///
136/// Constructors accept the 33-byte SEC1-compressed encoding too.
137#[derive(Clone, Copy, PartialEq, Eq, Hash)]
138pub struct PublicKey([u8; PUBLIC_KEY_LENGTH]);
139
140impl PublicKey {
141    /// Length in bytes when stored uncompressed.
142    pub const LENGTH: usize = PUBLIC_KEY_LENGTH;
143
144    /// Construct from bytes. Accepts 64-byte uncompressed (`X || Y`)
145    /// or 33-byte compressed (SEC1).
146    pub fn new(b: &[u8]) -> Result<Self, Error> {
147        match b.len() {
148            PUBLIC_KEY_LENGTH => {
149                let mut a = [0u8; PUBLIC_KEY_LENGTH];
150                a.copy_from_slice(b);
151                Ok(Self(a))
152            }
153            33 => {
154                let pk = SecpPublicKey::from_slice(b).map_err(Error::crypto)?;
155                let serialized = pk.serialize_uncompressed();
156                let mut a = [0u8; PUBLIC_KEY_LENGTH];
157                a.copy_from_slice(&serialized[1..]);
158                Ok(Self(a))
159            }
160            n => Err(Error::LengthMismatch {
161                kind: "PublicKey",
162                expected: &[33, PUBLIC_KEY_LENGTH],
163                got: n,
164            }),
165        }
166    }
167
168    /// Parse from hex (with or without `0x` prefix). Length must be 33
169    /// (compressed) or 64 (uncompressed).
170    pub fn from_hex(s: &str) -> Result<Self, Error> {
171        Self::new(&decode_hex(s)?)
172    }
173
174    /// Borrow the raw 64-byte uncompressed encoding.
175    pub fn as_bytes(&self) -> &[u8] {
176        &self.0
177    }
178
179    /// Lowercase hex of the uncompressed encoding, no `0x` prefix.
180    pub fn to_hex(&self) -> String {
181        encode_hex(&self.0)
182    }
183
184    /// Ethereum address: last 20 bytes of `keccak256(X || Y)`.
185    pub fn address(&self) -> EthAddress {
186        let mut h = Keccak256::new();
187        h.update(self.0);
188        let out = h.finalize();
189        let mut a = [0u8; ETH_ADDRESS_LENGTH];
190        a.copy_from_slice(&out[12..]);
191        EthAddress::new(&a).expect("hash slice has fixed length")
192    }
193
194    /// 33-byte SEC1-compressed encoding.
195    pub fn compressed_bytes(&self) -> Result<[u8; 33], Error> {
196        let mut full = [0u8; 65];
197        full[0] = 0x04;
198        full[1..].copy_from_slice(&self.0);
199        let pk = SecpPublicKey::from_slice(&full).map_err(Error::crypto)?;
200        Ok(pk.serialize())
201    }
202
203    /// Lowercase hex of the compressed encoding, no `0x` prefix.
204    pub fn compressed_hex(&self) -> Result<String, Error> {
205        Ok(encode_hex(&self.compressed_bytes()?))
206    }
207}
208
209impl fmt::Debug for PublicKey {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        write!(f, "PublicKey({})", self.to_hex())
212    }
213}
214
215impl fmt::Display for PublicKey {
216    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217        f.write_str(&self.to_hex())
218    }
219}
220
221impl FromStr for PublicKey {
222    type Err = Error;
223    fn from_str(s: &str) -> Result<Self, Self::Err> {
224        Self::from_hex(s)
225    }
226}
227
228// ---- Signature recovery ------------------------------------------------
229
230impl Signature {
231    /// Recover the public key that produced this signature for `data`,
232    /// using the Ethereum signed-message scheme.
233    pub fn recover_public_key(&self, data: &[u8]) -> Result<PublicKey, Error> {
234        let digest = eth_signed_message_digest(data);
235        let bytes = self.as_bytes();
236        let v = bytes[64];
237        let recovery_byte = if v >= 27 { v - 27 } else { v };
238        let recid = RecoveryId::try_from(recovery_byte as i32)
239            .map_err(|_| Error::crypto("invalid V byte"))?;
240        let recsig =
241            RecoverableSignature::from_compact(&bytes[..64], recid).map_err(Error::crypto)?;
242        let msg = Message::from_digest(digest);
243        let pk = SECP256K1
244            .recover_ecdsa(&msg, &recsig)
245            .map_err(Error::crypto)?;
246        let serialized = pk.serialize_uncompressed();
247        let mut a = [0u8; PUBLIC_KEY_LENGTH];
248        a.copy_from_slice(&serialized[1..]);
249        Ok(PublicKey(a))
250    }
251
252    /// True iff the signature is valid against `data` and the recovered
253    /// signer matches `expected`.
254    pub fn is_valid(&self, data: &[u8], expected: EthAddress) -> bool {
255        match self.recover_public_key(data) {
256            Ok(pk) => pk.address() == expected,
257            Err(_) => false,
258        }
259    }
260}
261
262// ---- digest helper -----------------------------------------------------
263
264/// `keccak256("\x19Ethereum Signed Message:\n32" || keccak256(data))`.
265/// The exact digest Bee verifies SOC and feed signatures against.
266pub fn eth_signed_message_digest(data: &[u8]) -> [u8; 32] {
267    let mut h = Keccak256::new();
268    h.update(data);
269    let inner = h.finalize();
270
271    let mut h = Keccak256::new();
272    h.update(b"\x19Ethereum Signed Message:\n32");
273    h.update(inner);
274    let out = h.finalize();
275    let mut a = [0u8; 32];
276    a.copy_from_slice(&out);
277    a
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    fn priv_repeat(byte: u8) -> PrivateKey {
285        PrivateKey::new(&[byte; PRIVATE_KEY_LENGTH]).unwrap()
286    }
287
288    #[test]
289    fn private_to_public_to_address_is_consistent() {
290        let pk = priv_repeat(0x11);
291        let pub_a = pk.public_key().unwrap();
292        let pub_b = pk.public_key().unwrap();
293        assert_eq!(pub_a.as_bytes(), pub_b.as_bytes());
294        assert_eq!(pub_a.address(), pub_b.address());
295    }
296
297    #[test]
298    fn public_key_compressed_round_trip() {
299        let pk = priv_repeat(0x22);
300        let pub_a = pk.public_key().unwrap();
301        let compressed = pub_a.compressed_bytes().unwrap();
302        assert_eq!(compressed.len(), 33);
303        let pub_b = PublicKey::new(&compressed).unwrap();
304        assert_eq!(pub_a.as_bytes(), pub_b.as_bytes());
305    }
306
307    #[test]
308    fn sign_recover_round_trip() {
309        let pk = priv_repeat(0x33);
310        let data = b"hello swarm";
311        let sig = pk.sign(data).unwrap();
312        // V normalized to {27, 28} per bee-js wire format.
313        let v = sig.as_bytes()[64];
314        assert!(v == 27 || v == 28, "V was {v}");
315        let recovered = sig.recover_public_key(data).unwrap();
316        assert_eq!(recovered.as_bytes(), pk.public_key().unwrap().as_bytes());
317        assert!(sig.is_valid(data, pk.public_key().unwrap().address()));
318        assert!(!sig.is_valid(b"tampered", pk.public_key().unwrap().address()));
319    }
320
321    #[test]
322    fn debug_does_not_leak_private_bytes() {
323        let pk = priv_repeat(0x44);
324        let s = format!("{pk:?}");
325        assert!(!s.contains("44"));
326        assert!(s.contains("redacted"));
327    }
328
329    #[test]
330    fn zeroize_clears_private_bytes() {
331        let mut pk = priv_repeat(0x55);
332        assert_eq!(pk.as_bytes(), &[0x55; PRIVATE_KEY_LENGTH]);
333        pk.zeroize();
334        assert_eq!(pk.as_bytes(), &[0u8; PRIVATE_KEY_LENGTH]);
335    }
336
337    #[test]
338    fn private_key_is_zeroize_on_drop() {
339        // Compile-time bound: ZeroizeOnDrop is what we promise in the
340        // type's docs; this assertion fails to compile if the trait is
341        // ever removed.
342        fn assert_zod<T: zeroize::ZeroizeOnDrop>() {}
343        assert_zod::<PrivateKey>();
344    }
345
346    #[test]
347    fn private_key_eq_is_correct() {
348        let a = priv_repeat(0x66);
349        let b = priv_repeat(0x66);
350        let c = priv_repeat(0x77);
351        assert_eq!(a, b);
352        assert_ne!(a, c);
353    }
354}