use k256::ecdsa::{RecoveryId, Signature, VerifyingKey};
use sha2::{Digest as _, Sha256};
use sha3::Keccak256;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ChatSignature {
pub text: String,
pub signature: String,
pub signing_address: String,
pub signing_algo: String,
}
pub fn chat_signing_text(model: &str, request_hash: &str, response_hash: &str) -> String {
format!("{model}:{request_hash}:{response_hash}")
}
pub fn sha256_hex(bytes: &[u8]) -> String {
hex::encode(Sha256::digest(bytes))
}
fn eip191_digest(message: &[u8]) -> [u8; 32] {
let mut hasher = Keccak256::new();
hasher.update(b"\x19Ethereum Signed Message:\n");
hasher.update(message.len().to_string().as_bytes());
hasher.update(message);
hasher.finalize().into()
}
fn address_from_verifying_key(vk: &VerifyingKey) -> String {
let encoded = vk.to_encoded_point(false); let hash = Keccak256::digest(&encoded.as_bytes()[1..]);
format!("0x{}", hex::encode(&hash[12..]))
}
fn recovery_id(v: u8) -> Option<RecoveryId> {
let raw = match v {
27 | 28 => v - 27,
0 | 1 => v,
_ => return None,
};
RecoveryId::from_byte(raw)
}
pub fn recover_eip191_address(message: &[u8], signature_hex: &str) -> Option<String> {
let bytes = hex::decode(signature_hex.strip_prefix("0x").unwrap_or(signature_hex)).ok()?;
if bytes.len() != 65 {
return None;
}
let rec_id = recovery_id(bytes[64])?;
let signature = Signature::from_slice(&bytes[..64]).ok()?;
let digest = eip191_digest(message);
let vk = VerifyingKey::recover_from_prehash(&digest, &signature, rec_id).ok()?;
Some(address_from_verifying_key(&vk))
}
#[cfg(test)]
mod tests {
use super::*;
use k256::ecdsa::SigningKey;
fn privkey_one() -> SigningKey {
let mut bytes = [0u8; 32];
bytes[31] = 1;
SigningKey::from_slice(&bytes).unwrap()
}
const ADDR_FOR_PRIVKEY_ONE: &str = "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf";
fn sign_eip191(key: &SigningKey, message: &[u8]) -> String {
let digest = eip191_digest(message);
let (sig, rec_id) = key.sign_prehash_recoverable(&digest).unwrap();
let mut out = sig.to_bytes().to_vec(); out.push(27 + rec_id.to_byte()); hex::encode(out)
}
#[test]
fn address_derivation_matches_the_known_answer() {
let addr = address_from_verifying_key(privkey_one().verifying_key());
assert_eq!(addr, ADDR_FOR_PRIVKEY_ONE);
}
#[test]
fn recovers_the_signer_of_a_chat_text() {
let key = privkey_one();
let text = chat_signing_text("Qwen/Qwen3.5-122B-A10B", &"ab".repeat(32), &"cd".repeat(32));
let sig = sign_eip191(&key, text.as_bytes());
let recovered = recover_eip191_address(text.as_bytes(), &sig).expect("recovers");
assert_eq!(recovered, ADDR_FOR_PRIVKEY_ONE);
}
#[test]
fn a_tampered_message_recovers_a_different_address() {
let key = privkey_one();
let text = chat_signing_text("m", &"ab".repeat(32), &"cd".repeat(32));
let sig = sign_eip191(&key, text.as_bytes());
let tampered = chat_signing_text("m", &"ab".repeat(32), &"ce".repeat(32));
let recovered = recover_eip191_address(tampered.as_bytes(), &sig);
assert_ne!(recovered.as_deref(), Some(ADDR_FOR_PRIVKEY_ONE));
}
#[test]
fn malformed_signatures_return_none() {
assert!(recover_eip191_address(b"x", "not-hex").is_none());
assert!(recover_eip191_address(b"x", &"00".repeat(64)).is_none()); assert!(recover_eip191_address(b"x", &format!("{}07", "00".repeat(64))).is_none()); }
#[test]
fn sha256_hex_and_text_format() {
assert_eq!(
sha256_hex(b""),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
assert_eq!(chat_signing_text("m", "r", "s"), "m:r:s");
}
}