Skip to main content

tempo_precompiles/signature_verifier/
mod.rs

1pub mod dispatch;
2
3use crate::{SIGNATURE_VERIFIER_ADDRESS, account_keychain::AccountKeychain, error::Result};
4use alloy::primitives::{Address, B256, Bytes};
5use tempo_contracts::precompiles::SignatureVerifierError;
6use tempo_precompiles_macros::contract;
7use tempo_primitives::transaction::{
8    SignatureType,
9    tt_signature::{KeychainSignature, PrimitiveSignature, TempoSignature},
10};
11
12/// Gas cost for secp256k1 signature verification.
13const SECP256K1_VERIFY_GAS: u64 = 3_000;
14
15/// Gas cost for P256 signature verification.
16const P256_VERIFY_GAS: u64 = 8_000;
17
18/// Gas cost for WebAuthn signature verification.
19const WEBAUTHN_VERIFY_GAS: u64 = 8_000;
20
21#[contract(addr = SIGNATURE_VERIFIER_ADDRESS)]
22pub struct SignatureVerifier {}
23
24impl SignatureVerifier {
25    pub fn initialize(&mut self) -> Result<()> {
26        self.__initialize()
27    }
28
29    pub fn recover(&mut self, hash: B256, signature: Bytes) -> Result<Address> {
30        // Parse and validate signature (handles size checks + type disambiguation).
31        let sig = PrimitiveSignature::from_bytes(&signature)
32            .map_err(|_| SignatureVerifierError::invalid_format())?;
33
34        // Charge verification gas before performing verification.
35        let verify_gas = match sig.signature_type() {
36            SignatureType::Secp256k1 => SECP256K1_VERIFY_GAS,
37            SignatureType::P256 => P256_VERIFY_GAS,
38            SignatureType::WebAuthn => WEBAUTHN_VERIFY_GAS,
39        };
40        self.storage.deduct_gas(verify_gas)?;
41
42        // Verify and recover signer.
43        sig.recover_signer(&hash)
44            .map_err(|_| SignatureVerifierError::invalid_signature().into())
45    }
46
47    pub fn verify_keychain(
48        &mut self,
49        account: Address,
50        hash: B256,
51        signature: Bytes,
52    ) -> Result<bool> {
53        let (embedded_account, key_id) = self.recover_keychain_key(hash, signature)?;
54        if embedded_account != account {
55            return Ok(false);
56        }
57
58        AccountKeychain::new().is_active_key(account, key_id)
59    }
60
61    pub fn verify_keychain_admin(
62        &mut self,
63        account: Address,
64        hash: B256,
65        signature: Bytes,
66    ) -> Result<bool> {
67        let (embedded_account, key_id) = self.recover_keychain_key(hash, signature)?;
68        if embedded_account != account {
69            return Ok(false);
70        }
71
72        AccountKeychain::new().is_admin_key(account, key_id)
73    }
74
75    fn recover_keychain_key(&mut self, hash: B256, signature: Bytes) -> Result<(Address, Address)> {
76        let sig = TempoSignature::from_bytes(&signature)
77            .map_err(|_| SignatureVerifierError::invalid_format())?;
78        let keychain_sig = sig
79            .as_keychain()
80            .ok_or_else(SignatureVerifierError::invalid_format)?;
81
82        if keychain_sig.is_legacy() {
83            return Err(SignatureVerifierError::invalid_format().into());
84        }
85
86        let signing_hash = KeychainSignature::signing_hash(hash, keychain_sig.user_address);
87        let key_id = self.recover(signing_hash, keychain_sig.signature.to_bytes())?;
88        Ok((keychain_sig.user_address, key_id))
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use crate::storage::{StorageCtx, hashmap::HashMapStorageProvider};
96    use alloy_signer::SignerSync;
97    use alloy_signer_local::PrivateKeySigner;
98    use tempo_chainspec::hardfork::TempoHardfork;
99    use tempo_primitives::transaction::tt_signature::{
100        SIGNATURE_TYPE_P256, SIGNATURE_TYPE_WEBAUTHN,
101    };
102
103    fn sign_recover(hash: B256, signature: Vec<u8>) -> Result<Address> {
104        SignatureVerifier::new().recover(hash, Bytes::from(signature))
105    }
106
107    #[test]
108    fn test_verify_secp256k1_valid() -> eyre::Result<()> {
109        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
110        StorageCtx::enter(&mut storage, || {
111            let signer = PrivateKeySigner::random();
112            let hash = B256::from([0xAA; 32]);
113            let sig = signer.sign_hash_sync(&hash)?;
114            let sig_bytes = sig.as_bytes().to_vec();
115            assert_eq!(sig_bytes.len(), 65);
116
117            let result = sign_recover(hash, sig_bytes)?;
118            assert_eq!(result, signer.address());
119            Ok(())
120        })
121    }
122
123    #[test]
124    fn test_verify_p256_valid() -> eyre::Result<()> {
125        use p256::{ecdsa::SigningKey, elliptic_curve::rand_core::OsRng};
126        use tempo_primitives::transaction::tt_signature::{derive_p256_address, normalize_p256_s};
127
128        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
129        StorageCtx::enter(&mut storage, || {
130            let signing_key = SigningKey::random(&mut OsRng);
131            let verifying_key = signing_key.verifying_key();
132            let encoded = verifying_key.to_encoded_point(false);
133            let pub_key_x =
134                B256::from_slice(encoded.x().ok_or_else(|| eyre::eyre!("missing x coord"))?);
135            let pub_key_y =
136                B256::from_slice(encoded.y().ok_or_else(|| eyre::eyre!("missing y coord"))?);
137            let expected_address = derive_p256_address(&pub_key_x, &pub_key_y);
138
139            let hash = B256::from([0xBB; 32]);
140            let (signature, _) = signing_key.sign_prehash_recoverable(hash.as_slice())?;
141            let r = B256::from_slice(&signature.r().to_bytes());
142            let s =
143                normalize_p256_s(&signature.s().to_bytes()).expect("p256 crate produces valid s");
144
145            // Build encoded P256 signature: 0x01 || r || s || x || y || prehash(0)
146            let mut sig_bytes = Vec::new();
147            sig_bytes.push(SIGNATURE_TYPE_P256);
148            sig_bytes.extend_from_slice(r.as_slice());
149            sig_bytes.extend_from_slice(s.as_slice());
150            sig_bytes.extend_from_slice(pub_key_x.as_slice());
151            sig_bytes.extend_from_slice(pub_key_y.as_slice());
152            sig_bytes.push(0); // pre_hash = false
153            assert_eq!(sig_bytes.len(), 130);
154
155            let result = sign_recover(hash, sig_bytes)?;
156            assert_eq!(result, expected_address);
157            Ok(())
158        })
159    }
160
161    #[test]
162    fn test_verify_empty_signature_reverts() -> eyre::Result<()> {
163        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
164        StorageCtx::enter(&mut storage, || {
165            let result = sign_recover(B256::ZERO, vec![]);
166            assert!(result.is_err());
167            Ok(())
168        })
169    }
170
171    #[test]
172    fn test_verify_secp256k1_wrong_length_reverts() -> eyre::Result<()> {
173        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
174        StorageCtx::enter(&mut storage, || {
175            // 64 bytes — not 65
176            let result = sign_recover(B256::ZERO, vec![0u8; 64]);
177            assert!(result.is_err());
178            // 66 bytes — not 65
179            let result = sign_recover(B256::ZERO, vec![0u8; 66]);
180            assert!(result.is_err());
181            Ok(())
182        })
183    }
184
185    #[test]
186    fn test_verify_p256_wrong_length_reverts() -> eyre::Result<()> {
187        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
188        StorageCtx::enter(&mut storage, || {
189            // 0x01 prefix + 128 bytes (should be 129)
190            let mut sig = vec![SIGNATURE_TYPE_P256];
191            sig.extend_from_slice(&[0u8; 128]);
192            let result = sign_recover(B256::ZERO, sig);
193            assert!(result.is_err());
194            Ok(())
195        })
196    }
197
198    #[test]
199    fn test_verify_webauthn_too_short_reverts() -> eyre::Result<()> {
200        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
201        StorageCtx::enter(&mut storage, || {
202            // 0x02 prefix + 127 bytes (min is 128)
203            let mut sig = vec![SIGNATURE_TYPE_WEBAUTHN];
204            sig.extend_from_slice(&[0u8; 127]);
205            let result = sign_recover(B256::ZERO, sig);
206            assert!(result.is_err());
207            Ok(())
208        })
209    }
210
211    #[test]
212    fn test_verify_webauthn_too_long_reverts() -> eyre::Result<()> {
213        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
214        StorageCtx::enter(&mut storage, || {
215            // 0x02 prefix + 2049 bytes (max is 2048)
216            let mut sig = vec![SIGNATURE_TYPE_WEBAUTHN];
217            sig.extend_from_slice(&[0u8; 2049]);
218            let result = sign_recover(B256::ZERO, sig);
219            assert!(result.is_err());
220            Ok(())
221        })
222    }
223
224    #[test]
225    fn test_verify_unknown_type_reverts() -> eyre::Result<()> {
226        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
227        StorageCtx::enter(&mut storage, || {
228            let mut sig = vec![0x05];
229            sig.extend_from_slice(&[0u8; 129]);
230            let result = sign_recover(B256::ZERO, sig);
231            assert!(result.is_err());
232            Ok(())
233        })
234    }
235
236    #[test]
237    fn test_verify_invalid_secp256k1_signature_reverts() -> eyre::Result<()> {
238        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
239        StorageCtx::enter(&mut storage, || {
240            let result = sign_recover(B256::ZERO, vec![0u8; 65]);
241            assert!(result.is_err());
242            Ok(())
243        })
244    }
245}