1use std::sync::atomic::{AtomicU64, Ordering};
8use std::time::Duration;
9
10use async_trait::async_trait;
11use chrono::{DateTime, Utc};
12use dashmap::DashMap;
13use serde::{Deserialize, Serialize};
14use sha2::{Digest, Sha256};
15use tracing::{info, warn};
16
17use crate::error::KernelError;
18use crate::health::HealthStatus;
19use crate::process::Pid;
20use crate::service::{ServiceType, SystemService};
21
22#[non_exhaustive]
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
29pub enum CredentialType {
30 ApiKey,
32 BearerToken,
34 Certificate,
36 Custom(String),
38}
39
40impl std::fmt::Display for CredentialType {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 match self {
43 Self::ApiKey => write!(f, "api_key"),
44 Self::BearerToken => write!(f, "bearer_token"),
45 Self::Certificate => write!(f, "certificate"),
46 Self::Custom(s) => write!(f, "custom({s})"),
47 }
48 }
49}
50
51#[derive(Debug, Clone)]
57pub struct StoredCredential {
58 pub name: String,
60 pub credential_type: CredentialType,
62 encrypted_value: Vec<u8>,
64 pub allowed_agents: Vec<String>,
66 pub created_at: DateTime<Utc>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct IssuedToken {
77 pub token_id: String,
79 pub credential_name: String,
81 pub issued_to: Pid,
83 pub issued_at: DateTime<Utc>,
85 pub expires_at: DateTime<Utc>,
87 pub scope: Vec<String>,
89}
90
91impl IssuedToken {
92 pub fn is_expired(&self) -> bool {
94 Utc::now() > self.expires_at
95 }
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct CredentialRequest {
105 pub credential_name: String,
107 pub requester_pid: Pid,
109 pub agent_id: String,
111 pub scope: Vec<String>,
113 pub ttl_secs: u64,
115}
116
117#[non_exhaustive]
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub enum CredentialGrant {
121 Granted(IssuedToken),
123 Denied { reason: String },
125}
126
127#[derive(Debug, Clone)]
139pub struct HashedCredential {
140 pub agent_id: String,
142 pub hash: Vec<u8>,
144 pub created_at: DateTime<Utc>,
146 pub scopes: Vec<String>,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct AuthToken {
153 pub token_id: String,
155 pub agent_id: String,
157 pub scopes: Vec<String>,
159 pub expires_at: DateTime<Utc>,
161 pub created_at: DateTime<Utc>,
163}
164
165impl AuthToken {
166 pub fn is_expired(&self) -> bool {
168 Utc::now() > self.expires_at
169 }
170}
171
172pub struct AuthService {
183 credentials: DashMap<String, StoredCredential>,
185 hashed_credentials: DashMap<String, HashedCredential>,
187 active_tokens: DashMap<String, IssuedToken>,
189 auth_tokens: DashMap<String, AuthToken>,
191 audit_log: std::sync::RwLock<Vec<AuditEntry>>,
193 encryption_key: [u8; 32],
195 token_counter: AtomicU64,
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct AuditEntry {
202 pub action: String,
204 pub agent_id: String,
206 pub credential_name: String,
208 pub timestamp: DateTime<Utc>,
210 pub allowed: bool,
212}
213
214impl AuthService {
215 pub fn new(encryption_key: [u8; 32]) -> Self {
217 Self {
218 credentials: DashMap::new(),
219 hashed_credentials: DashMap::new(),
220 active_tokens: DashMap::new(),
221 auth_tokens: DashMap::new(),
222 audit_log: std::sync::RwLock::new(Vec::new()),
223 encryption_key,
224 token_counter: AtomicU64::new(0),
225 }
226 }
227
228 pub fn new_default() -> Self {
230 Self::new([0u8; 32])
231 }
232
233 pub fn register_credential(
237 &self,
238 name: &str,
239 credential_type: CredentialType,
240 value: &[u8],
241 allowed_agents: Vec<String>,
242 ) -> Result<(), KernelError> {
243 if self.credentials.contains_key(name) {
244 return Err(KernelError::Service(format!(
245 "credential already registered: {name}"
246 )));
247 }
248
249 let encrypted = self.xor_encrypt(value);
250 self.credentials.insert(
251 name.to_string(),
252 StoredCredential {
253 name: name.to_string(),
254 credential_type,
255 encrypted_value: encrypted,
256 allowed_agents,
257 created_at: Utc::now(),
258 },
259 );
260
261 info!(name, "credential registered");
262 Ok(())
263 }
264
265 pub fn rotate_credential(
267 &self,
268 name: &str,
269 new_value: &[u8],
270 ) -> Result<(), KernelError> {
271 let mut cred = self
272 .credentials
273 .get_mut(name)
274 .ok_or_else(|| KernelError::Service(format!("credential not found: {name}")))?;
275 cred.encrypted_value = self.xor_encrypt(new_value);
276 info!(name, "credential rotated");
277 Ok(())
278 }
279
280 pub fn request_token(
284 &self,
285 request: &CredentialRequest,
286 ) -> Result<IssuedToken, KernelError> {
287 let cred = self
288 .credentials
289 .get(&request.credential_name)
290 .ok_or_else(|| {
291 KernelError::Service(format!(
292 "credential not found: {}",
293 request.credential_name
294 ))
295 })?;
296
297 if !cred.allowed_agents.is_empty()
299 && !cred.allowed_agents.contains(&request.agent_id)
300 {
301 self.audit("token.denied", &request.agent_id, &request.credential_name, false);
302 warn!(
303 agent_id = %request.agent_id,
304 credential = %request.credential_name,
305 "token request denied"
306 );
307 return Err(KernelError::CapabilityDenied {
308 pid: request.requester_pid,
309 action: "request_token".into(),
310 reason: format!(
311 "agent '{}' not authorized for credential '{}'",
312 request.agent_id, request.credential_name
313 ),
314 });
315 }
316
317 let ttl = Duration::from_secs(request.ttl_secs.max(1));
318 let token = IssuedToken {
319 token_id: uuid::Uuid::new_v4().to_string(),
320 credential_name: request.credential_name.clone(),
321 issued_to: request.requester_pid,
322 issued_at: Utc::now(),
323 expires_at: Utc::now() + chrono::Duration::from_std(ttl).unwrap_or(chrono::Duration::hours(1)),
324 scope: request.scope.clone(),
325 };
326
327 self.active_tokens
328 .insert(token.token_id.clone(), token.clone());
329 self.audit("token.issued", &request.agent_id, &request.credential_name, true);
330
331 info!(
332 token_id = %token.token_id,
333 credential = %request.credential_name,
334 agent = %request.agent_id,
335 ttl_secs = request.ttl_secs,
336 "token issued"
337 );
338 Ok(token)
339 }
340
341 pub fn validate_token(&self, token_id: &str) -> Result<IssuedToken, KernelError> {
343 let token = self
344 .active_tokens
345 .get(token_id)
346 .ok_or_else(|| KernelError::Service("token not found".into()))?;
347
348 if token.is_expired() {
349 return Err(KernelError::Service("token expired".into()));
350 }
351
352 Ok(token.clone())
353 }
354
355 pub fn revoke_token(&self, token_id: &str) -> bool {
357 self.active_tokens.remove(token_id).is_some()
358 }
359
360 pub fn active_token_count(&self) -> usize {
362 self.active_tokens
363 .iter()
364 .filter(|t| !t.value().is_expired())
365 .count()
366 }
367
368 fn sha256_hash(data: &[u8]) -> Vec<u8> {
372 let mut hasher = Sha256::new();
373 hasher.update(data);
374 hasher.finalize().to_vec()
375 }
376
377 pub fn register_hashed_credential(
381 &self,
382 agent_id: &str,
383 raw_credential: &[u8],
384 scopes: Vec<String>,
385 ) -> Result<(), KernelError> {
386 if self.hashed_credentials.contains_key(agent_id) {
387 return Err(KernelError::Service(format!(
388 "hashed credential already registered for agent: {agent_id}"
389 )));
390 }
391
392 let hash = Self::sha256_hash(raw_credential);
393 self.hashed_credentials.insert(
394 agent_id.to_string(),
395 HashedCredential {
396 agent_id: agent_id.to_string(),
397 hash,
398 created_at: Utc::now(),
399 scopes,
400 },
401 );
402
403 info!(agent_id, "hashed credential registered");
404 Ok(())
405 }
406
407 pub fn authenticate(
411 &self,
412 agent_id: &str,
413 raw_credential: &[u8],
414 ) -> Result<AuthToken, KernelError> {
415 let cred = self
416 .hashed_credentials
417 .get(agent_id)
418 .ok_or_else(|| KernelError::Service(format!("no credential for agent: {agent_id}")))?;
419
420 let provided_hash = Self::sha256_hash(raw_credential);
421 if cred.hash != provided_hash {
422 self.audit("authenticate.failed", agent_id, agent_id, false);
423 warn!(agent_id, "authentication failed — hash mismatch");
424 return Err(KernelError::Service("authentication failed".into()));
425 }
426
427 let seq = self.token_counter.fetch_add(1, Ordering::Relaxed);
428 let token = AuthToken {
429 token_id: format!("auth-{}-{seq}", uuid::Uuid::new_v4()),
430 agent_id: agent_id.to_string(),
431 scopes: cred.scopes.clone(),
432 expires_at: Utc::now() + chrono::Duration::hours(1),
433 created_at: Utc::now(),
434 };
435
436 self.auth_tokens
437 .insert(token.token_id.clone(), token.clone());
438 self.audit("authenticate.success", agent_id, agent_id, true);
439
440 info!(agent_id, token_id = %token.token_id, "agent authenticated");
441 Ok(token)
442 }
443
444 pub fn validate_auth_token(&self, token_id: &str) -> Result<AuthToken, KernelError> {
446 let token = self
447 .auth_tokens
448 .get(token_id)
449 .ok_or_else(|| KernelError::Service("auth token not found".into()))?;
450
451 if token.is_expired() {
452 return Err(KernelError::Service("auth token expired".into()));
453 }
454
455 Ok(token.clone())
456 }
457
458 pub fn revoke_auth_token(&self, token_id: &str) -> bool {
460 self.auth_tokens.remove(token_id).is_some()
461 }
462
463 pub fn check_scope(&self, token_id: &str, required_scope: &str) -> bool {
465 self.auth_tokens
466 .get(token_id)
467 .map(|t| !t.is_expired() && t.scopes.contains(&required_scope.to_string()))
468 .unwrap_or(false)
469 }
470
471 fn audit(&self, action: &str, agent_id: &str, credential_name: &str, allowed: bool) {
474 if let Ok(mut log) = self.audit_log.write() {
475 log.push(AuditEntry {
476 action: action.to_string(),
477 agent_id: agent_id.to_string(),
478 credential_name: credential_name.to_string(),
479 timestamp: Utc::now(),
480 allowed,
481 });
482 }
483 }
484
485 pub fn audit_log(&self) -> Vec<AuditEntry> {
487 self.audit_log.read().map(|l| l.clone()).unwrap_or_default()
488 }
489
490 fn xor_encrypt(&self, data: &[u8]) -> Vec<u8> {
493 data.iter()
494 .enumerate()
495 .map(|(i, b)| b ^ self.encryption_key[i % 32])
496 .collect()
497 }
498}
499
500#[async_trait]
501impl SystemService for AuthService {
502 fn name(&self) -> &str {
503 "auth-service"
504 }
505
506 fn service_type(&self) -> ServiceType {
507 ServiceType::Core
508 }
509
510 async fn start(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
511 info!("auth service started");
512 Ok(())
513 }
514
515 async fn stop(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
516 info!(
517 credentials = self.credentials.len(),
518 active_tokens = self.active_token_count(),
519 "auth service stopped"
520 );
521 Ok(())
522 }
523
524 async fn health_check(&self) -> HealthStatus {
525 HealthStatus::Healthy
526 }
527}
528
529#[cfg(test)]
532mod tests {
533 use super::*;
534
535 fn pid(n: u64) -> Pid {
536 n
537 }
538
539 fn make_request(cred: &str, agent: &str, pid_val: u64) -> CredentialRequest {
540 CredentialRequest {
541 credential_name: cred.to_string(),
542 requester_pid: pid(pid_val),
543 agent_id: agent.to_string(),
544 scope: vec!["read".to_string()],
545 ttl_secs: 3600,
546 }
547 }
548
549 #[test]
550 fn register_and_request_token() {
551 let svc = AuthService::new_default();
552 svc.register_credential(
553 "github-key",
554 CredentialType::ApiKey,
555 b"ghp_secret_value",
556 vec!["deploy-agent".to_string()],
557 )
558 .unwrap();
559
560 let req = make_request("github-key", "deploy-agent", 1);
561 let token = svc.request_token(&req).unwrap();
562 assert!(!token.token_id.is_empty());
563 assert_eq!(token.credential_name, "github-key");
564 }
565
566 #[test]
567 fn unauthorized_agent_denied() {
568 let svc = AuthService::new_default();
569 svc.register_credential(
570 "secret",
571 CredentialType::BearerToken,
572 b"token",
573 vec!["allowed-agent".to_string()],
574 )
575 .unwrap();
576
577 let req = make_request("secret", "other-agent", 2);
578 let result = svc.request_token(&req);
579 assert!(result.is_err());
580 let err = result.unwrap_err().to_string();
581 assert!(err.contains("denied") || err.contains("authorized"), "got: {err}");
582 }
583
584 #[test]
585 fn empty_allowed_agents_permits_all() {
586 let svc = AuthService::new_default();
587 svc.register_credential(
588 "open-cred",
589 CredentialType::ApiKey,
590 b"value",
591 vec![], )
593 .unwrap();
594
595 let req = make_request("open-cred", "random-agent", 5);
596 let token = svc.request_token(&req).unwrap();
597 assert!(!token.is_expired());
598 }
599
600 #[test]
601 fn validate_token() {
602 let svc = AuthService::new_default();
603 svc.register_credential("cred", CredentialType::ApiKey, b"val", vec![])
604 .unwrap();
605 let req = make_request("cred", "agent", 1);
606 let token = svc.request_token(&req).unwrap();
607 let validated = svc.validate_token(&token.token_id).unwrap();
608 assert_eq!(validated.token_id, token.token_id);
609 }
610
611 #[test]
612 fn validate_nonexistent_token_fails() {
613 let svc = AuthService::new_default();
614 assert!(svc.validate_token("no-such-token").is_err());
615 }
616
617 #[test]
618 fn revoke_token() {
619 let svc = AuthService::new_default();
620 svc.register_credential("cred", CredentialType::ApiKey, b"val", vec![])
621 .unwrap();
622 let req = make_request("cred", "agent", 1);
623 let token = svc.request_token(&req).unwrap();
624 assert!(svc.revoke_token(&token.token_id));
625 assert!(svc.validate_token(&token.token_id).is_err());
626 }
627
628 #[test]
629 fn credential_rotation_preserves_tokens() {
630 let svc = AuthService::new_default();
631 svc.register_credential("rotate-cred", CredentialType::ApiKey, b"old_val", vec![])
632 .unwrap();
633 let req = make_request("rotate-cred", "agent", 1);
634 let token = svc.request_token(&req).unwrap();
635
636 svc.rotate_credential("rotate-cred", b"new_val").unwrap();
638
639 let validated = svc.validate_token(&token.token_id).unwrap();
641 assert_eq!(validated.credential_name, "rotate-cred");
642 }
643
644 #[test]
645 fn raw_credential_never_exposed() {
646 let key = [0xAB; 32];
647 let svc = AuthService::new(key);
648 svc.register_credential("secret", CredentialType::ApiKey, b"raw_secret", vec![])
649 .unwrap();
650
651 let cred = svc.credentials.get("secret").unwrap();
653 assert_ne!(cred.encrypted_value, b"raw_secret");
654 }
655
656 #[test]
657 fn audit_log_records_events() {
658 let svc = AuthService::new_default();
659 svc.register_credential(
660 "audited",
661 CredentialType::ApiKey,
662 b"val",
663 vec!["agent-a".to_string()],
664 )
665 .unwrap();
666
667 let req = make_request("audited", "agent-a", 1);
669 svc.request_token(&req).unwrap();
670
671 let bad_req = make_request("audited", "agent-b", 2);
673 let _ = svc.request_token(&bad_req);
674
675 let log = svc.audit_log();
676 assert_eq!(log.len(), 2);
677 assert!(log[0].allowed);
678 assert!(!log[1].allowed);
679 }
680
681 #[test]
682 fn duplicate_credential_registration_fails() {
683 let svc = AuthService::new_default();
684 svc.register_credential("dup", CredentialType::ApiKey, b"val", vec![])
685 .unwrap();
686 let result = svc.register_credential("dup", CredentialType::ApiKey, b"val2", vec![]);
687 assert!(result.is_err());
688 }
689
690 #[test]
691 fn request_nonexistent_credential_fails() {
692 let svc = AuthService::new_default();
693 let req = make_request("missing", "agent", 1);
694 assert!(svc.request_token(&req).is_err());
695 }
696
697 #[tokio::test]
698 async fn system_service_impl() {
699 let svc = AuthService::new_default();
700 assert_eq!(svc.name(), "auth-service");
701 assert_eq!(svc.service_type(), ServiceType::Core);
702 svc.start().await.unwrap();
703 assert_eq!(svc.health_check().await, HealthStatus::Healthy);
704 svc.stop().await.unwrap();
705 }
706
707 #[test]
710 fn register_and_authenticate_success() {
711 let svc = AuthService::new_default();
712 svc.register_hashed_credential("agent-1", b"my_password", vec!["read".into()])
713 .unwrap();
714 let token = svc.authenticate("agent-1", b"my_password").unwrap();
715 assert_eq!(token.agent_id, "agent-1");
716 assert!(!token.token_id.is_empty());
717 assert_eq!(token.scopes, vec!["read".to_string()]);
718 }
719
720 #[test]
721 fn authenticate_wrong_credential_fails() {
722 let svc = AuthService::new_default();
723 svc.register_hashed_credential("agent-2", b"correct", vec![]).unwrap();
724 let result = svc.authenticate("agent-2", b"wrong");
725 assert!(result.is_err());
726 let err = result.unwrap_err().to_string();
727 assert!(err.contains("failed"), "got: {err}");
728 }
729
730 #[test]
731 fn authenticate_issues_token_with_scopes() {
732 let svc = AuthService::new_default();
733 svc.register_hashed_credential(
734 "scoped-agent",
735 b"secret",
736 vec!["read".into(), "write".into()],
737 )
738 .unwrap();
739 let token = svc.authenticate("scoped-agent", b"secret").unwrap();
740 assert_eq!(token.scopes, vec!["read".to_string(), "write".to_string()]);
741 }
742
743 #[test]
744 fn validate_auth_token_succeeds() {
745 let svc = AuthService::new_default();
746 svc.register_hashed_credential("v-agent", b"pass", vec![]).unwrap();
747 let token = svc.authenticate("v-agent", b"pass").unwrap();
748 let validated = svc.validate_auth_token(&token.token_id).unwrap();
749 assert_eq!(validated.agent_id, "v-agent");
750 }
751
752 #[test]
753 fn validate_expired_auth_token_fails() {
754 let svc = AuthService::new_default();
755 svc.register_hashed_credential("exp-agent", b"pass", vec![]).unwrap();
756 let token = svc.authenticate("exp-agent", b"pass").unwrap();
757
758 let expired = AuthToken {
760 token_id: "expired-tok".to_string(),
761 agent_id: "exp-agent".to_string(),
762 scopes: vec![],
763 expires_at: Utc::now() - chrono::Duration::hours(1),
764 created_at: Utc::now() - chrono::Duration::hours(2),
765 };
766 svc.auth_tokens.insert("expired-tok".into(), expired);
767
768 assert!(svc.validate_auth_token(&token.token_id).is_ok());
770 let result = svc.validate_auth_token("expired-tok");
772 assert!(result.is_err());
773 assert!(result.unwrap_err().to_string().contains("expired"));
774 }
775
776 #[test]
777 fn revoke_auth_token_works() {
778 let svc = AuthService::new_default();
779 svc.register_hashed_credential("rev-agent", b"pass", vec![]).unwrap();
780 let token = svc.authenticate("rev-agent", b"pass").unwrap();
781 assert!(svc.revoke_auth_token(&token.token_id));
782 assert!(svc.validate_auth_token(&token.token_id).is_err());
783 assert!(!svc.revoke_auth_token(&token.token_id));
785 }
786
787 #[test]
788 fn check_scope_with_matching_scope() {
789 let svc = AuthService::new_default();
790 svc.register_hashed_credential("sc-agent", b"cred", vec!["admin".into(), "read".into()])
791 .unwrap();
792 let token = svc.authenticate("sc-agent", b"cred").unwrap();
793 assert!(svc.check_scope(&token.token_id, "admin"));
794 assert!(svc.check_scope(&token.token_id, "read"));
795 }
796
797 #[test]
798 fn check_scope_with_missing_scope_fails() {
799 let svc = AuthService::new_default();
800 svc.register_hashed_credential("sc-agent2", b"cred", vec!["read".into()])
801 .unwrap();
802 let token = svc.authenticate("sc-agent2", b"cred").unwrap();
803 assert!(!svc.check_scope(&token.token_id, "write"));
804 assert!(!svc.check_scope(&token.token_id, "admin"));
805 }
806
807 #[test]
808 fn raw_credentials_never_stored_in_hash() {
809 let svc = AuthService::new_default();
810 let raw = b"super_secret_password";
811 svc.register_hashed_credential("hash-check", raw, vec![]).unwrap();
812
813 let cred = svc.hashed_credentials.get("hash-check").unwrap();
814 assert_ne!(cred.hash.as_slice(), raw.as_slice());
816 assert_eq!(cred.hash.len(), 32);
818 }
819
820 #[test]
821 fn check_scope_on_nonexistent_token_returns_false() {
822 let svc = AuthService::new_default();
823 assert!(!svc.check_scope("no-such-token", "read"));
824 }
825}