1use super::{KeyType, registry::*, tempo_home};
4use alloy_primitives::{Address, B256, Selector, U256};
5use alloy_signer::Signer;
6use eyre::ensure;
7use foundry_wallets::{TempoAccessKeyConfig, WalletSigner};
8use serde::{Deserialize, Serialize};
9use std::{fmt, num::NonZeroU64, path::PathBuf};
10use tempo_primitives::transaction::{
11 CallScope, KeyAuthorization, SelectorRule, SignatureType, SignedKeyAuthorization, TokenLimit,
12};
13
14pub const WALLET_SESSIONS_PATH: &str = "wallet/sessions.toml";
16
17const SESSIONS_HEADER: &str =
18 "# Tempo session registry — managed by Foundry / Tempo CLI.\n# Do not edit manually.";
19
20#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
22#[serde(rename_all = "snake_case")]
23pub enum SessionStatus {
24 #[default]
25 Pending,
26 Active,
27 Revoking,
30 Revoked,
31 Expired,
32 Failed,
33}
34
35impl SessionStatus {
36 pub const fn is_terminal(self) -> bool {
38 matches!(self, Self::Revoked | Self::Expired | Self::Failed)
39 }
40
41 const fn clears_key_material(self) -> bool {
43 matches!(self, Self::Revoking) || self.is_terminal()
44 }
45
46 pub const fn is_live(self) -> bool {
49 !self.is_terminal()
50 }
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
55pub struct SessionTokenLimit {
56 pub currency: Address,
57 pub limit: String,
58}
59
60#[derive(Clone, PartialEq, Eq, Deserialize, Serialize)]
66pub struct SessionKeyMaterial {
67 #[serde(default)]
68 pub key_type: KeyType,
69 pub key: String,
71 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub key_authorization: Option<String>,
75}
76
77impl fmt::Debug for SessionKeyMaterial {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 f.debug_struct("SessionKeyMaterial")
81 .field("key_type", &self.key_type)
82 .field("key", &super::redacted_debug(&self.key))
83 .field(
84 "key_authorization",
85 &self.key_authorization.as_deref().map(super::redacted_debug),
86 )
87 .finish()
88 }
89}
90
91impl SessionKeyMaterial {
92 pub fn has_inline_key(&self) -> bool {
94 !self.key.trim().is_empty()
95 }
96}
97
98#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
100pub struct SessionSelectorRule {
101 pub selector: Selector,
102 #[serde(default, skip_serializing_if = "Vec::is_empty")]
103 pub recipients: Vec<Address>,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
108pub struct SessionCallScope {
109 pub target: Address,
110 #[serde(default, skip_serializing_if = "Vec::is_empty")]
112 pub selector_rules: Vec<SessionSelectorRule>,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
117pub struct SessionEntry {
118 pub session_id: B256,
119 pub root_account: Address,
120 pub chain_id: u64,
121 pub key_address: Address,
122 pub expiry: u64,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub scope: Option<Vec<SessionCallScope>>,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub limits: Option<Vec<SessionTokenLimit>>,
135 #[serde(default)]
136 pub status: SessionStatus,
137 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub key: Option<SessionKeyMaterial>,
141}
142
143impl SessionEntry {
144 pub const fn is_expired_at(&self, now: u64) -> bool {
146 now >= self.expiry
147 }
148
149 pub fn has_inline_key(&self) -> bool {
151 self.key.as_ref().is_some_and(SessionKeyMaterial::has_inline_key)
152 }
153
154 pub fn has_live_key_at(&self, now: u64) -> bool {
156 self.status == SessionStatus::Active && !self.is_expired_at(now) && self.has_inline_key()
157 }
158}
159
160#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
162pub struct SessionRecord {
163 #[serde(default)]
164 pub sessions: Vec<SessionEntry>,
165}
166
167impl SessionRecord {
168 pub const fn is_empty(&self) -> bool {
170 self.sessions.is_empty()
171 }
172
173 pub fn upsert(&mut self, entry: SessionEntry) {
175 self.sessions.retain(|session| session.session_id != entry.session_id);
176 self.sessions.push(entry);
177 }
178
179 pub fn remove(&mut self, session_id: B256) -> bool {
181 let before = self.sessions.len();
182 self.sessions.retain(|session| session.session_id != session_id);
183 self.sessions.len() != before
184 }
185
186 pub fn get(&self, session_id: B256) -> Option<&SessionEntry> {
188 self.sessions.iter().find(|session| session.session_id == session_id)
189 }
190
191 pub fn live_key(&self, session_id: B256, now: u64) -> Option<&SessionEntry> {
193 self.get(session_id).filter(|session| session.has_live_key_at(now))
194 }
195
196 pub fn set_status(&mut self, session_id: B256, status: SessionStatus) -> bool {
201 let Some(session) =
202 self.sessions.iter_mut().find(|session| session.session_id == session_id)
203 else {
204 return false;
205 };
206
207 set_session_status(session, status)
208 }
209
210 pub fn mark_expired(&mut self, now: u64) -> usize {
212 let mut updated = 0;
213 for session in &mut self.sessions {
214 let should_expire = session.status.is_live() && session.is_expired_at(now);
215 let should_clear_key =
216 session.key.is_some() && (should_expire || session.status.clears_key_material());
217
218 if should_expire {
219 session.status = SessionStatus::Expired;
220 }
221 if should_clear_key {
222 session.key = None;
223 }
224 if should_expire || should_clear_key {
225 updated += 1;
226 }
227 }
228 updated
229 }
230}
231
232fn set_session_status(session: &mut SessionEntry, status: SessionStatus) -> bool {
233 let changed = session.status != status || status.clears_key_material() && session.key.is_some();
234 if !changed {
235 return false;
236 }
237
238 session.status = status;
239 if status.clears_key_material() {
240 session.key = None;
241 }
242 true
243}
244
245#[derive(Debug)]
247pub struct ResolvedSessionSigner {
248 pub session: SessionEntry,
249 pub signer: WalletSigner,
250 pub access_key: TempoAccessKeyConfig,
251}
252
253pub fn session_registry_path() -> Option<PathBuf> {
255 tempo_home().map(|home| home.join(WALLET_SESSIONS_PATH))
256}
257
258pub fn read_session_record() -> Option<SessionRecord> {
263 let path = session_registry_path()?;
264 match read_toml_file(&path, "tempo sessions") {
265 Ok(value) => value,
266 Err(e) => {
267 tracing::warn!(?path, %e, "failed to load tempo sessions file");
268 None
269 }
270 }
271}
272
273pub fn read_live_session_key(session_id: B256, now: u64) -> Option<SessionEntry> {
275 read_session_record()?.live_key(session_id, now).cloned()
276}
277
278pub fn read_session_entry(session_id: B256) -> eyre::Result<Option<SessionEntry>> {
280 let path =
281 session_registry_path().ok_or_else(|| eyre::eyre!("could not resolve tempo home"))?;
282 Ok(read_toml_file::<SessionRecord>(&path, "tempo sessions")?
283 .and_then(|record| record.get(session_id).cloned()))
284}
285
286pub fn resolve_live_session_signer(
288 session_id: B256,
289 now: u64,
290) -> eyre::Result<Option<ResolvedSessionSigner>> {
291 mark_expired_session_entries(now)?;
292
293 let path =
294 session_registry_path().ok_or_else(|| eyre::eyre!("could not resolve tempo home"))?;
295 let Some(record) = read_toml_file::<SessionRecord>(&path, "tempo sessions")? else {
296 return Ok(None);
297 };
298 let Some(session) = record.live_key(session_id, now).cloned() else {
299 return Ok(None);
300 };
301 let key =
302 session.key.as_ref().ok_or_else(|| eyre::eyre!("live session has no key material"))?;
303
304 let signer = foundry_wallets::utils::create_private_key_signer(&key.key)?;
305 let signer_address = signer.address();
306 if signer_address != session.key_address {
307 eyre::bail!(
308 "session {} key material resolves to {}, expected {}",
309 session.session_id,
310 signer_address,
311 session.key_address
312 );
313 }
314
315 let key_authorization = key
316 .key_authorization
317 .as_deref()
318 .map(|raw| {
319 super::decode_key_authorization::<SignedKeyAuthorization>(raw)
320 .map_err(|err| eyre::eyre!("failed to decode session key_authorization: {err}"))
321 })
322 .transpose()?;
323 if let Some(auth) = &key_authorization {
324 validate_signed_session_authorization(
325 &session,
326 key_type_to_signature_type(key.key_type),
327 auth,
328 )?;
329 }
330 let access_key = TempoAccessKeyConfig {
331 wallet_address: session.root_account,
332 key_address: session.key_address,
333 key_authorization,
334 };
335
336 Ok(Some(ResolvedSessionSigner { session, signer, access_key }))
337}
338
339pub(crate) fn validate_signed_session_authorization(
341 session: &SessionEntry,
342 expected_key_type: SignatureType,
343 authorization: &SignedKeyAuthorization,
344) -> eyre::Result<()> {
345 let auth = &authorization.authorization;
346 ensure!(
347 auth.key_id == session.key_address,
348 "session {} key_authorization key_id is {}, expected {}",
349 session.session_id,
350 auth.key_id,
351 session.key_address
352 );
353 ensure!(
354 auth.chain_id == session.chain_id,
355 "session {} key_authorization chain_id is {}, expected {}",
356 session.session_id,
357 auth.chain_id,
358 session.chain_id
359 );
360 ensure!(
361 auth.key_type == expected_key_type,
362 "session {} key_authorization key_type is {:?}, expected {:?}",
363 session.session_id,
364 auth.key_type,
365 expected_key_type
366 );
367 ensure!(
369 !auth.is_admin(),
370 "session {} key_authorization is an admin key, expected a limited access key",
371 session.session_id
372 );
373 if let Some(account) = auth.account {
376 ensure!(
377 account == session.root_account,
378 "session {} key_authorization is bound to account {}, expected {}",
379 session.session_id,
380 account,
381 session.root_account
382 );
383 }
384 ensure!(
386 auth.witness == Some(session.session_id),
387 "session {} key_authorization witness is {:?}, expected {}",
388 session.session_id,
389 auth.witness,
390 session.session_id
391 );
392 let recovered = authorization
393 .recover_signer()
394 .map_err(|err| eyre::eyre!("failed to recover session key_authorization signer: {err}"))?;
395 ensure!(
396 recovered == session.root_account,
397 "session {} key_authorization signer is {}, expected {}",
398 session.session_id,
399 recovered,
400 session.root_account
401 );
402 validate_session_authorization_policy(session, auth)
403}
404
405fn validate_session_authorization_policy(
407 session: &SessionEntry,
408 auth: &KeyAuthorization,
409) -> eyre::Result<()> {
410 let expected_expiry = NonZeroU64::new(session.expiry)
411 .ok_or_else(|| eyre::eyre!("session {} has invalid zero expiry", session.session_id))?;
412 ensure!(
413 auth.expiry == Some(expected_expiry),
414 "session {} key_authorization expiry is {:?}, expected {}",
415 session.session_id,
416 auth.expiry.map(NonZeroU64::get),
417 session.expiry
418 );
419
420 let expected_limits = session_authorization_limits(session)?;
421 let actual_limits = auth.limits.as_deref().map(authorization_limits);
422 ensure!(
423 actual_limits == expected_limits,
424 "session {} key_authorization limits do not match session limits",
425 session.session_id
426 );
427
428 let expected_scope = session_authorization_scope(session);
429 let actual_scope = auth.allowed_calls.as_deref().map(authorization_scope);
430 ensure!(
431 actual_scope == expected_scope,
432 "session {} key_authorization allowed_calls do not match session scope",
433 session.session_id
434 );
435
436 Ok(())
437}
438
439#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
441struct CanonicalTokenLimit {
442 token: Address,
443 limit: U256,
444 period: u64,
445}
446
447#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
449struct CanonicalCallScope {
450 target: Address,
451 selector_rules: Vec<CanonicalSelectorRule>,
452}
453
454#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
456struct CanonicalSelectorRule {
457 selector: [u8; 4],
458 recipients: Vec<Address>,
459}
460
461fn session_authorization_limits(
463 session: &SessionEntry,
464) -> eyre::Result<Option<Vec<CanonicalTokenLimit>>> {
465 let Some(limits) = session.limits.as_deref() else {
466 return Ok(None);
467 };
468 let mut limits = limits
469 .iter()
470 .map(|limit| {
471 Ok(CanonicalTokenLimit {
472 token: limit.currency,
473 limit: parse_session_limit(&limit.limit)?,
474 period: 0,
475 })
476 })
477 .collect::<eyre::Result<Vec<_>>>()?;
478 limits.sort();
479 Ok(Some(limits))
480}
481
482fn authorization_limits(limits: &[TokenLimit]) -> Vec<CanonicalTokenLimit> {
484 let mut limits = limits
485 .iter()
486 .map(|limit| CanonicalTokenLimit {
487 token: limit.token,
488 limit: limit.limit,
489 period: limit.period,
490 })
491 .collect::<Vec<_>>();
492 limits.sort();
493 limits
494}
495
496fn parse_session_limit(raw: &str) -> eyre::Result<U256> {
498 let raw = raw.trim();
499 if let Some(hex) = raw.strip_prefix("0x") { U256::from_str_radix(hex, 16) } else { raw.parse() }
500 .map_err(|err| eyre::eyre!("invalid session spending limit `{raw}`: {err}"))
501}
502
503fn session_authorization_scope(session: &SessionEntry) -> Option<Vec<CanonicalCallScope>> {
505 let mut scope = session
506 .scope
507 .as_deref()?
508 .iter()
509 .map(|scope| CanonicalCallScope {
510 target: scope.target,
511 selector_rules: session_authorization_selector_rules(&scope.selector_rules),
512 })
513 .collect::<Vec<_>>();
514 scope.sort();
515 Some(scope)
516}
517
518fn authorization_scope(scope: &[CallScope]) -> Vec<CanonicalCallScope> {
520 let mut scope = scope
521 .iter()
522 .map(|scope| CanonicalCallScope {
523 target: scope.target,
524 selector_rules: authorization_selector_rules(&scope.selector_rules),
525 })
526 .collect::<Vec<_>>();
527 scope.sort();
528 scope
529}
530
531fn session_authorization_selector_rules(
533 rules: &[SessionSelectorRule],
534) -> Vec<CanonicalSelectorRule> {
535 let mut rules = rules
536 .iter()
537 .map(|rule| {
538 let mut recipients = rule.recipients.clone();
539 recipients.sort();
540 CanonicalSelectorRule { selector: rule.selector.into(), recipients }
541 })
542 .collect::<Vec<_>>();
543 rules.sort();
544 rules
545}
546
547fn authorization_selector_rules(rules: &[SelectorRule]) -> Vec<CanonicalSelectorRule> {
549 let mut rules = rules
550 .iter()
551 .map(|rule| {
552 let mut recipients = rule.recipients.clone();
553 recipients.sort();
554 CanonicalSelectorRule { selector: rule.selector, recipients }
555 })
556 .collect::<Vec<_>>();
557 rules.sort();
558 rules
559}
560
561const fn key_type_to_signature_type(key_type: KeyType) -> SignatureType {
563 match key_type {
564 KeyType::Secp256k1 => SignatureType::Secp256k1,
565 KeyType::P256 => SignatureType::P256,
566 KeyType::WebAuthn => SignatureType::WebAuthn,
567 }
568}
569
570fn mutate_session_record<R>(f: impl FnOnce(&mut SessionRecord) -> (R, bool)) -> eyre::Result<R> {
571 let path =
572 session_registry_path().ok_or_else(|| eyre::eyre!("could not resolve tempo home"))?;
573 let mut record = read_toml_file::<SessionRecord>(&path, "tempo sessions")?.unwrap_or_default();
574 let (result, changed) = f(&mut record);
575 if changed {
576 write_toml_file_atomic(&path, &record, SESSIONS_HEADER)?;
577 }
578 Ok(result)
579}
580
581pub fn upsert_session_entry(entry: SessionEntry) -> eyre::Result<()> {
583 mutate_session_record(|record| {
584 record.upsert(entry);
585 ((), true)
586 })
587}
588
589pub fn update_session_status(session_id: B256, status: SessionStatus) -> eyre::Result<bool> {
594 mutate_session_record(|record| {
595 let changed = record.set_status(session_id, status);
596 (changed, changed)
597 })
598}
599
600pub fn update_session_status_if(
605 session_id: B256,
606 current: SessionStatus,
607 status: SessionStatus,
608) -> eyre::Result<bool> {
609 mutate_session_record(|record| {
610 let Some(session) =
611 record.sessions.iter_mut().find(|session| session.session_id == session_id)
612 else {
613 return (false, false);
614 };
615 if session.status != current {
616 return (false, false);
617 }
618
619 let changed = set_session_status(session, status);
620 (true, changed)
621 })
622}
623
624pub fn remove_session_entry(session_id: B256) -> eyre::Result<bool> {
626 mutate_session_record(|record| {
627 let removed = record.remove(session_id);
628 (removed, removed)
629 })
630}
631
632pub fn mark_expired_session_entries(now: u64) -> eyre::Result<usize> {
634 mutate_session_record(|record| {
635 let updated = record.mark_expired(now);
636 (updated, updated != 0)
637 })
638}
639
640#[cfg(test)]
641mod tests {
642 use super::*;
643 use crate::tempo::with_tempo_home;
644 use alloy_primitives::hex;
645 use alloy_rlp::Encodable;
646 use alloy_signer::SignerSync;
647 use alloy_signer_local::PrivateKeySigner;
648 use std::{fs, str::FromStr};
649 use tempo_primitives::transaction::PrimitiveSignature;
650
651 const ROOT_PRIVATE_KEY: &str =
652 "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
653 const SESSION_PRIVATE_KEY: &str =
654 "0x59c6995e998f97a5a004497e5da3b5d2b2b66a87f064d39c44da0b6d6e4f8ff0";
655
656 #[test]
657 fn debug_redacts_session_key_material() {
658 let mut entry = sample_entry_with_key(B256::from([0x77; 32]), 200, SessionStatus::Active);
660 let key = entry.key.as_mut().unwrap();
661 key.key = "0xPRIVATE_KEY_MUST_NOT_LEAK".to_string();
662 key.key_authorization = Some("0xKEY_AUTH_MUST_NOT_LEAK".to_string());
663
664 let entry_dbg = format!("{entry:?}");
665 let record_dbg = format!("{:?}", SessionRecord { sessions: vec![entry] });
666
667 for rendered in [&entry_dbg, &record_dbg] {
668 assert!(!rendered.contains("PRIVATE_KEY_MUST_NOT_LEAK"), "key leaked in: {rendered}");
669 assert!(!rendered.contains("KEY_AUTH_MUST_NOT_LEAK"), "auth leaked in: {rendered}");
670 }
671 assert!(entry_dbg.contains("key: \"<redacted>\""), "got: {entry_dbg}");
672 assert!(entry_dbg.contains("key_authorization: Some(\"<redacted>\")"), "got: {entry_dbg}");
673 assert!(entry_dbg.contains("key_type"));
675 }
676
677 fn sample_entry(session_id: B256, expiry: u64, status: SessionStatus) -> SessionEntry {
678 SessionEntry {
679 session_id,
680 root_account: Address::from_str("0x0000000000000000000000000000000000000001").unwrap(),
681 chain_id: 4217,
682 key_address: Address::from_str("0x0000000000000000000000000000000000000abc").unwrap(),
683 expiry,
684 scope: Some(vec![SessionCallScope {
685 target: Address::from_str("0x00000000000000000000000000000000000000aa").unwrap(),
686 selector_rules: vec![SessionSelectorRule {
687 selector: Selector::from_slice(&[0x12, 0x34, 0x56, 0x78]),
688 recipients: vec![],
689 }],
690 }]),
691 limits: Some(vec![SessionTokenLimit {
692 currency: Address::from_str("0x00000000000000000000000000000000000000ff").unwrap(),
693 limit: "0".to_string(),
694 }]),
695 status,
696 key: None,
697 }
698 }
699
700 fn sample_entry_with_key(session_id: B256, expiry: u64, status: SessionStatus) -> SessionEntry {
701 SessionEntry {
702 key: Some(SessionKeyMaterial {
703 key_type: KeyType::Secp256k1,
704 key: "0xdeadbeef".to_string(),
705 key_authorization: Some("0xfeed".to_string()),
706 }),
707 ..sample_entry(session_id, expiry, status)
708 }
709 }
710
711 fn sample_entry_with_valid_key(
713 session_id: B256,
714 expiry: u64,
715 status: SessionStatus,
716 ) -> SessionEntry {
717 let root_signer: PrivateKeySigner = ROOT_PRIVATE_KEY.parse().unwrap();
718 let signer = foundry_wallets::utils::create_private_key_signer(SESSION_PRIVATE_KEY)
719 .expect("valid test private key");
720 SessionEntry {
721 root_account: root_signer.address(),
722 key_address: signer.address(),
723 key: Some(SessionKeyMaterial {
724 key_type: KeyType::Secp256k1,
725 key: SESSION_PRIVATE_KEY.to_string(),
726 key_authorization: None,
727 }),
728 ..sample_entry(session_id, expiry, status)
729 }
730 }
731
732 fn signed_key_authorization_hex(entry: &SessionEntry) -> String {
734 signed_key_authorization_hex_with(entry, std::convert::identity)
735 }
736
737 fn signed_key_authorization_hex_with(
739 entry: &SessionEntry,
740 update: impl FnOnce(KeyAuthorization) -> KeyAuthorization,
741 ) -> String {
742 let root_signer: PrivateKeySigner = ROOT_PRIVATE_KEY.parse().unwrap();
743 let auth = update(session_key_authorization(entry));
744 let signature = root_signer.sign_hash_sync(&auth.signature_hash()).unwrap();
745 let signed = auth.into_signed(PrimitiveSignature::Secp256k1(signature));
746 let mut buf = Vec::new();
747 signed.encode(&mut buf);
748 hex::encode_prefixed(buf)
749 }
750
751 fn session_key_authorization(entry: &SessionEntry) -> KeyAuthorization {
753 let mut authorization = KeyAuthorization::unrestricted(
754 entry.chain_id,
755 SignatureType::Secp256k1,
756 entry.key_address,
757 )
758 .with_expiry(entry.expiry)
759 .with_witness(entry.session_id);
760 if let Some(limits) = &entry.limits {
761 authorization = authorization.with_limits(
762 limits
763 .iter()
764 .map(|limit| TokenLimit {
765 token: limit.currency,
766 limit: parse_session_limit(&limit.limit).unwrap(),
767 period: 0,
768 })
769 .collect(),
770 );
771 }
772 if let Some(scope) = &entry.scope {
773 authorization = authorization.with_allowed_calls(
774 scope
775 .iter()
776 .map(|scope| CallScope {
777 target: scope.target,
778 selector_rules: scope
779 .selector_rules
780 .iter()
781 .map(|rule| SelectorRule {
782 selector: rule.selector.into(),
783 recipients: rule.recipients.clone(),
784 })
785 .collect(),
786 })
787 .collect(),
788 );
789 }
790 authorization
791 }
792
793 #[test]
794 fn session_registry_is_separate_from_keys_registry() {
795 with_tempo_home(|| {
796 let session_id = B256::from([0x11; 32]);
797 upsert_session_entry(sample_entry(session_id, 100, SessionStatus::Pending)).unwrap();
798
799 let session_path = session_registry_path().unwrap();
800 let keys_path = crate::tempo::tempo_keys_path().unwrap();
801 assert_eq!(session_path.file_name().and_then(|s| s.to_str()), Some("sessions.toml"));
802 assert_eq!(keys_path.file_name().and_then(|s| s.to_str()), Some("keys.toml"));
803 assert_ne!(session_path, keys_path);
804
805 let record = read_session_record().unwrap();
806 assert_eq!(record.sessions.len(), 1);
807 assert_eq!(record.sessions[0].session_id, session_id);
808 });
809 }
810
811 #[test]
812 fn session_registry_upsert_replaces_matching_session_id() {
813 with_tempo_home(|| {
814 let session_id = B256::from([0x22; 32]);
815 upsert_session_entry(sample_entry(session_id, 100, SessionStatus::Pending)).unwrap();
816 upsert_session_entry(sample_entry(session_id, 200, SessionStatus::Active)).unwrap();
817
818 let record = read_session_record().unwrap();
819 assert_eq!(record.sessions.len(), 1);
820 assert_eq!(record.sessions[0].expiry, 200);
821 assert_eq!(record.sessions[0].status, SessionStatus::Active);
822 });
823 }
824
825 #[test]
826 fn session_registry_remove_deletes_entry() {
827 with_tempo_home(|| {
828 let session_id = B256::from([0x33; 32]);
829 upsert_session_entry(sample_entry(session_id, 100, SessionStatus::Active)).unwrap();
830 assert!(remove_session_entry(session_id).unwrap());
831 assert!(read_session_record().unwrap().is_empty());
832 });
833 }
834
835 #[test]
836 fn session_record_marks_expired_live_entries() {
837 let mut record = SessionRecord {
838 sessions: vec![
839 sample_entry(B256::from([0x44; 32]), 10, SessionStatus::Pending),
840 sample_entry(B256::from([0x55; 32]), 10, SessionStatus::Revoked),
841 ],
842 };
843
844 assert_eq!(record.mark_expired(11), 1);
845 assert_eq!(record.sessions[0].status, SessionStatus::Expired);
846 assert_eq!(record.sessions[1].status, SessionStatus::Revoked);
847 }
848
849 #[test]
850 fn session_record_status_updates_clear_cleanup_and_terminal_keys() {
851 let active_id = B256::from([0x67; 32]);
852 let revoking_id = B256::from([0x68; 32]);
853 let revoked_id = B256::from([0x69; 32]);
854 let failed_id = B256::from([0x6a; 32]);
855 let missing_id = B256::from([0x6b; 32]);
856 let mut record = SessionRecord {
857 sessions: vec![
858 sample_entry_with_key(active_id, 200, SessionStatus::Pending),
859 sample_entry_with_key(revoking_id, 200, SessionStatus::Active),
860 sample_entry_with_key(revoked_id, 200, SessionStatus::Revoking),
861 sample_entry_with_key(failed_id, 200, SessionStatus::Pending),
862 ],
863 };
864
865 assert!(record.set_status(active_id, SessionStatus::Active));
866 assert!(record.set_status(revoking_id, SessionStatus::Revoking));
867 assert!(record.set_status(revoked_id, SessionStatus::Revoked));
868 assert!(record.set_status(failed_id, SessionStatus::Failed));
869 assert!(!record.set_status(missing_id, SessionStatus::Active));
870 assert!(!record.set_status(active_id, SessionStatus::Active));
871
872 assert_eq!(record.get(active_id).unwrap().status, SessionStatus::Active);
873 assert!(record.get(active_id).unwrap().key.is_some());
874 assert_eq!(record.get(revoking_id).unwrap().status, SessionStatus::Revoking);
875 assert!(record.get(revoking_id).unwrap().key.is_none());
876 assert_eq!(record.get(revoked_id).unwrap().status, SessionStatus::Revoked);
877 assert!(record.get(revoked_id).unwrap().key.is_none());
878 assert_eq!(record.get(failed_id).unwrap().status, SessionStatus::Failed);
879 assert!(record.get(failed_id).unwrap().key.is_none());
880 }
881
882 #[test]
883 fn session_entry_roundtrips_scope_limits_and_status() {
884 let entry = sample_entry(B256::from([0x66; 32]), 1234, SessionStatus::Revoking);
885 let toml = toml::to_string(&entry).unwrap();
886 let decoded: SessionEntry = toml::from_str(&toml).unwrap();
887
888 assert_eq!(decoded.session_id, entry.session_id);
889 assert_eq!(decoded.scope.as_ref().unwrap().len(), 1);
890 assert_eq!(decoded.limits.as_ref().unwrap().len(), 1);
891 assert_eq!(decoded.status, SessionStatus::Revoking);
892 assert!(decoded.key.is_none());
893 assert!(!decoded.has_inline_key());
894 assert!(decoded.is_expired_at(1234));
895 }
896
897 #[test]
898 fn live_session_key_requires_key_material_live_status_and_unexpired_entry() {
899 let live_id = B256::from([0x01; 32]);
900 let expired_id = B256::from([0x02; 32]);
901 let revoked_id = B256::from([0x03; 32]);
902 let no_key_id = B256::from([0x04; 32]);
903 let pending_id = B256::from([0x05; 32]);
904 let revoking_id = B256::from([0x06; 32]);
905
906 let record = SessionRecord {
907 sessions: vec![
908 sample_entry_with_key(live_id, 200, SessionStatus::Active),
909 sample_entry_with_key(expired_id, 100, SessionStatus::Active),
910 sample_entry_with_key(revoked_id, 200, SessionStatus::Revoked),
911 sample_entry(no_key_id, 200, SessionStatus::Active),
912 sample_entry_with_key(pending_id, 200, SessionStatus::Pending),
913 sample_entry_with_key(revoking_id, 200, SessionStatus::Revoking),
914 ],
915 };
916
917 assert_eq!(record.live_key(live_id, 100).unwrap().session_id, live_id);
918 assert!(record.live_key(expired_id, 100).is_none());
919 assert!(record.live_key(revoked_id, 100).is_none());
920 assert!(record.live_key(no_key_id, 100).is_none());
921 assert!(record.live_key(pending_id, 100).is_none());
922 assert!(record.live_key(revoking_id, 100).is_none());
923 }
924
925 #[test]
926 fn resolve_live_session_signer_returns_signer_and_access_key_config() {
927 with_tempo_home(|| {
928 let session_id = B256::from([0x06; 32]);
929 let entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active);
930 upsert_session_entry(entry.clone()).unwrap();
931
932 let resolved = resolve_live_session_signer(session_id, 100).unwrap().unwrap();
933
934 assert_eq!(resolved.session, entry);
935 assert_eq!(Signer::address(&resolved.signer), entry.key_address);
936 assert_eq!(resolved.access_key.wallet_address, entry.root_account);
937 assert_eq!(resolved.access_key.key_address, entry.key_address);
938 assert!(resolved.access_key.key_authorization.is_none());
939 });
940 }
941
942 #[test]
943 fn resolve_live_session_signer_rejects_mismatched_private_key() {
944 with_tempo_home(|| {
945 let session_id = B256::from([0x07; 32]);
946 let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active);
947 entry.key_address =
948 Address::from_str("0x0000000000000000000000000000000000000abc").unwrap();
949 upsert_session_entry(entry).unwrap();
950
951 let error = resolve_live_session_signer(session_id, 100).unwrap_err();
952
953 assert!(error.to_string().contains("key material resolves to"));
954 });
955 }
956
957 #[test]
958 fn resolve_live_session_signer_expires_stale_entries_before_resolving() {
959 with_tempo_home(|| {
960 let session_id = B256::from([0x08; 32]);
961 upsert_session_entry(sample_entry_with_valid_key(
962 session_id,
963 100,
964 SessionStatus::Active,
965 ))
966 .unwrap();
967
968 assert!(resolve_live_session_signer(session_id, 100).unwrap().is_none());
969
970 let record = read_session_record().unwrap();
971 let session = record.get(session_id).unwrap();
972 assert_eq!(session.status, SessionStatus::Expired);
973 assert!(session.key.is_none());
974 });
975 }
976
977 #[test]
978 fn resolve_live_session_signer_decodes_and_validates_key_authorization() {
979 with_tempo_home(|| {
980 let session_id = B256::from([0x09; 32]);
981 let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active);
982 let auth = signed_key_authorization_hex(&entry);
983 entry.key.as_mut().unwrap().key_authorization = Some(auth);
984 upsert_session_entry(entry.clone()).unwrap();
985
986 let resolved = resolve_live_session_signer(session_id, 100).unwrap().unwrap();
987 let key_authorization = resolved.access_key.key_authorization.unwrap();
988
989 assert_eq!(key_authorization.authorization.key_id, entry.key_address);
990 assert_eq!(key_authorization.authorization.chain_id, entry.chain_id);
991 assert_eq!(key_authorization.authorization.key_type, SignatureType::Secp256k1);
992 assert_eq!(key_authorization.authorization.expiry.unwrap().get(), entry.expiry);
993 assert!(key_authorization.authorization.limits.is_some());
994 assert!(key_authorization.authorization.allowed_calls.is_some());
995 assert_eq!(key_authorization.recover_signer().unwrap(), entry.root_account);
996 });
997 }
998
999 #[test]
1000 fn resolve_live_session_signer_accepts_unrestricted_authorization_when_policy_is_omitted() {
1001 with_tempo_home(|| {
1002 let session_id = B256::from([0x13; 32]);
1003 let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active);
1004 entry.limits = None;
1005 entry.scope = None;
1006 entry.key.as_mut().unwrap().key_authorization =
1007 Some(signed_key_authorization_hex(&entry));
1008 upsert_session_entry(entry.clone()).unwrap();
1009
1010 let resolved = resolve_live_session_signer(session_id, 100).unwrap().unwrap();
1011 let key_authorization = resolved.access_key.key_authorization.unwrap();
1012
1013 assert!(key_authorization.authorization.limits.is_none());
1014 assert!(key_authorization.authorization.allowed_calls.is_none());
1015 });
1016 }
1017
1018 #[test]
1019 fn resolve_live_session_signer_rejects_unrestricted_authorization_when_policy_is_empty() {
1020 with_tempo_home(|| {
1021 let session_id = B256::from([0x14; 32]);
1022 let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active);
1023 let mut auth_entry = entry.clone();
1024 auth_entry.limits = None;
1025 auth_entry.scope = None;
1026 entry.limits = Some(vec![]);
1027 entry.scope = Some(vec![]);
1028 entry.key.as_mut().unwrap().key_authorization =
1029 Some(signed_key_authorization_hex(&auth_entry));
1030 upsert_session_entry(entry).unwrap();
1031
1032 let error = resolve_live_session_signer(session_id, 100).unwrap_err();
1033
1034 assert!(error.to_string().contains("limits"));
1035 });
1036 }
1037
1038 #[test]
1039 fn resolve_live_session_signer_rejects_invalid_key_authorization() {
1040 with_tempo_home(|| {
1041 let session_id = B256::from([0x0a; 32]);
1042 let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active);
1043 entry.key.as_mut().unwrap().key_authorization = Some("0xdeadbeef".to_string());
1044 upsert_session_entry(entry).unwrap();
1045
1046 let error = resolve_live_session_signer(session_id, 100).unwrap_err();
1047
1048 assert!(error.to_string().contains("key_authorization"));
1049 });
1050 }
1051
1052 #[test]
1053 fn resolve_live_session_signer_rejects_authorization_for_wrong_chain() {
1054 with_tempo_home(|| {
1055 let session_id = B256::from([0x0b; 32]);
1056 let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active);
1057 let mut auth_entry = entry.clone();
1058 auth_entry.chain_id += 1;
1059 entry.key.as_mut().unwrap().key_authorization =
1060 Some(signed_key_authorization_hex(&auth_entry));
1061 upsert_session_entry(entry).unwrap();
1062
1063 let error = resolve_live_session_signer(session_id, 100).unwrap_err();
1064
1065 assert!(error.to_string().contains("chain_id"));
1066 });
1067 }
1068
1069 #[test]
1070 fn resolve_live_session_signer_rejects_authorization_without_session_expiry() {
1071 with_tempo_home(|| {
1072 let session_id = B256::from([0x0d; 32]);
1073 let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active);
1074 entry.key.as_mut().unwrap().key_authorization =
1075 Some(signed_key_authorization_hex_with(&entry, |mut auth| {
1076 auth.expiry = None;
1077 auth
1078 }));
1079 upsert_session_entry(entry).unwrap();
1080
1081 let error = resolve_live_session_signer(session_id, 100).unwrap_err();
1082
1083 assert!(error.to_string().contains("expiry"));
1084 });
1085 }
1086
1087 #[test]
1088 fn resolve_live_session_signer_rejects_authorization_without_session_limits() {
1089 with_tempo_home(|| {
1090 let session_id = B256::from([0x0e; 32]);
1091 let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active);
1092 entry.key.as_mut().unwrap().key_authorization =
1093 Some(signed_key_authorization_hex_with(&entry, |mut auth| {
1094 auth.limits = None;
1095 auth
1096 }));
1097 upsert_session_entry(entry).unwrap();
1098
1099 let error = resolve_live_session_signer(session_id, 100).unwrap_err();
1100
1101 assert!(error.to_string().contains("limits"));
1102 });
1103 }
1104
1105 #[test]
1106 fn resolve_live_session_signer_rejects_authorization_without_session_scope() {
1107 with_tempo_home(|| {
1108 let session_id = B256::from([0x0f; 32]);
1109 let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active);
1110 entry.key.as_mut().unwrap().key_authorization =
1111 Some(signed_key_authorization_hex_with(&entry, |mut auth| {
1112 auth.allowed_calls = None;
1113 auth
1114 }));
1115 upsert_session_entry(entry).unwrap();
1116
1117 let error = resolve_live_session_signer(session_id, 100).unwrap_err();
1118
1119 assert!(error.to_string().contains("allowed_calls"));
1120 });
1121 }
1122
1123 #[test]
1124 fn resolve_live_session_signer_rejects_authorization_for_wrong_session_id() {
1125 with_tempo_home(|| {
1126 let session_id = B256::from([0x15; 32]);
1127 let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active);
1128 entry.key.as_mut().unwrap().key_authorization =
1129 Some(signed_key_authorization_hex_with(&entry, |auth| {
1130 auth.with_witness(B256::from([0x16; 32]))
1131 }));
1132 upsert_session_entry(entry).unwrap();
1133
1134 let error = resolve_live_session_signer(session_id, 100).unwrap_err();
1135
1136 assert!(error.to_string().contains("witness"));
1137 });
1138 }
1139
1140 #[test]
1141 fn resolve_rejects_admin_key_authorization() {
1142 with_tempo_home(|| {
1143 let session_id = B256::from([0x17; 32]);
1144 let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active);
1145 entry.key.as_mut().unwrap().key_authorization =
1147 Some(signed_key_authorization_hex_with(&entry, |mut auth| {
1148 auth.is_admin = true;
1149 auth
1150 }));
1151 upsert_session_entry(entry).unwrap();
1152
1153 let error = resolve_live_session_signer(session_id, 100).unwrap_err();
1154
1155 assert!(error.to_string().contains("admin key"), "got: {error}");
1156 });
1157 }
1158
1159 #[test]
1160 fn resolve_rejects_account_bound_to_other_account() {
1161 with_tempo_home(|| {
1162 let session_id = B256::from([0x18; 32]);
1163 let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active);
1164 entry.key.as_mut().unwrap().key_authorization =
1166 Some(signed_key_authorization_hex_with(&entry, |auth| {
1167 auth.with_account(
1168 Address::from_str("0x000000000000000000000000000000000000dead").unwrap(),
1169 )
1170 }));
1171 upsert_session_entry(entry).unwrap();
1172
1173 let error = resolve_live_session_signer(session_id, 100).unwrap_err();
1174
1175 assert!(error.to_string().contains("bound to account"), "got: {error}");
1176 });
1177 }
1178
1179 #[test]
1180 fn resolve_accepts_account_bound_to_root() {
1181 with_tempo_home(|| {
1182 let session_id = B256::from([0x19; 32]);
1183 let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active);
1184 let root_account = entry.root_account;
1185 entry.key.as_mut().unwrap().key_authorization =
1187 Some(signed_key_authorization_hex_with(&entry, |auth| {
1188 auth.with_account(root_account)
1189 }));
1190 upsert_session_entry(entry).unwrap();
1191
1192 assert!(resolve_live_session_signer(session_id, 100).is_ok());
1193 });
1194 }
1195
1196 #[test]
1197 fn resolve_live_session_signer_rejects_authorization_with_wider_session_limit() {
1198 with_tempo_home(|| {
1199 let session_id = B256::from([0x10; 32]);
1200 let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active);
1201 entry.key.as_mut().unwrap().key_authorization =
1202 Some(signed_key_authorization_hex_with(&entry, |mut auth| {
1203 auth.limits.as_mut().unwrap()[0].limit = U256::from(1);
1204 auth
1205 }));
1206 upsert_session_entry(entry).unwrap();
1207
1208 let error = resolve_live_session_signer(session_id, 100).unwrap_err();
1209
1210 assert!(error.to_string().contains("limits"));
1211 });
1212 }
1213
1214 #[test]
1215 fn resolve_live_session_signer_rejects_authorization_with_wider_session_scope() {
1216 with_tempo_home(|| {
1217 let session_id = B256::from([0x12; 32]);
1218 let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active);
1219 entry.key.as_mut().unwrap().key_authorization =
1220 Some(signed_key_authorization_hex_with(&entry, |mut auth| {
1221 auth.allowed_calls.as_mut().unwrap()[0].selector_rules.clear();
1222 auth
1223 }));
1224 upsert_session_entry(entry).unwrap();
1225
1226 let error = resolve_live_session_signer(session_id, 100).unwrap_err();
1227
1228 assert!(error.to_string().contains("allowed_calls"));
1229 });
1230 }
1231
1232 #[test]
1233 fn resolve_live_session_signer_fails_closed_when_session_file_is_corrupt() {
1234 with_tempo_home(|| {
1235 let path = session_registry_path().unwrap();
1236 fs::create_dir_all(path.parent().unwrap()).unwrap();
1237 fs::write(&path, "sessions = [").unwrap();
1238 let original = fs::read_to_string(&path).unwrap();
1239
1240 assert!(resolve_live_session_signer(B256::from([0x0c; 32]), 100).is_err());
1241 assert_eq!(fs::read_to_string(&path).unwrap(), original);
1242 });
1243 }
1244
1245 #[test]
1246 fn session_key_storage_does_not_replace_persistent_keys_file() {
1247 with_tempo_home(|| {
1248 let keys_path = crate::tempo::tempo_keys_path().unwrap();
1249 fs::create_dir_all(keys_path.parent().unwrap()).unwrap();
1250 let original_keys = r#"[[keys]]
1251wallet_type = "local"
1252wallet_address = "0x0000000000000000000000000000000000000001"
1253chain_id = 4217
1254key_type = "secp256k1"
1255key_address = "0x0000000000000000000000000000000000000001"
1256key = "0x1111"
1257expiry = 999
1258"#;
1259 fs::write(&keys_path, original_keys).unwrap();
1260
1261 let session_id = B256::from([0x99; 32]);
1262 upsert_session_entry(sample_entry_with_key(session_id, 200, SessionStatus::Active))
1263 .unwrap();
1264
1265 assert_eq!(fs::read_to_string(&keys_path).unwrap(), original_keys);
1266 let session = read_live_session_key(session_id, 100).unwrap();
1267 assert_eq!(session.key.unwrap().key, "0xdeadbeef");
1268 });
1269 }
1270
1271 #[test]
1272 fn removing_session_key_preserves_persistent_key() {
1273 with_tempo_home(|| {
1274 let keys_path = crate::tempo::tempo_keys_path().unwrap();
1275 fs::create_dir_all(keys_path.parent().unwrap()).unwrap();
1276 let original_keys = r#"[[keys]]
1277wallet_type = "local"
1278wallet_address = "0x0000000000000000000000000000000000000001"
1279chain_id = 4217
1280key_type = "secp256k1"
1281key_address = "0x0000000000000000000000000000000000000001"
1282key = "0x1111"
1283"#;
1284 fs::write(&keys_path, original_keys).unwrap();
1285
1286 let session_id = B256::from([0xaa; 32]);
1287 upsert_session_entry(sample_entry_with_key(session_id, 200, SessionStatus::Active))
1288 .unwrap();
1289 assert!(remove_session_entry(session_id).unwrap());
1290
1291 assert_eq!(fs::read_to_string(&keys_path).unwrap(), original_keys);
1292 assert!(read_session_record().unwrap().is_empty());
1293 });
1294 }
1295
1296 #[test]
1297 fn mark_expired_session_entries_persists_status_without_touching_keys_file() {
1298 with_tempo_home(|| {
1299 let keys_path = crate::tempo::tempo_keys_path().unwrap();
1300 fs::create_dir_all(keys_path.parent().unwrap()).unwrap();
1301 let original_keys = "[[keys]]\nkey = \"0x1111\"\n";
1302 fs::write(&keys_path, original_keys).unwrap();
1303
1304 let session_id = B256::from([0xbb; 32]);
1305 upsert_session_entry(sample_entry_with_key(session_id, 100, SessionStatus::Active))
1306 .unwrap();
1307
1308 assert_eq!(mark_expired_session_entries(100).unwrap(), 1);
1309 let record = read_session_record().unwrap();
1310 let session = record.get(session_id).unwrap();
1311 assert_eq!(session.status, SessionStatus::Expired);
1312 assert!(session.key.is_none());
1313 assert!(read_live_session_key(session_id, 100).is_none());
1314 assert_eq!(fs::read_to_string(&keys_path).unwrap(), original_keys);
1315 });
1316 }
1317
1318 #[test]
1319 fn mark_expired_session_entries_clears_unusable_session_keys() {
1320 with_tempo_home(|| {
1321 let expired_id = B256::from([0xbc; 32]);
1322 let revoked_id = B256::from([0xbd; 32]);
1323 let failed_id = B256::from([0xbe; 32]);
1324 let revoking_id = B256::from([0xbf; 32]);
1325
1326 upsert_session_entry(sample_entry_with_key(expired_id, 100, SessionStatus::Expired))
1327 .unwrap();
1328 upsert_session_entry(sample_entry_with_key(revoked_id, 200, SessionStatus::Revoked))
1329 .unwrap();
1330 upsert_session_entry(sample_entry_with_key(failed_id, 200, SessionStatus::Failed))
1331 .unwrap();
1332 upsert_session_entry(sample_entry_with_key(revoking_id, 200, SessionStatus::Revoking))
1333 .unwrap();
1334
1335 assert_eq!(mark_expired_session_entries(100).unwrap(), 4);
1336 let record = read_session_record().unwrap();
1337 for session_id in [expired_id, revoked_id, failed_id, revoking_id] {
1338 assert!(record.get(session_id).unwrap().key.is_none());
1339 }
1340 assert_eq!(record.get(expired_id).unwrap().status, SessionStatus::Expired);
1341 assert_eq!(record.get(revoked_id).unwrap().status, SessionStatus::Revoked);
1342 assert_eq!(record.get(failed_id).unwrap().status, SessionStatus::Failed);
1343 assert_eq!(record.get(revoking_id).unwrap().status, SessionStatus::Revoking);
1344 });
1345 }
1346
1347 #[test]
1348 fn update_session_status_persists_lifecycle_state_and_key_cleanup() {
1349 with_tempo_home(|| {
1350 let session_id = B256::from([0xbf; 32]);
1351 upsert_session_entry(sample_entry_with_key(session_id, 200, SessionStatus::Pending))
1352 .unwrap();
1353
1354 assert!(update_session_status(session_id, SessionStatus::Active).unwrap());
1355 let record = read_session_record().unwrap();
1356 let session = record.get(session_id).unwrap();
1357 assert_eq!(session.status, SessionStatus::Active);
1358 assert!(session.key.is_some());
1359
1360 assert!(update_session_status(session_id, SessionStatus::Revoking).unwrap());
1361 let record = read_session_record().unwrap();
1362 let session = record.get(session_id).unwrap();
1363 assert_eq!(session.status, SessionStatus::Revoking);
1364 assert!(session.key.is_none());
1365
1366 assert!(update_session_status(session_id, SessionStatus::Revoked).unwrap());
1367 let record = read_session_record().unwrap();
1368 let session = record.get(session_id).unwrap();
1369 assert_eq!(session.status, SessionStatus::Revoked);
1370 assert!(session.key.is_none());
1371 assert!(read_live_session_key(session_id, 100).is_none());
1372
1373 assert!(!update_session_status(session_id, SessionStatus::Revoked).unwrap());
1374 assert!(!update_session_status(B256::from([0xc0; 32]), SessionStatus::Failed).unwrap());
1375 });
1376 }
1377
1378 #[test]
1379 fn update_session_status_to_failed_clears_key_material() {
1380 with_tempo_home(|| {
1381 let session_id = B256::from([0xc1; 32]);
1382 upsert_session_entry(sample_entry_with_key(session_id, 200, SessionStatus::Active))
1383 .unwrap();
1384
1385 assert!(update_session_status(session_id, SessionStatus::Failed).unwrap());
1386
1387 let record = read_session_record().unwrap();
1388 let session = record.get(session_id).unwrap();
1389 assert_eq!(session.status, SessionStatus::Failed);
1390 assert!(session.key.is_none());
1391 });
1392 }
1393
1394 #[test]
1395 fn update_session_status_if_only_updates_matching_current_status() {
1396 with_tempo_home(|| {
1397 let session_id = B256::from([0xc2; 32]);
1398 upsert_session_entry(sample_entry_with_key(session_id, 200, SessionStatus::Active))
1399 .unwrap();
1400
1401 assert!(
1402 update_session_status_if(
1403 session_id,
1404 SessionStatus::Active,
1405 SessionStatus::Revoking,
1406 )
1407 .unwrap()
1408 );
1409 let record = read_session_record().unwrap();
1410 let session = record.get(session_id).unwrap();
1411 assert_eq!(session.status, SessionStatus::Revoking);
1412 assert!(session.key.is_none());
1413
1414 assert!(!update_session_status_if(
1415 session_id,
1416 SessionStatus::Active,
1417 SessionStatus::Failed,
1418 )
1419 .unwrap());
1420 assert_eq!(
1421 read_session_record().unwrap().get(session_id).unwrap().status,
1422 SessionStatus::Revoking
1423 );
1424 });
1425 }
1426
1427 #[test]
1428 fn upsert_fails_closed_when_session_file_is_corrupt() {
1429 with_tempo_home(|| {
1430 let path = session_registry_path().unwrap();
1431 fs::create_dir_all(path.parent().unwrap()).unwrap();
1432 fs::write(&path, "sessions = [").unwrap();
1433 let original = fs::read_to_string(&path).unwrap();
1434
1435 let session_id = B256::from([0x77; 32]);
1436 let entry = sample_entry(session_id, 100, SessionStatus::Pending);
1437
1438 assert!(read_session_record().is_none());
1439 assert!(upsert_session_entry(entry).is_err());
1440 assert_eq!(fs::read_to_string(&path).unwrap(), original);
1441 });
1442 }
1443
1444 #[test]
1445 fn remove_fails_closed_when_session_file_is_corrupt() {
1446 with_tempo_home(|| {
1447 let path = session_registry_path().unwrap();
1448 fs::create_dir_all(path.parent().unwrap()).unwrap();
1449 fs::write(&path, "sessions = [").unwrap();
1450 let original = fs::read_to_string(&path).unwrap();
1451
1452 assert!(remove_session_entry(B256::from([0x88; 32])).is_err());
1453 assert_eq!(fs::read_to_string(&path).unwrap(), original);
1454 });
1455 }
1456
1457 #[test]
1458 fn mark_expired_fails_closed_when_session_file_is_corrupt() {
1459 with_tempo_home(|| {
1460 let path = session_registry_path().unwrap();
1461 fs::create_dir_all(path.parent().unwrap()).unwrap();
1462 fs::write(&path, "sessions = [").unwrap();
1463 let original = fs::read_to_string(&path).unwrap();
1464
1465 assert!(mark_expired_session_entries(100).is_err());
1466 assert_eq!(fs::read_to_string(&path).unwrap(), original);
1467 });
1468 }
1469
1470 #[test]
1471 fn update_session_status_fails_closed_when_session_file_is_corrupt() {
1472 with_tempo_home(|| {
1473 let path = session_registry_path().unwrap();
1474 fs::create_dir_all(path.parent().unwrap()).unwrap();
1475 fs::write(&path, "sessions = [").unwrap();
1476 let original = fs::read_to_string(&path).unwrap();
1477
1478 assert!(update_session_status(B256::from([0xc2; 32]), SessionStatus::Failed).is_err());
1479 assert_eq!(fs::read_to_string(&path).unwrap(), original);
1480 });
1481 }
1482}