1use aura_core::types::identifiers::{AuthorityId, ContextId};
28use aura_core::DeviceId;
29use aura_core::FlowCost;
30use aura_guards::types;
31use aura_signature::session::SessionScope;
32use serde::{Deserialize, Serialize};
33
34use crate::capabilities::{
35 AuthenticationCapability, GuardianAuthCapability, RecoveryAuthorizationCapability,
36};
37
38pub mod costs {
44 use aura_core::FlowCost;
45
46 pub const CHALLENGE_REQUEST_COST: FlowCost = FlowCost::new(1);
52
53 pub const PROOF_SUBMISSION_COST: FlowCost = FlowCost::new(2);
55
56 pub const PROOF_VERIFICATION_COST: FlowCost = FlowCost::new(2);
58
59 pub const SESSION_CREATION_COST: FlowCost = FlowCost::new(2);
61
62 pub const GUARDIAN_APPROVAL_REQUEST_COST: FlowCost = FlowCost::new(3);
64
65 pub const GUARDIAN_APPROVAL_DECISION_COST: FlowCost = FlowCost::new(2);
67}
68
69#[derive(Debug, Clone)]
78pub struct GuardSnapshot {
79 pub authority_id: AuthorityId,
81
82 pub context_id: Option<ContextId>,
84
85 pub device_id: Option<DeviceId>,
87
88 pub flow_budget_remaining: FlowCost,
90
91 pub capabilities: Vec<types::CapabilityId>,
93
94 pub epoch: u64,
96
97 pub now_ms: u64,
99
100 pub is_emergency: bool,
102}
103
104impl GuardSnapshot {
105 pub fn new(
107 authority_id: AuthorityId,
108 context_id: Option<ContextId>,
109 device_id: Option<DeviceId>,
110 flow_budget_remaining: FlowCost,
111 capabilities: Vec<types::CapabilityId>,
112 epoch: u64,
113 now_ms: u64,
114 ) -> Self {
115 Self {
116 authority_id,
117 context_id,
118 device_id,
119 flow_budget_remaining,
120 capabilities,
121 epoch,
122 now_ms,
123 is_emergency: false,
124 }
125 }
126
127 pub fn with_emergency(mut self, is_emergency: bool) -> Self {
129 self.is_emergency = is_emergency;
130 self
131 }
132
133 pub fn has_capability(&self, cap: &types::CapabilityId) -> bool {
135 self.capabilities.iter().any(|c| c == cap)
136 }
137
138 pub fn has_budget(&self, cost: FlowCost) -> bool {
140 self.flow_budget_remaining >= cost
141 }
142}
143
144#[derive(Debug, Clone)]
150pub enum GuardRequest {
151 ChallengeRequest {
153 scope: SessionScope,
155 },
156
157 ProofSubmission {
159 session_id: String,
161 proof_hash: [u8; 32],
163 },
164
165 ProofVerification {
167 session_id: String,
169 },
170
171 SessionCreation {
173 scope: SessionScope,
175 duration_seconds: u64,
177 },
178
179 GuardianApprovalRequest {
181 account_id: AuthorityId,
183 operation_type: RecoveryOperationType,
185 required_guardians: u32,
187 },
188
189 GuardianApprovalDecision {
191 request_id: String,
193 approved: bool,
195 },
196}
197
198#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
200pub enum RecoveryOperationType {
201 DeviceKeyRecovery,
203 AccountAccessRecovery,
205 GuardianSetModification,
207 EmergencyFreeze,
209 AccountUnfreeze,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct RecoveryContext {
219 pub operation_type: RecoveryOperationType,
221 pub justification: String,
223 pub is_emergency: bool,
225 pub timestamp: u64,
227}
228
229impl RecoveryContext {
230 pub fn new(
232 operation_type: RecoveryOperationType,
233 justification: impl Into<String>,
234 timestamp: u64,
235 ) -> Self {
236 Self {
237 operation_type,
238 justification: justification.into(),
239 is_emergency: false,
240 timestamp,
241 }
242 }
243
244 pub fn emergency(
246 operation_type: RecoveryOperationType,
247 justification: impl Into<String>,
248 timestamp: u64,
249 ) -> Self {
250 Self {
251 operation_type,
252 justification: justification.into(),
253 is_emergency: true,
254 timestamp,
255 }
256 }
257}
258
259pub type GuardDecision = types::GuardDecision;
261
262#[derive(Debug, Clone)]
271pub enum EffectCommand {
272 GenerateChallenge {
274 session_id: String,
276 expires_at_ms: u64,
278 },
279
280 SignMessage {
282 message: Vec<u8>,
284 context: String,
286 },
287
288 VerifySignature {
290 message: Vec<u8>,
292 signature: Vec<u8>,
294 public_key: Vec<u8>,
296 },
297
298 IssueSessionTicket {
300 session_id: String,
302 scope: SessionScope,
304 expires_at_ms: u64,
306 },
307
308 ChargeFlowBudget {
310 cost: FlowCost,
312 },
313
314 JournalAppend {
316 fact_type: String,
318 fact_data: Vec<u8>,
320 },
321
322 NotifyPeer {
324 peer: AuthorityId,
326 event_type: String,
328 event_data: Vec<u8>,
330 },
331
332 RecordReceipt {
334 operation: String,
336 peer: Option<AuthorityId>,
338 timestamp_ms: u64,
340 },
341
342 SendGuardianChallenge {
344 guardian_id: AuthorityId,
346 request_id: String,
348 challenge: Vec<u8>,
350 expires_at_ms: u64,
352 },
353
354 AggregateGuardianApprovals {
356 request_id: String,
358 threshold: u32,
360 },
361}
362
363pub type GuardOutcome = types::GuardOutcome<EffectCommand>;
365
366#[derive(Debug, Clone, Copy)]
368pub struct GuardReject {
369 pub code: &'static str,
370 pub category: &'static str,
371 pub message: &'static str,
372}
373
374impl std::fmt::Display for GuardReject {
375 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
376 write!(f, "[{}:{}] {}", self.category, self.code, self.message)
377 }
378}
379
380fn deny(reject: GuardReject) -> GuardOutcome {
381 GuardOutcome::denied(types::GuardViolation::other(reject.to_string()))
382}
383
384impl types::CapabilitySnapshot for GuardSnapshot {
389 fn has_capability(&self, cap: &types::CapabilityId) -> bool {
390 GuardSnapshot::has_capability(self, cap)
391 }
392}
393
394impl types::FlowBudgetSnapshot for GuardSnapshot {
395 fn flow_budget_remaining(&self) -> FlowCost {
396 self.flow_budget_remaining
397 }
398}
399
400pub fn check_capability(
402 snapshot: &GuardSnapshot,
403 required_cap: &types::CapabilityId,
404) -> Option<GuardOutcome> {
405 if snapshot.has_capability(required_cap) {
406 None
407 } else {
408 Some(deny(GuardReject {
409 code: "capability-missing",
410 category: "auth",
411 message: "Required capability missing",
412 }))
413 }
414}
415
416pub fn check_flow_budget(
418 snapshot: &GuardSnapshot,
419 required_cost: FlowCost,
420) -> Option<GuardOutcome> {
421 if snapshot.flow_budget_remaining >= required_cost {
422 None
423 } else {
424 Some(deny(GuardReject {
425 code: "flow-budget-insufficient",
426 category: "auth",
427 message: "Flow budget insufficient",
428 }))
429 }
430}
431
432pub fn check_challenge_expiry(
434 snapshot: &GuardSnapshot,
435 expires_at_ms: u64,
436) -> Option<GuardOutcome> {
437 if snapshot.now_ms > expires_at_ms {
438 Some(deny(GuardReject {
439 code: "challenge-expired",
440 category: "auth",
441 message: "Challenge has expired",
442 }))
443 } else {
444 None
445 }
446}
447
448pub fn check_session_duration(duration_seconds: u64) -> Option<GuardOutcome> {
450 const MAX_SESSION_DURATION_SECS: u64 = 86400; if duration_seconds > MAX_SESSION_DURATION_SECS {
453 Some(deny(GuardReject {
454 code: "session-duration-too-long",
455 category: "auth",
456 message: "Session duration exceeds maximum",
457 }))
458 } else {
459 None
460 }
461}
462
463pub fn check_recovery_operation(
465 snapshot: &GuardSnapshot,
466 operation_type: &RecoveryOperationType,
467) -> Option<GuardOutcome> {
468 if snapshot.is_emergency {
470 return None;
471 }
472
473 match operation_type {
474 RecoveryOperationType::GuardianSetModification => {
475 if !snapshot.has_capability(&RecoveryAuthorizationCapability::Approve.as_name()) {
477 return Some(deny(GuardReject {
478 code: "guardian-set-approval-required",
479 category: "auth",
480 message: "Guardian set modification requires recovery:approve capability",
481 }));
482 }
483 }
484 RecoveryOperationType::EmergencyFreeze => {
485 if !snapshot.has_capability(&RecoveryAuthorizationCapability::Initiate.as_name()) {
487 return Some(deny(GuardReject {
488 code: "emergency-freeze-requires-capability",
489 category: "auth",
490 message: "Emergency freeze requires recovery:initiate capability",
491 }));
492 }
493 }
494 _ => {
495 }
497 }
498
499 None
500}
501
502pub fn evaluate_request(snapshot: &GuardSnapshot, request: &GuardRequest) -> GuardOutcome {
508 match request {
509 GuardRequest::ChallengeRequest { scope: _ } => {
510 if let Some(outcome) =
512 check_capability(snapshot, &AuthenticationCapability::Request.as_name())
513 {
514 return outcome;
515 }
516
517 if let Some(outcome) = check_flow_budget(snapshot, costs::CHALLENGE_REQUEST_COST) {
519 return outcome;
520 }
521
522 let session_id = format!("session_{}", snapshot.epoch);
524 let expires_at_ms = snapshot.now_ms + 300_000; GuardOutcome::allowed(vec![
527 EffectCommand::ChargeFlowBudget {
528 cost: costs::CHALLENGE_REQUEST_COST,
529 },
530 EffectCommand::GenerateChallenge {
531 session_id,
532 expires_at_ms,
533 },
534 ])
535 }
536
537 GuardRequest::ProofSubmission {
538 session_id,
539 proof_hash,
540 } => {
541 if let Some(outcome) =
543 check_capability(snapshot, &AuthenticationCapability::SubmitProof.as_name())
544 {
545 return outcome;
546 }
547
548 if let Some(outcome) = check_flow_budget(snapshot, costs::PROOF_SUBMISSION_COST) {
550 return outcome;
551 }
552
553 GuardOutcome::allowed(vec![
554 EffectCommand::ChargeFlowBudget {
555 cost: costs::PROOF_SUBMISSION_COST,
556 },
557 EffectCommand::JournalAppend {
558 fact_type: "auth_proof_submitted".to_string(),
559 fact_data: proof_hash.to_vec(),
560 },
561 EffectCommand::RecordReceipt {
562 operation: format!("proof_submission:{session_id}"),
563 peer: None,
564 timestamp_ms: snapshot.now_ms,
565 },
566 ])
567 }
568
569 GuardRequest::ProofVerification { session_id } => {
570 if let Some(outcome) =
572 check_capability(snapshot, &AuthenticationCapability::Verify.as_name())
573 {
574 return outcome;
575 }
576
577 if let Some(outcome) = check_flow_budget(snapshot, costs::PROOF_VERIFICATION_COST) {
579 return outcome;
580 }
581
582 GuardOutcome::allowed(vec![
583 EffectCommand::ChargeFlowBudget {
584 cost: costs::PROOF_VERIFICATION_COST,
585 },
586 EffectCommand::JournalAppend {
587 fact_type: "auth_verification_started".to_string(),
588 fact_data: session_id.as_bytes().to_vec(),
589 },
590 ])
591 }
592
593 GuardRequest::SessionCreation {
594 scope,
595 duration_seconds,
596 } => {
597 if let Some(outcome) =
599 check_capability(snapshot, &AuthenticationCapability::CreateSession.as_name())
600 {
601 return outcome;
602 }
603
604 if let Some(outcome) = check_flow_budget(snapshot, costs::SESSION_CREATION_COST) {
606 return outcome;
607 }
608
609 if let Some(outcome) = check_session_duration(*duration_seconds) {
611 return outcome;
612 }
613
614 let session_id = format!("session_{}", snapshot.epoch);
615 let expires_at_ms = snapshot.now_ms + (duration_seconds * 1000);
616
617 GuardOutcome::allowed(vec![
618 EffectCommand::ChargeFlowBudget {
619 cost: costs::SESSION_CREATION_COST,
620 },
621 EffectCommand::IssueSessionTicket {
622 session_id,
623 scope: scope.clone(),
624 expires_at_ms,
625 },
626 ])
627 }
628
629 GuardRequest::GuardianApprovalRequest {
630 account_id,
631 operation_type,
632 required_guardians,
633 } => {
634 if let Some(outcome) =
636 check_capability(snapshot, &GuardianAuthCapability::RequestApproval.as_name())
637 {
638 return outcome;
639 }
640
641 if let Some(outcome) =
643 check_flow_budget(snapshot, costs::GUARDIAN_APPROVAL_REQUEST_COST)
644 {
645 return outcome;
646 }
647
648 if let Some(outcome) = check_recovery_operation(snapshot, operation_type) {
650 return outcome;
651 }
652
653 let request_id = format!("guardian_req_{}", snapshot.epoch);
654
655 GuardOutcome::allowed(vec![
656 EffectCommand::ChargeFlowBudget {
657 cost: costs::GUARDIAN_APPROVAL_REQUEST_COST,
658 },
659 EffectCommand::JournalAppend {
660 fact_type: "guardian_approval_requested".to_string(),
661 fact_data: request_id.as_bytes().to_vec(),
662 },
663 EffectCommand::AggregateGuardianApprovals {
664 request_id: request_id.clone(),
665 threshold: *required_guardians,
666 },
667 EffectCommand::NotifyPeer {
668 peer: *account_id,
669 event_type: "guardian_approval_request".to_string(),
670 event_data: request_id.into_bytes(),
671 },
672 ])
673 }
674
675 GuardRequest::GuardianApprovalDecision {
676 request_id,
677 approved,
678 } => {
679 if let Some(outcome) =
681 check_capability(snapshot, &GuardianAuthCapability::Verify.as_name())
682 {
683 return outcome;
684 }
685
686 if let Some(outcome) =
688 check_flow_budget(snapshot, costs::GUARDIAN_APPROVAL_DECISION_COST)
689 {
690 return outcome;
691 }
692
693 let fact_type = if *approved {
694 "guardian_approved"
695 } else {
696 "guardian_denied"
697 };
698
699 GuardOutcome::allowed(vec![
700 EffectCommand::ChargeFlowBudget {
701 cost: costs::GUARDIAN_APPROVAL_DECISION_COST,
702 },
703 EffectCommand::JournalAppend {
704 fact_type: fact_type.to_string(),
705 fact_data: request_id.as_bytes().to_vec(),
706 },
707 EffectCommand::RecordReceipt {
708 operation: format!("guardian_decision:{request_id}:{approved}"),
709 peer: Some(snapshot.authority_id),
710 timestamp_ms: snapshot.now_ms,
711 },
712 ])
713 }
714 }
715}
716
717#[cfg(test)]
722mod tests {
723 use super::*;
724
725 fn test_authority() -> AuthorityId {
726 AuthorityId::new_from_entropy([1u8; 32])
727 }
728
729 fn test_snapshot() -> GuardSnapshot {
730 GuardSnapshot::new(
731 test_authority(),
732 None,
733 None,
734 FlowCost::new(100),
735 vec![
736 AuthenticationCapability::Request.as_name(),
737 AuthenticationCapability::SubmitProof.as_name(),
738 AuthenticationCapability::Verify.as_name(),
739 AuthenticationCapability::CreateSession.as_name(),
740 ],
741 1,
742 1000,
743 )
744 }
745
746 #[test]
747 fn test_guard_snapshot_has_capability() {
748 let snapshot = test_snapshot();
749 assert!(snapshot.has_capability(&AuthenticationCapability::Request.as_name()));
750 assert!(snapshot.has_capability(&AuthenticationCapability::SubmitProof.as_name()));
751 assert!(!snapshot.has_capability(&GuardianAuthCapability::RequestApproval.as_name()));
752 }
753
754 #[test]
755 fn test_guard_snapshot_has_budget() {
756 let snapshot = test_snapshot();
757 assert!(snapshot.has_budget(FlowCost::new(50)));
758 assert!(snapshot.has_budget(FlowCost::new(100)));
759 assert!(!snapshot.has_budget(FlowCost::new(101)));
760 }
761
762 #[test]
763 fn test_guard_decision_allow() {
764 let decision = GuardDecision::allow();
765 assert!(decision.is_allowed());
766 assert!(!decision.is_denied());
767 assert!(decision.denial_reason().is_none());
768 }
769
770 #[test]
771 fn test_guard_decision_deny() {
772 let decision = GuardDecision::deny(types::GuardViolation::other("test reason"));
773 assert!(!decision.is_allowed());
774 assert!(decision.is_denied());
775 assert!(matches!(
776 decision.denial_reason(),
777 Some(types::GuardViolation::Other(reason)) if reason == "test reason"
778 ));
779 }
780
781 #[test]
782 fn test_guard_outcome_allowed() {
783 let outcome = GuardOutcome::allowed(vec![EffectCommand::ChargeFlowBudget {
784 cost: FlowCost::new(10),
785 }]);
786 assert!(outcome.is_allowed());
787 assert_eq!(outcome.effects.len(), 1);
788 }
789
790 #[test]
791 fn test_guard_outcome_denied() {
792 let outcome = GuardOutcome::denied(types::GuardViolation::other("no budget"));
793 assert!(outcome.is_denied());
794 assert!(outcome.effects.is_empty());
795 }
796
797 #[test]
798 fn test_check_capability_success() {
799 let snapshot = test_snapshot();
800 let result = check_capability(&snapshot, &AuthenticationCapability::Request.as_name());
801 assert!(result.is_none());
802 }
803
804 #[test]
805 fn test_check_capability_failure() {
806 let snapshot = test_snapshot();
807 let result = check_capability(
808 &snapshot,
809 &GuardianAuthCapability::RequestApproval.as_name(),
810 );
811 assert!(result.is_some());
812 assert!(result.unwrap().is_denied());
813 }
814
815 #[test]
816 fn test_check_flow_budget_success() {
817 let snapshot = test_snapshot();
818 let result = check_flow_budget(&snapshot, FlowCost::new(50));
819 assert!(result.is_none());
820 }
821
822 #[test]
823 fn test_check_flow_budget_failure() {
824 let snapshot = test_snapshot();
825 let result = check_flow_budget(&snapshot, FlowCost::new(150));
826 assert!(result.is_some());
827 assert!(result.unwrap().is_denied());
828 }
829
830 #[test]
832 fn test_evaluate_challenge_request() {
833 let snapshot = test_snapshot();
834 let request = GuardRequest::ChallengeRequest {
835 scope: SessionScope::Protocol {
836 protocol_type: "test".to_string(),
837 },
838 };
839
840 let outcome = evaluate_request(&snapshot, &request);
841 assert!(outcome.is_allowed());
842 assert!(!outcome.effects.is_empty());
843 }
844
845 #[test]
848 fn test_evaluate_session_creation_duration_exceeded() {
849 let snapshot = test_snapshot();
850 let request = GuardRequest::SessionCreation {
851 scope: SessionScope::Protocol {
852 protocol_type: "test".to_string(),
853 },
854 duration_seconds: 100_000, };
856
857 let outcome = evaluate_request(&snapshot, &request);
858 assert!(outcome.is_denied());
859 }
860
861 #[test]
863 fn test_check_challenge_expiry() {
864 let snapshot = test_snapshot();
865
866 let result = check_challenge_expiry(&snapshot, 2000);
868 assert!(result.is_none());
869
870 let result = check_challenge_expiry(&snapshot, 500);
872 assert!(result.is_some());
873 assert!(result.unwrap().is_denied());
874 }
875
876 #[test]
877 fn test_guard_costs_defined() {
878 assert_eq!(costs::CHALLENGE_REQUEST_COST.value(), 1);
879 assert_eq!(costs::PROOF_SUBMISSION_COST.value(), 2);
880 assert_eq!(
881 AuthenticationCapability::Request.as_name().as_str(),
882 "auth:request"
883 );
884 }
885}