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