44//! resolves a network's key entry into a ready-to-use [`Signer`]
55//! (private key signer + signing mode + effective `from` address).
66
7- use alloy:: { primitives:: Address , signers:: local:: PrivateKeySigner } ;
7+ use alloy:: {
8+ primitives:: { Address , TxKind } ,
9+ signers:: local:: PrivateKeySigner ,
10+ } ;
811use mpp:: client:: tempo:: signing:: { KeychainVersion , TempoSigningMode } ;
9- use tempo_primitives:: transaction:: SignedKeyAuthorization ;
12+ use tempo_primitives:: transaction:: { Call , CallScope , SelectorRule , SignedKeyAuthorization } ;
1013
1114use crate :: {
1215 error:: { ConfigError , KeyError , TempoError } ,
@@ -78,6 +81,23 @@ impl Signer {
7881 } )
7982 }
8083
84+ /// Returns a copy whose signing mode includes the stored key authorization,
85+ /// after verifying that authorization is valid for this transaction.
86+ ///
87+ /// This prevents stale or unrelated local authorizations from being attached
88+ /// to provisioning retries for a different chain, key, or scoped call set.
89+ pub fn with_key_authorization_for_transaction (
90+ & self ,
91+ chain_id : u64 ,
92+ calls : & [ Call ] ,
93+ ) -> Result < Option < Self > , TempoError > {
94+ let Some ( auth) = self . stored_key_authorization . as_deref ( ) else {
95+ return Ok ( None ) ;
96+ } ;
97+ validate_key_authorization_for_transaction ( auth, self . signer . address ( ) , chain_id, calls) ?;
98+ Ok ( self . with_key_authorization ( ) )
99+ }
100+
81101 /// Whether this signer has a stored key authorization available for
82102 /// provisioning retries.
83103 #[ must_use]
@@ -86,6 +106,94 @@ impl Signer {
86106 }
87107}
88108
109+ fn validate_key_authorization_for_transaction (
110+ auth : & SignedKeyAuthorization ,
111+ signer_address : Address ,
112+ chain_id : u64 ,
113+ calls : & [ Call ] ,
114+ ) -> Result < ( ) , TempoError > {
115+ if auth. authorization . key_id != signer_address {
116+ return Err ( KeyError :: SigningOperation {
117+ operation : "key authorization validation" ,
118+ reason : format ! (
119+ "authorization key {:#x} does not match signer {:#x}" ,
120+ auth. authorization. key_id, signer_address
121+ ) ,
122+ }
123+ . into ( ) ) ;
124+ }
125+
126+ if auth. authorization . chain_id != chain_id {
127+ return Err ( KeyError :: SigningOperation {
128+ operation : "key authorization validation" ,
129+ reason : format ! (
130+ "authorization chain {} does not match transaction chain {}" ,
131+ auth. authorization. chain_id, chain_id
132+ ) ,
133+ }
134+ . into ( ) ) ;
135+ }
136+
137+ if let Some ( expiry) = auth. authorization . expiry {
138+ let now = std:: time:: SystemTime :: now ( )
139+ . duration_since ( std:: time:: UNIX_EPOCH )
140+ . unwrap_or_default ( )
141+ . as_secs ( ) ;
142+ if expiry. get ( ) <= now {
143+ return Err ( KeyError :: SigningOperation {
144+ operation : "key authorization validation" ,
145+ reason : "authorization is expired" . to_string ( ) ,
146+ }
147+ . into ( ) ) ;
148+ }
149+ }
150+
151+ let Some ( scopes) = auth. authorization . allowed_calls . as_deref ( ) else {
152+ return Ok ( ( ) ) ;
153+ } ;
154+ if calls. is_empty ( ) || !calls. iter ( ) . all ( |call| call_matches_scopes ( call, scopes) ) {
155+ return Err ( KeyError :: SigningOperation {
156+ operation : "key authorization validation" ,
157+ reason : "authorization scopes do not cover transaction calls" . to_string ( ) ,
158+ }
159+ . into ( ) ) ;
160+ }
161+
162+ Ok ( ( ) )
163+ }
164+
165+ fn call_matches_scopes ( call : & Call , scopes : & [ CallScope ] ) -> bool {
166+ let TxKind :: Call ( target) = call. to else {
167+ return false ;
168+ } ;
169+ scopes. iter ( ) . any ( |scope| {
170+ scope. target == target && call_matches_scope_rules ( call, & scope. selector_rules )
171+ } )
172+ }
173+
174+ fn call_matches_scope_rules ( call : & Call , rules : & [ SelectorRule ] ) -> bool {
175+ if rules. is_empty ( ) {
176+ return true ;
177+ }
178+ let Some ( selector) = call. input . get ( ..4 ) else {
179+ return false ;
180+ } ;
181+ rules. iter ( ) . any ( |rule| {
182+ rule. selector == selector
183+ && ( rule. recipients . is_empty ( )
184+ || first_abi_address_argument ( call)
185+ . is_some_and ( |recipient| rule. recipients . contains ( & recipient) ) )
186+ } )
187+ }
188+
189+ fn first_abi_address_argument ( call : & Call ) -> Option < Address > {
190+ let arg = call. input . get ( 4 ..36 ) ?;
191+ if arg[ ..12 ] . iter ( ) . any ( |byte| * byte != 0 ) {
192+ return None ;
193+ }
194+ Some ( Address :: from_slice ( & arg[ 12 ..] ) )
195+ }
196+
89197impl Keystore {
90198 /// Resolve the wallet signer for a network.
91199 ///
@@ -158,6 +266,9 @@ impl Keystore {
158266#[ cfg( test) ]
159267mod tests {
160268 use super :: * ;
269+ use alloy:: primitives:: { Bytes , U256 } ;
270+ use alloy:: signers:: SignerSync ;
271+ use tempo_primitives:: transaction:: { KeyAuthorization , PrimitiveSignature , SignatureType } ;
161272 use zeroize:: Zeroizing ;
162273
163274 use crate :: keys:: KeyEntry ;
@@ -292,6 +403,52 @@ mod tests {
292403 ) ;
293404 }
294405
406+ #[ test]
407+ fn test_key_authorization_for_transaction_rejects_chain_mismatch ( ) {
408+ let signer = signer_with_auth ( |auth| auth) ;
409+
410+ let result = signer
411+ . with_key_authorization_for_transaction ( 4218 , & [ call_to ( Address :: repeat_byte ( 0x11 ) ) ] ) ;
412+ assert ! ( result. is_err( ) ) ;
413+ let err = result. err ( ) . unwrap ( ) . to_string ( ) ;
414+ assert ! ( err. contains( "authorization chain 4217" ) ) ;
415+ assert ! ( err. contains( "transaction chain 4218" ) ) ;
416+ }
417+
418+ #[ test]
419+ fn test_key_authorization_for_transaction_rejects_uncovered_scope ( ) {
420+ let allowed = Address :: repeat_byte ( 0x11 ) ;
421+ let denied = Address :: repeat_byte ( 0x22 ) ;
422+ let signer = signer_with_auth ( |auth| {
423+ auth. with_allowed_calls ( vec ! [ CallScope {
424+ target: allowed,
425+ selector_rules: vec![ ] ,
426+ } ] )
427+ } ) ;
428+
429+ let result = signer. with_key_authorization_for_transaction ( 4217 , & [ call_to ( denied) ] ) ;
430+ assert ! ( result. is_err( ) ) ;
431+ let err = result. err ( ) . unwrap ( ) . to_string ( ) ;
432+ assert ! ( err. contains( "authorization scopes do not cover transaction calls" ) ) ;
433+ }
434+
435+ #[ test]
436+ fn test_key_authorization_for_transaction_accepts_covered_scope ( ) {
437+ let allowed = Address :: repeat_byte ( 0x11 ) ;
438+ let signer = signer_with_auth ( |auth| {
439+ auth. with_allowed_calls ( vec ! [ CallScope {
440+ target: allowed,
441+ selector_rules: vec![ ] ,
442+ } ] )
443+ } ) ;
444+
445+ let provisioning = signer
446+ . with_key_authorization_for_transaction ( 4217 , & [ call_to ( allowed) ] )
447+ . unwrap ( )
448+ . expect ( "covered auth should attach" ) ;
449+ assert ! ( provisioning. signing_mode. key_authorization( ) . is_some( ) ) ;
450+ }
451+
295452 #[ test]
296453 fn test_signer_direct_has_no_stored_auth ( ) {
297454 let keys = Keystore :: from_private_key ( TEST_PRIVATE_KEY ) . unwrap ( ) ;
@@ -300,6 +457,47 @@ mod tests {
300457 assert ! ( signer. with_key_authorization( ) . is_none( ) ) ;
301458 }
302459
460+ fn signer_with_auth ( update : impl FnOnce ( KeyAuthorization ) -> KeyAuthorization ) -> Signer {
461+ let wallet_signer = parse_private_key_signer (
462+ "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" ,
463+ )
464+ . unwrap ( ) ;
465+ let access_signer = parse_private_key_signer ( TEST_PRIVATE_KEY ) . unwrap ( ) ;
466+ let auth = KeyAuthorization {
467+ chain_id : 4217 ,
468+ key_type : SignatureType :: Secp256k1 ,
469+ key_id : access_signer. address ( ) ,
470+ expiry : std:: num:: NonZero :: new ( 9_999_999_999 ) ,
471+ limits : None ,
472+ allowed_calls : None ,
473+ } ;
474+ let auth = update ( auth) ;
475+ let sig = wallet_signer
476+ . sign_hash_sync ( & auth. signature_hash ( ) )
477+ . unwrap ( ) ;
478+
479+ Signer {
480+ signer : access_signer,
481+ signing_mode : TempoSigningMode :: Keychain {
482+ wallet : wallet_signer. address ( ) ,
483+ key_authorization : None ,
484+ version : KeychainVersion :: V2 ,
485+ } ,
486+ from : wallet_signer. address ( ) ,
487+ stored_key_authorization : Some ( Box :: new (
488+ auth. into_signed ( PrimitiveSignature :: Secp256k1 ( sig) ) ,
489+ ) ) ,
490+ }
491+ }
492+
493+ fn call_to ( to : Address ) -> Call {
494+ Call {
495+ to : TxKind :: Call ( to) ,
496+ value : U256 :: ZERO ,
497+ input : Bytes :: from_static ( & [ 0xaa , 0xbb , 0xcc , 0xdd ] ) ,
498+ }
499+ }
500+
303501 #[ test]
304502 fn test_signer_no_key_for_network ( ) {
305503 let keys = Keystore :: default ( ) ;
0 commit comments