1use super::{
4 KeyType, SessionCallScope, SessionEntry, SessionKeyMaterial, SessionSelectorRule,
5 SessionStatus, SessionTokenLimit, session::validate_signed_session_authorization,
6};
7use alloy_primitives::{Address, B256, U256, hex};
8use alloy_rlp::Encodable;
9use alloy_signer_local::PrivateKeySigner;
10use eyre::ensure;
11use std::{fmt, num::NonZeroU64};
12use tempo_primitives::transaction::{
13 CallScope, KeyAuthorization, SelectorRule, SignatureType, SignedKeyAuthorization, TokenLimit,
14};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct SessionSpendLimit {
19 pub token: Address,
20 pub amount: U256,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct SessionAuthorizationRequest {
29 pub session_id: B256,
30 pub root_account: Address,
31 pub chain_id: u64,
32 pub key_address: Address,
33 pub expiry: NonZeroU64,
34 pub scope: Vec<CallScope>,
35 pub spend_limits: Vec<SessionSpendLimit>,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct PreparedSessionAuthorization {
41 pub entry: SessionEntry,
42 pub authorization: KeyAuthorization,
43}
44
45impl SessionAuthorizationRequest {
46 pub fn prepare(&self, now: u64) -> eyre::Result<PreparedSessionAuthorization> {
48 ensure!(self.session_id != B256::ZERO, "session id cannot be zero");
49 ensure!(self.root_account != Address::ZERO, "session root account cannot be zero");
50 ensure!(self.chain_id != 0, "session chain id cannot be zero");
51 ensure!(self.key_address != Address::ZERO, "session key address cannot be zero");
52 ensure!(
53 self.key_address != self.root_account,
54 "session key address must differ from the root account"
55 );
56
57 let expiry = self.expiry.get();
58 ensure!(
59 expiry > now,
60 "session expiry {expiry} must be greater than current timestamp {now}"
61 );
62 ensure!(!self.scope.is_empty(), "session authorization requires a call scope");
63
64 let authorization = KeyAuthorization::unrestricted(
65 self.chain_id,
66 SignatureType::Secp256k1,
67 self.key_address,
68 )
69 .with_expiry(expiry)
70 .with_limits(session_spend_limits_to_authorization(&self.spend_limits))
71 .with_allowed_calls(self.scope.clone())
72 .with_witness(self.session_id);
73
74 Ok(PreparedSessionAuthorization {
75 entry: SessionEntry {
76 session_id: self.session_id,
77 root_account: self.root_account,
78 chain_id: self.chain_id,
79 key_address: self.key_address,
80 expiry,
81 scope: Some(session_scopes_to_entry(&self.scope)),
82 limits: Some(session_spend_limits_to_entry(&self.spend_limits)),
83 status: SessionStatus::Pending,
84 key: None,
85 },
86 authorization,
87 })
88 }
89}
90
91impl PreparedSessionAuthorization {
92 pub fn into_active_entry(
94 mut self,
95 session_key: GeneratedSessionKey,
96 signed_authorization: &SignedKeyAuthorization,
97 ) -> eyre::Result<SessionEntry> {
98 ensure!(
99 session_key.address == self.entry.key_address,
100 "session key material resolves to {}, expected {}",
101 session_key.address,
102 self.entry.key_address
103 );
104 validate_signed_session_authorization(
105 &self.entry,
106 SignatureType::Secp256k1,
107 signed_authorization,
108 )?;
109
110 let mut buf = Vec::new();
111 signed_authorization.encode(&mut buf);
112 self.entry.status = SessionStatus::Active;
113 self.entry.key = Some(SessionKeyMaterial {
114 key_type: KeyType::Secp256k1,
115 key: session_key.private_key,
116 key_authorization: Some(hex::encode_prefixed(buf)),
117 });
118 Ok(self.entry)
119 }
120}
121
122#[derive(Clone, PartialEq, Eq)]
124pub struct GeneratedSessionKey {
125 address: Address,
126 private_key: String,
127}
128
129impl GeneratedSessionKey {
130 pub fn random() -> Self {
132 Self::from_signer(&PrivateKeySigner::random())
133 }
134
135 pub fn from_private_key(private_key: impl AsRef<str>) -> eyre::Result<Self> {
137 let signer = private_key.as_ref().parse::<PrivateKeySigner>()?;
138 Ok(Self::from_signer(&signer))
139 }
140
141 pub const fn address(&self) -> Address {
143 self.address
144 }
145
146 pub fn private_key(&self) -> &str {
148 &self.private_key
149 }
150
151 fn from_signer(signer: &PrivateKeySigner) -> Self {
152 Self { address: signer.address(), private_key: hex::encode_prefixed(signer.to_bytes()) }
153 }
154}
155
156impl fmt::Debug for GeneratedSessionKey {
157 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158 f.debug_struct("GeneratedSessionKey")
159 .field("address", &self.address)
160 .finish_non_exhaustive()
161 }
162}
163
164fn session_spend_limits_to_entry(limits: &[SessionSpendLimit]) -> Vec<SessionTokenLimit> {
165 limits
166 .iter()
167 .map(|limit| SessionTokenLimit { currency: limit.token, limit: limit.amount.to_string() })
168 .collect()
169}
170
171fn session_spend_limits_to_authorization(limits: &[SessionSpendLimit]) -> Vec<TokenLimit> {
172 limits
173 .iter()
174 .map(|limit| TokenLimit { token: limit.token, limit: limit.amount, period: 0 })
175 .collect()
176}
177
178fn session_scopes_to_entry(scope: &[CallScope]) -> Vec<SessionCallScope> {
179 scope
180 .iter()
181 .map(|scope| SessionCallScope {
182 target: scope.target,
183 selector_rules: session_selector_rules_to_entry(&scope.selector_rules),
184 })
185 .collect()
186}
187
188fn session_selector_rules_to_entry(rules: &[SelectorRule]) -> Vec<SessionSelectorRule> {
189 rules
190 .iter()
191 .map(|rule| SessionSelectorRule {
192 selector: rule.selector.into(),
193 recipients: rule.recipients.clone(),
194 })
195 .collect()
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use alloy_primitives::Selector;
202 use alloy_signer::SignerSync;
203 use tempo_primitives::transaction::PrimitiveSignature;
204
205 const ROOT_PRIVATE_KEY: &str =
206 "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
207 const SESSION_PRIVATE_KEY: &str =
208 "0x59c6995e998f97a5a004497e5da3b5d2b2b66a87f064d39c44da0b6d6e4f8ff0";
209
210 #[test]
211 fn prepared_session_authorization_builds_entry_and_key_authorization() {
212 let session_id = B256::from([0x42; 32]);
213 let root = Address::from([0x11; 20]);
214 let key = Address::from([0x22; 20]);
215 let target = Address::from([0x33; 20]);
216 let token = Address::from([0x44; 20]);
217 let recipient = Address::from([0x55; 20]);
218
219 let request = SessionAuthorizationRequest {
220 session_id,
221 root_account: root,
222 chain_id: 4217,
223 key_address: key,
224 expiry: NonZeroU64::new(1_700_000_600).unwrap(),
225 scope: vec![CallScope {
226 target,
227 selector_rules: vec![SelectorRule {
228 selector: [0x12, 0x34, 0x56, 0x78],
229 recipients: vec![recipient],
230 }],
231 }],
232 spend_limits: vec![SessionSpendLimit { token, amount: U256::ZERO }],
233 };
234
235 let prepared = request.prepare(1_700_000_000).unwrap();
236
237 assert_eq!(prepared.entry.session_id, session_id);
238 assert_eq!(prepared.entry.root_account, root);
239 assert_eq!(prepared.entry.chain_id, 4217);
240 assert_eq!(prepared.entry.key_address, key);
241 assert_eq!(prepared.entry.expiry, 1_700_000_600);
242 assert_eq!(prepared.entry.status, SessionStatus::Pending);
243 assert!(prepared.entry.key.is_none());
244 assert_eq!(
245 prepared.entry.scope,
246 Some(vec![SessionCallScope {
247 target,
248 selector_rules: vec![SessionSelectorRule {
249 selector: Selector::from_slice(&[0x12, 0x34, 0x56, 0x78]),
250 recipients: vec![recipient],
251 }],
252 }])
253 );
254 assert_eq!(
255 prepared.entry.limits,
256 Some(vec![SessionTokenLimit { currency: token, limit: "0".to_string() }])
257 );
258
259 assert_eq!(prepared.authorization.chain_id, 4217);
260 assert_eq!(prepared.authorization.key_type, SignatureType::Secp256k1);
261 assert_eq!(prepared.authorization.key_id, key);
262 assert_eq!(prepared.authorization.expiry.map(NonZeroU64::get), Some(1_700_000_600));
263 assert_eq!(prepared.authorization.witness, Some(session_id));
264 assert_eq!(
265 prepared.authorization.limits,
266 Some(vec![TokenLimit { token, limit: U256::ZERO, period: 0 }])
267 );
268 assert_eq!(
269 prepared.authorization.allowed_calls,
270 Some(vec![CallScope {
271 target,
272 selector_rules: vec![SelectorRule {
273 selector: [0x12, 0x34, 0x56, 0x78],
274 recipients: vec![recipient],
275 }],
276 }])
277 );
278 }
279
280 #[test]
281 fn prepared_session_authorization_rejects_invalid_policy() {
282 let base = SessionAuthorizationRequest {
283 session_id: B256::from([0x42; 32]),
284 root_account: Address::from([0x11; 20]),
285 chain_id: 4217,
286 key_address: Address::from([0x22; 20]),
287 expiry: NonZeroU64::new(1_700_000_600).unwrap(),
288 scope: vec![CallScope { target: Address::from([0x33; 20]), selector_rules: vec![] }],
289 spend_limits: vec![],
290 };
291
292 let mut expired = base.clone();
293 expired.expiry = NonZeroU64::new(1_700_000_000).unwrap();
294 assert!(expired.prepare(1_700_000_000).is_err());
295
296 let mut no_scope = base.clone();
297 no_scope.scope = vec![];
298 let error = no_scope.prepare(1_700_000_000).unwrap_err();
299 assert!(error.to_string().contains("call scope"));
300
301 let mut zero_root = base.clone();
302 zero_root.root_account = Address::ZERO;
303 assert!(zero_root.prepare(1_700_000_000).is_err());
304
305 let mut zero_key = base.clone();
306 zero_key.key_address = Address::ZERO;
307 assert!(zero_key.prepare(1_700_000_000).is_err());
308
309 let mut root_key = base;
310 root_key.key_address = root_key.root_account;
311 assert!(root_key.prepare(1_700_000_000).is_err());
312 }
313
314 #[test]
315 fn signed_session_authorization_activates_entry_with_key_material() {
316 let root: PrivateKeySigner = ROOT_PRIVATE_KEY.parse().unwrap();
317 let session_key = GeneratedSessionKey::from_private_key(SESSION_PRIVATE_KEY).unwrap();
318 let request = SessionAuthorizationRequest {
319 session_id: B256::from([0x66; 32]),
320 root_account: root.address(),
321 chain_id: 4217,
322 key_address: session_key.address(),
323 expiry: NonZeroU64::new(1_700_000_600).unwrap(),
324 scope: vec![CallScope { target: Address::from([0x33; 20]), selector_rules: vec![] }],
325 spend_limits: vec![],
326 };
327 let prepared = request.prepare(1_700_000_000).unwrap();
328 let signature = root.sign_hash_sync(&prepared.authorization.signature_hash()).unwrap();
329 let signed =
330 prepared.authorization.clone().into_signed(PrimitiveSignature::Secp256k1(signature));
331
332 let entry = prepared.into_active_entry(session_key, &signed).unwrap();
333
334 assert_eq!(entry.status, SessionStatus::Active);
335 let key = entry.key.unwrap();
336 assert_eq!(key.key_type, KeyType::Secp256k1);
337 assert_eq!(key.key, SESSION_PRIVATE_KEY);
338 assert!(key.key_authorization.unwrap().starts_with("0x"));
339 }
340
341 #[test]
342 fn prepared_session_authorization_enforces_empty_spend_policy() {
343 let target = Address::from([0x33; 20]);
344 let request = SessionAuthorizationRequest {
345 session_id: B256::from([0x68; 32]),
346 root_account: Address::from([0x11; 20]),
347 chain_id: 4217,
348 key_address: Address::from([0x22; 20]),
349 expiry: NonZeroU64::new(1_700_000_600).unwrap(),
350 scope: vec![CallScope { target, selector_rules: vec![] }],
351 spend_limits: vec![],
352 };
353
354 let prepared = request.prepare(1_700_000_000).unwrap();
355
356 assert_eq!(
357 prepared.entry.scope,
358 Some(vec![SessionCallScope { target, selector_rules: vec![] }])
359 );
360 assert_eq!(prepared.entry.limits, Some(vec![]));
361 assert_eq!(
362 prepared.authorization.allowed_calls,
363 Some(vec![CallScope { target, selector_rules: vec![] }])
364 );
365 assert_eq!(prepared.authorization.limits, Some(vec![]));
366 }
367
368 #[test]
369 fn signed_session_authorization_rejects_policy_mismatch() {
370 let root: PrivateKeySigner = ROOT_PRIVATE_KEY.parse().unwrap();
371 let session_key = GeneratedSessionKey::from_private_key(SESSION_PRIVATE_KEY).unwrap();
372 let token = Address::from([0x44; 20]);
373 let request = SessionAuthorizationRequest {
374 session_id: B256::from([0x67; 32]),
375 root_account: root.address(),
376 chain_id: 4217,
377 key_address: session_key.address(),
378 expiry: NonZeroU64::new(1_700_000_600).unwrap(),
379 scope: vec![CallScope { target: Address::from([0x33; 20]), selector_rules: vec![] }],
380 spend_limits: vec![SessionSpendLimit { token, amount: U256::ZERO }],
381 };
382 let prepared = request.prepare(1_700_000_000).unwrap();
383 let mut authorization = prepared.authorization.clone();
384 authorization.limits = None;
385 let signature = root.sign_hash_sync(&authorization.signature_hash()).unwrap();
386 let signed = authorization.into_signed(PrimitiveSignature::Secp256k1(signature));
387
388 let error = prepared.into_active_entry(session_key, &signed).unwrap_err();
389
390 assert!(error.to_string().contains("limits"));
391 }
392
393 #[test]
394 fn signed_session_authorization_rejects_session_id_mismatch() {
395 let root: PrivateKeySigner = ROOT_PRIVATE_KEY.parse().unwrap();
396 let session_key = GeneratedSessionKey::from_private_key(SESSION_PRIVATE_KEY).unwrap();
397 let request = SessionAuthorizationRequest {
398 session_id: B256::from([0x70; 32]),
399 root_account: root.address(),
400 chain_id: 4217,
401 key_address: session_key.address(),
402 expiry: NonZeroU64::new(1_700_000_600).unwrap(),
403 scope: vec![CallScope { target: Address::from([0x33; 20]), selector_rules: vec![] }],
404 spend_limits: vec![],
405 };
406 let prepared = request.prepare(1_700_000_000).unwrap();
407 let signature = root.sign_hash_sync(&prepared.authorization.signature_hash()).unwrap();
408 let signed = prepared.authorization.into_signed(PrimitiveSignature::Secp256k1(signature));
409
410 let mut other_request = request;
411 other_request.session_id = B256::from([0x71; 32]);
412 let other_prepared = other_request.prepare(1_700_000_000).unwrap();
413
414 let error = other_prepared.into_active_entry(session_key, &signed).unwrap_err();
415
416 assert!(error.to_string().contains("witness"));
417 }
418
419 #[test]
420 fn signed_session_authorization_accepts_order_independent_policy_match() {
421 let root: PrivateKeySigner = ROOT_PRIVATE_KEY.parse().unwrap();
422 let session_key = GeneratedSessionKey::from_private_key(SESSION_PRIVATE_KEY).unwrap();
423 let token_a = Address::from([0x44; 20]);
424 let token_b = Address::from([0x45; 20]);
425 let target_a = Address::from([0x46; 20]);
426 let target_b = Address::from([0x47; 20]);
427 let recipient_a = Address::from([0x48; 20]);
428 let recipient_b = Address::from([0x49; 20]);
429 let request = SessionAuthorizationRequest {
430 session_id: B256::from([0x69; 32]),
431 root_account: root.address(),
432 chain_id: 4217,
433 key_address: session_key.address(),
434 expiry: NonZeroU64::new(1_700_000_600).unwrap(),
435 scope: vec![
436 CallScope {
437 target: target_a,
438 selector_rules: vec![SelectorRule {
439 selector: [0x12, 0x34, 0x56, 0x78],
440 recipients: vec![recipient_a, recipient_b],
441 }],
442 },
443 CallScope { target: target_b, selector_rules: vec![] },
444 ],
445 spend_limits: vec![
446 SessionSpendLimit { token: token_a, amount: U256::from(1) },
447 SessionSpendLimit { token: token_b, amount: U256::from(2) },
448 ],
449 };
450 let prepared = request.prepare(1_700_000_000).unwrap();
451 let mut authorization = prepared.authorization.clone();
452 authorization.limits.as_mut().unwrap().reverse();
453 authorization.allowed_calls.as_mut().unwrap().reverse();
454 authorization.allowed_calls.as_mut().unwrap()[1].selector_rules[0].recipients.reverse();
455 let signature = root.sign_hash_sync(&authorization.signature_hash()).unwrap();
456 let signed = authorization.into_signed(PrimitiveSignature::Secp256k1(signature));
457
458 let entry = prepared.into_active_entry(session_key, &signed).unwrap();
459
460 assert_eq!(entry.status, SessionStatus::Active);
461 }
462
463 #[test]
464 fn generated_session_key_roundtrips_without_debug_leaking_private_key() {
465 let session_key = GeneratedSessionKey::from_private_key(SESSION_PRIVATE_KEY).unwrap();
466
467 assert_eq!(session_key.private_key(), SESSION_PRIVATE_KEY);
468 assert_ne!(session_key.address(), Address::ZERO);
469 assert!(!format!("{session_key:?}").contains(SESSION_PRIVATE_KEY));
470 }
471}