openzeppelin_relayer/utils/
secp256k.rs

1use k256::ecdsa::{RecoveryId, Signature, VerifyingKey};
2use serde::Serialize;
3use sha3::{Digest, Keccak256};
4
5#[derive(Debug, Clone, thiserror::Error, Serialize)]
6pub enum Secp256k1Error {
7    #[error("Secp256k1 recovery error: {0}")]
8    RecoveryError(String),
9}
10
11/// Internal helper for public key recovery that handles the common logic.
12///
13/// This function tries both recovery IDs (0 and 1) and returns the one that matches
14/// the provided public key.
15fn recover_v_internal<F>(pk: &[u8], _sig: &Signature, recover_fn: F) -> Result<u8, Secp256k1Error>
16where
17    F: Fn(u8, RecoveryId) -> Option<Vec<u8>>,
18{
19    for v in 0..2 {
20        let rec_id = match RecoveryId::try_from(v) {
21            Ok(id) => id,
22            Err(_) => continue,
23        };
24
25        if let Some(recovered_key) = recover_fn(v, rec_id) {
26            // Validate recovered key has expected format (65 bytes: 0x04 + 64 bytes)
27            if recovered_key.len() != 65 || recovered_key[0] != 0x04 {
28                continue;
29            }
30
31            // Skip first byte (0x04 uncompressed point marker) and compare 64-byte public key
32            if recovered_key[1..] == pk[..] {
33                return Ok(v);
34            }
35        }
36    }
37
38    Err(Secp256k1Error::RecoveryError(format!(
39        "Failed to recover v value: no valid recovery ID found. \
40         This usually indicates a signature/public key mismatch. \
41         Public key length: {} bytes, tried recovery IDs: 0, 1",
42        pk.len()
43    )))
44}
45
46/// Recover `v` point from a signature and from the message contents
47pub fn recover_public_key(pk: &[u8], sig: &Signature, bytes: &[u8]) -> Result<u8, Secp256k1Error> {
48    // Validate public key length (64 bytes for uncompressed key without 0x04 prefix)
49    if pk.len() != 64 {
50        return Err(Secp256k1Error::RecoveryError(format!(
51            "Invalid public key length: expected 64 bytes, got {}",
52            pk.len()
53        )));
54    }
55
56    let mut hasher = Keccak256::new();
57    hasher.update(bytes);
58
59    recover_v_internal(pk, sig, |_, rec_id| {
60        VerifyingKey::recover_from_digest(hasher.clone(), sig, rec_id)
61            .ok()
62            .map(|key| key.to_encoded_point(false).as_bytes().to_vec())
63    })
64}
65
66/// Recovers the v value from a signature for a pre-hashed message.
67/// The hash parameter is already a 32-byte digest, not raw bytes.
68pub fn recover_public_key_from_hash(
69    pk: &[u8],
70    sig: &Signature,
71    hash: &[u8; 32],
72) -> Result<u8, Secp256k1Error> {
73    // Validate public key length (64 bytes for uncompressed key without 0x04 prefix)
74    if pk.len() != 64 {
75        return Err(Secp256k1Error::RecoveryError(format!(
76            "Invalid public key length: expected 64 bytes, got {}",
77            pk.len()
78        )));
79    }
80
81    recover_v_internal(pk, sig, |_, rec_id| {
82        VerifyingKey::recover_from_prehash(hash, sig, rec_id)
83            .ok()
84            .map(|key| key.to_encoded_point(false).as_bytes().to_vec())
85    })
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    use alloy::primitives::utils::eip191_message;
93    use k256::{ecdsa::SigningKey, elliptic_curve::rand_core::OsRng};
94
95    #[test]
96    fn test_recover_public_key() {
97        // Generate keypair
98        let signing_key = SigningKey::random(&mut OsRng);
99        let verifying_key = signing_key.verifying_key();
100        let public_key_vec = verifying_key.to_encoded_point(false).as_bytes().to_vec();
101        let public_key_bytes = &public_key_vec[1..];
102        println!("Pub key length: {}", public_key_bytes.len());
103
104        // EIP-191 style of a message
105        let eip_message = eip191_message(b"Hello World");
106
107        // Ethereum-style hash: keccak256 of message
108        let mut hasher = Keccak256::new();
109        hasher.update(eip_message.clone());
110
111        // Sign the message pre-hash
112        let (signature, rec_id) = signing_key.sign_digest_recoverable(hasher).unwrap();
113
114        // Try to recover the public key
115        let recovery_result = recover_public_key(public_key_bytes, &signature, &eip_message);
116
117        // Check that a valid recovery ID (0 or 1) is returned
118        match recovery_result {
119            Ok(v) => {
120                assert!(v == 0 || v == 1, "Recovery ID should be 0 or 1, got {}", v);
121                assert_eq!(rec_id.to_byte(), v, "Recovery ID should match")
122            }
123            Err(e) => panic!("Failed to recover public key: {:?}", e),
124        }
125    }
126
127    #[test]
128    fn test_recover_public_key_from_hash() {
129        // Generate keypair
130        let signing_key = SigningKey::random(&mut OsRng);
131        let verifying_key = signing_key.verifying_key();
132        let public_key_vec = verifying_key.to_encoded_point(false).as_bytes().to_vec();
133        let public_key_bytes = &public_key_vec[1..];
134
135        // Create a pre-computed hash (simulating EIP-712)
136        let message = b"Test message for EIP-712";
137        let mut hasher = Keccak256::new();
138        hasher.update(message);
139        let hash: [u8; 32] = hasher.finalize().into();
140
141        // Sign the hash directly (as KMS would do)
142        let (signature, rec_id) = signing_key.sign_prehash_recoverable(&hash).unwrap();
143
144        // Try to recover the public key from the hash
145        let recovery_result = recover_public_key_from_hash(public_key_bytes, &signature, &hash);
146
147        // Check that a valid recovery ID (0 or 1) is returned
148        match recovery_result {
149            Ok(v) => {
150                assert!(v == 0 || v == 1, "Recovery ID should be 0 or 1, got {}", v);
151                assert_eq!(rec_id.to_byte(), v, "Recovery ID should match")
152            }
153            Err(e) => panic!("Failed to recover public key from hash: {:?}", e),
154        }
155    }
156
157    #[test]
158    fn test_recover_public_key_from_hash_deterministic() {
159        // Test that signing the same hash produces consistent v values
160        let signing_key = SigningKey::random(&mut OsRng);
161        let verifying_key = signing_key.verifying_key();
162        let public_key_vec = verifying_key.to_encoded_point(false).as_bytes().to_vec();
163        let public_key_bytes = &public_key_vec[1..];
164
165        // Create a fixed hash
166        let hash: [u8; 32] = [0x42; 32];
167
168        // Sign multiple times
169        let (sig1, _) = signing_key.sign_prehash_recoverable(&hash).unwrap();
170        let (sig2, _) = signing_key.sign_prehash_recoverable(&hash).unwrap();
171
172        // Recover v values
173        let v1 = recover_public_key_from_hash(public_key_bytes, &sig1, &hash).unwrap();
174        let v2 = recover_public_key_from_hash(public_key_bytes, &sig2, &hash).unwrap();
175
176        // Both should produce the same v value (deterministic signing with same key and hash)
177        assert_eq!(v1, v2, "V values should be consistent for the same hash");
178    }
179}