posemesh_node_registration/
crypto.rs

1use chrono::{DateTime, SecondsFormat, Utc};
2use secp256k1::{ecdsa::Signature, Message, PublicKey, Secp256k1, SecretKey};
3use sha2::{Digest as Sha2Digest, Sha256};
4use sha3::Keccak256;
5
6/// Load a secp256k1 private key from lowercase hex (optionally 0x-prefixed).
7pub fn load_secp256k1_privhex(hex_str: &str) -> anyhow::Result<SecretKey> {
8    let s = hex_str.trim();
9    let s = s.strip_prefix("0x").unwrap_or(s);
10    let bytes = hex::decode(s)?;
11    if bytes.len() != 32 {
12        anyhow::bail!("invalid secp256k1 secret length: {}", bytes.len());
13    }
14    let sk = SecretKey::from_slice(&bytes)?;
15    Ok(sk)
16}
17
18/// Derive the uncompressed public key (0x04 || X || Y) as lowercase hex.
19pub fn secp256k1_pubkey_uncompressed_hex(sk: &SecretKey) -> String {
20    let secp = Secp256k1::new();
21    let pk = PublicKey::from_secret_key(&secp, sk);
22    let uncompressed = pk.serialize_uncompressed(); // 65 bytes, leading 0x04
23    hex::encode(uncompressed)
24}
25
26/// Sign arbitrary message bytes using RFC6979 deterministic ECDSA over SHA-256.
27/// Returns the compact 64-byte signature as lowercase hex (r||s).
28pub fn sign_compact_hex(sk: &SecretKey, msg: &[u8]) -> String {
29    let digest = Sha256::digest(msg);
30    let message = Message::from_digest_slice(&digest).expect("sha256 is 32 bytes");
31    let secp = Secp256k1::new();
32    let sig: Signature = secp.sign_ecdsa(&message, sk);
33    let compact = sig.serialize_compact();
34    hex::encode(compact)
35}
36
37/// Sign using Ethereum-style Keccak-256 digest and return 65-byte (r||s||v) hex.
38pub fn sign_recoverable_keccak_hex(sk: &SecretKey, msg: &[u8]) -> String {
39    // Keccak-256 of the raw message bytes (no prefixing)
40    let mut hasher = Keccak256::new();
41    hasher.update(msg);
42    let hash = hasher.finalize();
43    let message = Message::from_digest_slice(&hash).expect("keccak256 is 32 bytes");
44    let secp = Secp256k1::new();
45    let rsig = secp.sign_ecdsa_recoverable(&message, sk);
46    let (rid, sig_bytes) = rsig.serialize_compact();
47    let mut out = [0u8; 65];
48    out[..64].copy_from_slice(&sig_bytes);
49    out[64] = rid.to_i32() as u8; // 0 or 1
50    hex::encode(out)
51}
52
53/// RFC3339 with nanoseconds and Z suffix.
54pub fn format_timestamp_nanos(ts: DateTime<Utc>) -> String {
55    ts.to_rfc3339_opts(SecondsFormat::Nanos, true)
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use chrono::{NaiveDate, NaiveTime};
62
63    #[test]
64    fn timestamp_nanos_format() {
65        let date = NaiveDate::from_ymd_opt(2024, 1, 2).unwrap();
66        let time = NaiveTime::from_hms_nano_opt(3, 4, 5, 6_007_008).unwrap();
67        let dt = DateTime::<Utc>::from_naive_utc_and_offset(date.and_time(time), Utc);
68        let s = format_timestamp_nanos(dt);
69        assert_eq!(s, "2024-01-02T03:04:05.006007008Z");
70    }
71
72    #[test]
73    fn sign_fixed_keccak_recoverable_hex_has_expected_shape() {
74        // Fixed key and message; ensure output shape and stability invariants.
75        let sk = load_secp256k1_privhex(
76            "e331b6d69882b4ed5bb7f55b585d7d0f7dc3aeca4a3deee8d16bde3eca51aace",
77        )
78        .expect("key");
79        let url = "https://node.example.com";
80        let ts = "2024-01-02T03:04:05.000000000Z";
81        let msg = format!("{}{}", url, ts);
82        let sig = sign_recoverable_keccak_hex(&sk, msg.as_bytes());
83        // Expect 65-byte signature hex => 130 hex chars
84        assert_eq!(sig.len(), 130);
85        // All lowercase hex
86        assert!(sig
87            .chars()
88            .all(|c| c.is_ascii_hexdigit() && c.is_ascii_lowercase() || c.is_ascii_digit()));
89    }
90}