1use std::fs;
2use std::path::{Path, PathBuf};
3use std::sync::Mutex;
4
5use chio_core::capability::{CapabilityToken, CapabilityTokenBody, ChioScope};
6use chio_core::crypto::{Keypair, PublicKey};
7use chio_kernel::{
8 AuthoritySnapshot, AuthorityStatus, AuthorityStoreError, AuthorityTrustedKeySnapshot,
9 CapabilityAuthority, KernelError,
10};
11use rusqlite::{params, Connection};
12use uuid::Uuid;
13
14pub struct SqliteCapabilityAuthority {
15 path: PathBuf,
16 cached_public_key: Mutex<PublicKey>,
17 cached_trusted_public_keys: Mutex<Vec<PublicKey>>,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct AuthorityClusterFence {
22 pub leader_url: Option<String>,
23 pub election_term: u64,
24 pub updated_at: u64,
25 pub authority_generation: u64,
26 pub authority_rotated_at: u64,
27}
28
29impl SqliteCapabilityAuthority {
30 pub fn open(path: impl AsRef<Path>) -> Result<Self, AuthorityStoreError> {
31 let path = path.as_ref().to_path_buf();
32 if let Some(parent) = path.parent() {
33 fs::create_dir_all(parent)?;
34 }
35
36 let bootstrap = Keypair::generate();
37 let connection = Self::open_connection(&path)?;
38 connection.execute(
39 r#"
40 INSERT INTO authority_state (singleton_id, seed_hex, public_key_hex, generation, rotated_at)
41 VALUES (1, ?1, ?2, 1, ?3)
42 ON CONFLICT(singleton_id) DO NOTHING
43 "#,
44 params![
45 bootstrap.seed_hex(),
46 bootstrap.public_key().to_hex(),
47 unix_now() as i64
48 ],
49 )?;
50 let current_public_key = connection
51 .query_row(
52 r#"
53 SELECT seed_hex
54 FROM authority_state
55 WHERE singleton_id = 1
56 "#,
57 [],
58 |row| row.get::<_, String>(0),
59 )
60 .map(|seed_hex| Keypair::from_seed_hex(seed_hex.trim()))
61 .map_err(AuthorityStoreError::from)??;
62 connection.execute(
63 r#"
64 UPDATE authority_state
65 SET public_key_hex = COALESCE(NULLIF(public_key_hex, ''), ?1)
66 WHERE singleton_id = 1
67 "#,
68 params![current_public_key.public_key().to_hex()],
69 )?;
70 connection.execute(
71 r#"
72 INSERT INTO authority_trusted_keys (public_key_hex, generation, activated_at)
73 VALUES (?1, 1, ?2)
74 ON CONFLICT(public_key_hex) DO NOTHING
75 "#,
76 params![current_public_key.public_key().to_hex(), unix_now() as i64],
77 )?;
78 let status = Self::read_status_from_connection(&connection)?;
79 Ok(Self {
80 path,
81 cached_public_key: Mutex::new(status.public_key),
82 cached_trusted_public_keys: Mutex::new(status.trusted_public_keys),
83 })
84 }
85
86 pub fn status(&self) -> Result<AuthorityStatus, AuthorityStoreError> {
87 let connection = Self::open_connection(&self.path)?;
88 let status = Self::read_status_from_connection(&connection)?;
89 self.update_cached_public_key(status.public_key.clone());
90 self.update_cached_trusted_public_keys(status.trusted_public_keys.clone());
91 Ok(status)
92 }
93
94 pub fn rotate(&self) -> Result<AuthorityStatus, AuthorityStoreError> {
95 let connection = Self::open_connection(&self.path)?;
96 let status_before = Self::read_status_from_connection(&connection)?;
97 let keypair = Keypair::generate();
98 let rotated_at = unix_now();
99 let next_generation = status_before.generation.saturating_add(1);
100
101 connection.execute(
102 r#"
103 UPDATE authority_state
104 SET seed_hex = ?1, public_key_hex = ?2, generation = ?3, rotated_at = ?4
105 WHERE singleton_id = 1
106 "#,
107 params![
108 keypair.seed_hex(),
109 keypair.public_key().to_hex(),
110 next_generation as i64,
111 rotated_at as i64,
112 ],
113 )?;
114 connection.execute(
115 r#"
116 INSERT INTO authority_trusted_keys (public_key_hex, generation, activated_at)
117 VALUES (?1, ?2, ?3)
118 ON CONFLICT(public_key_hex) DO NOTHING
119 "#,
120 params![
121 keypair.public_key().to_hex(),
122 next_generation as i64,
123 rotated_at as i64
124 ],
125 )?;
126
127 let status = Self::read_status_from_connection(&connection)?;
128 self.update_cached_public_key(status.public_key.clone());
129 self.update_cached_trusted_public_keys(status.trusted_public_keys.clone());
130 Ok(status)
131 }
132
133 pub fn snapshot(&self) -> Result<AuthoritySnapshot, AuthorityStoreError> {
134 let connection = Self::open_connection(&self.path)?;
135 let status = Self::read_status_from_connection(&connection)?;
136 Ok(AuthoritySnapshot {
137 public_key_hex: status.public_key.to_hex(),
138 generation: status.generation,
139 rotated_at: status.rotated_at,
140 trusted_keys: Self::read_trusted_key_snapshots(&connection)?,
141 })
142 }
143
144 pub fn apply_snapshot(
145 &self,
146 snapshot: &AuthoritySnapshot,
147 ) -> Result<bool, AuthorityStoreError> {
148 let connection = Self::open_connection(&self.path)?;
149 let local_snapshot = self.snapshot()?;
150 let remote_public_key = PublicKey::from_hex(snapshot.public_key_hex.trim())?;
151
152 connection.execute(
154 r#"
155 INSERT INTO authority_trusted_keys (public_key_hex, generation, activated_at)
156 VALUES (?1, ?2, ?3)
157 ON CONFLICT(public_key_hex) DO UPDATE SET
158 generation = MAX(generation, excluded.generation),
159 activated_at = MIN(activated_at, excluded.activated_at)
160 "#,
161 params![
162 remote_public_key.to_hex(),
163 snapshot.generation as i64,
164 snapshot.rotated_at as i64
165 ],
166 )?;
167 for trusted_key in &snapshot.trusted_keys {
168 connection.execute(
169 r#"
170 INSERT INTO authority_trusted_keys (public_key_hex, generation, activated_at)
171 VALUES (?1, ?2, ?3)
172 ON CONFLICT(public_key_hex) DO UPDATE SET
173 generation = MAX(generation, excluded.generation),
174 activated_at = MIN(activated_at, excluded.activated_at)
175 "#,
176 params![
177 trusted_key.public_key_hex,
178 trusted_key.generation as i64,
179 trusted_key.activated_at as i64
180 ],
181 )?;
182 }
183
184 let should_replace = snapshot.generation > local_snapshot.generation
185 || (snapshot.generation == local_snapshot.generation
186 && (snapshot.rotated_at, snapshot.public_key_hex.as_str())
187 > (
188 local_snapshot.rotated_at,
189 local_snapshot.public_key_hex.as_str(),
190 ));
191 if should_replace {
192 connection.execute(
193 r#"
194 UPDATE authority_state
195 SET public_key_hex = ?1, generation = ?2, rotated_at = ?3
196 WHERE singleton_id = 1
197 "#,
198 params![
199 remote_public_key.to_hex(),
200 snapshot.generation as i64,
201 snapshot.rotated_at as i64,
202 ],
203 )?;
204 }
205
206 let status = Self::read_status_from_connection(&connection)?;
207 self.update_cached_public_key(status.public_key);
208 self.update_cached_trusted_public_keys(status.trusted_public_keys);
209 Ok(should_replace)
210 }
211
212 pub fn current_keypair(&self) -> Result<Keypair, AuthorityStoreError> {
213 self.read_current_keypair()
214 }
215
216 pub fn local_keypair(&self) -> Result<Keypair, AuthorityStoreError> {
217 let connection = Self::open_connection(&self.path)?;
218 Self::read_keypair_from_connection(&connection)
219 }
220
221 pub fn cluster_fence(&self) -> Result<AuthorityClusterFence, AuthorityStoreError> {
222 let connection = Self::open_connection(&self.path)?;
223 Self::read_cluster_fence_from_connection(&connection)
224 }
225
226 pub fn seed_cluster_fence(
227 &self,
228 leader_url: Option<&str>,
229 election_term: u64,
230 ) -> Result<bool, AuthorityStoreError> {
231 let connection = Self::open_connection(&self.path)?;
232 let current = Self::read_cluster_fence_from_connection(&connection)?;
233 let (_, authority_generation, authority_rotated_at) =
234 Self::read_public_state_from_connection(&connection)?;
235 let next_leader = leader_url.map(ToOwned::to_owned);
236 let same_term_same_leader = election_term == current.election_term
237 && current.leader_url.as_deref() == next_leader.as_deref();
238 let fence_authority_state_is_stale = (current.election_term > 0
239 || current.leader_url.is_some())
240 && (current.authority_generation != authority_generation
241 || current.authority_rotated_at != authority_rotated_at);
242 let should_update = election_term > current.election_term
243 || (election_term == current.election_term
244 && current.leader_url.is_none()
245 && next_leader.is_some())
246 || (same_term_same_leader && fence_authority_state_is_stale);
247 if should_update {
248 Self::write_cluster_fence_to_connection(&connection, next_leader, election_term)?;
249 }
250 Ok(should_update)
251 }
252
253 pub fn enforce_cluster_fence(
254 &self,
255 leader_url: &str,
256 election_term: u64,
257 ) -> Result<(), AuthorityStoreError> {
258 let connection = Self::open_connection(&self.path)?;
259 let current = Self::read_cluster_fence_from_connection(&connection)?;
260 let (_, authority_generation, authority_rotated_at) =
261 Self::read_public_state_from_connection(&connection)?;
262 if (current.election_term > 0 || current.leader_url.is_some())
263 && (current.authority_generation != authority_generation
264 || current.authority_rotated_at != authority_rotated_at)
265 {
266 return Err(AuthorityStoreError::Fence(format!(
267 "persisted authority fence generation `{}` rotated_at `{}` does not match current authority generation `{authority_generation}` rotated_at `{authority_rotated_at}`",
268 current.authority_generation, current.authority_rotated_at
269 )));
270 }
271 if election_term < current.election_term {
272 return Err(AuthorityStoreError::Fence(format!(
273 "stale authority term `{election_term}` is below persisted term `{}`",
274 current.election_term
275 )));
276 }
277 if election_term == current.election_term
278 && current
279 .leader_url
280 .as_deref()
281 .is_some_and(|current_leader| current_leader != leader_url)
282 {
283 return Err(AuthorityStoreError::Fence(format!(
284 "authority term `{election_term}` is already fenced to leader `{}`",
285 current.leader_url.unwrap_or_default()
286 )));
287 }
288 Self::write_cluster_fence_to_connection(
289 &connection,
290 Some(leader_url.to_string()),
291 election_term,
292 )
293 }
294
295 fn open_connection(path: &Path) -> Result<Connection, AuthorityStoreError> {
296 let connection = Connection::open(path)?;
297 connection.execute_batch(
298 r#"
299 PRAGMA journal_mode = WAL;
300 PRAGMA synchronous = FULL;
301 PRAGMA busy_timeout = 5000;
302
303 CREATE TABLE IF NOT EXISTS authority_state (
304 singleton_id INTEGER PRIMARY KEY CHECK (singleton_id = 1),
305 seed_hex TEXT NOT NULL,
306 public_key_hex TEXT,
307 generation INTEGER NOT NULL,
308 rotated_at INTEGER NOT NULL
309 );
310
311 CREATE TABLE IF NOT EXISTS authority_trusted_keys (
312 public_key_hex TEXT PRIMARY KEY,
313 generation INTEGER NOT NULL,
314 activated_at INTEGER NOT NULL
315 );
316
317 CREATE TABLE IF NOT EXISTS authority_cluster_fence (
318 singleton_id INTEGER PRIMARY KEY CHECK (singleton_id = 1),
319 leader_url TEXT,
320 election_term INTEGER NOT NULL,
321 updated_at INTEGER NOT NULL,
322 authority_generation INTEGER NOT NULL DEFAULT 0,
323 authority_rotated_at INTEGER NOT NULL DEFAULT 0
324 );
325
326 INSERT INTO authority_cluster_fence (singleton_id, leader_url, election_term, updated_at)
327 VALUES (1, NULL, 0, 0)
328 ON CONFLICT(singleton_id) DO NOTHING;
329 "#,
330 )?;
331 if !Self::table_has_column(&connection, "authority_state", "public_key_hex")? {
332 connection.execute(
333 "ALTER TABLE authority_state ADD COLUMN public_key_hex TEXT",
334 [],
335 )?;
336 }
337 if !Self::table_has_column(
338 &connection,
339 "authority_cluster_fence",
340 "authority_generation",
341 )? {
342 connection.execute(
343 "ALTER TABLE authority_cluster_fence ADD COLUMN authority_generation INTEGER NOT NULL DEFAULT 0",
344 [],
345 )?;
346 }
347 if !Self::table_has_column(
348 &connection,
349 "authority_cluster_fence",
350 "authority_rotated_at",
351 )? {
352 connection.execute(
353 "ALTER TABLE authority_cluster_fence ADD COLUMN authority_rotated_at INTEGER NOT NULL DEFAULT 0",
354 [],
355 )?;
356 }
357 Ok(connection)
358 }
359
360 fn read_status_from_connection(
361 connection: &Connection,
362 ) -> Result<AuthorityStatus, AuthorityStoreError> {
363 let (public_key, generation, rotated_at) =
364 Self::read_public_state_from_connection(connection)?;
365 Ok(AuthorityStatus {
366 public_key,
367 generation,
368 rotated_at,
369 trusted_public_keys: Self::read_trusted_public_keys(connection)?,
370 })
371 }
372
373 fn read_keypair_from_connection(
374 connection: &Connection,
375 ) -> Result<Keypair, AuthorityStoreError> {
376 let seed_hex = connection.query_row(
377 r#"
378 SELECT seed_hex
379 FROM authority_state
380 WHERE singleton_id = 1
381 "#,
382 [],
383 |row| row.get::<_, String>(0),
384 )?;
385 Keypair::from_seed_hex(seed_hex.trim()).map_err(Into::into)
386 }
387
388 fn read_public_state_from_connection(
389 connection: &Connection,
390 ) -> Result<(PublicKey, u64, u64), AuthorityStoreError> {
391 let (seed_hex, public_key_hex, generation, rotated_at) = connection.query_row(
392 r#"
393 SELECT seed_hex, public_key_hex, generation, rotated_at
394 FROM authority_state
395 WHERE singleton_id = 1
396 "#,
397 [],
398 |row| {
399 Ok((
400 row.get::<_, String>(0)?,
401 row.get::<_, Option<String>>(1)?,
402 row.get::<_, i64>(2)?,
403 row.get::<_, i64>(3)?,
404 ))
405 },
406 )?;
407 let public_key = match public_key_hex
408 .as_deref()
409 .map(str::trim)
410 .filter(|value| !value.is_empty())
411 {
412 Some(public_key_hex) => PublicKey::from_hex(public_key_hex)?,
413 None => Keypair::from_seed_hex(seed_hex.trim())?.public_key(),
414 };
415 Ok((
416 public_key,
417 generation.max(0) as u64,
418 rotated_at.max(0) as u64,
419 ))
420 }
421
422 fn read_current_keypair(&self) -> Result<Keypair, AuthorityStoreError> {
423 let connection = Self::open_connection(&self.path)?;
424 let keypair = Self::read_keypair_from_connection(&connection)?;
425 let status = Self::read_status_from_connection(&connection)?;
426 self.update_cached_public_key(status.public_key.clone());
427 self.update_cached_trusted_public_keys(status.trusted_public_keys.clone());
428 if keypair.public_key() != status.public_key {
429 return Err(AuthorityStoreError::Fence(format!(
430 "local signing seed public key {} does not match replicated authority public key {}",
431 keypair.public_key().to_hex(),
432 status.public_key.to_hex(),
433 )));
434 }
435 Ok(keypair)
436 }
437
438 fn table_has_column(
439 connection: &Connection,
440 table: &str,
441 column: &str,
442 ) -> Result<bool, AuthorityStoreError> {
443 let pragma = format!("PRAGMA table_info({table})");
444 let mut statement = connection.prepare(&pragma)?;
445 let mut rows = statement.query([])?;
446 while let Some(row) = rows.next()? {
447 if row.get::<_, String>(1)? == column {
448 return Ok(true);
449 }
450 }
451 Ok(false)
452 }
453
454 fn update_cached_public_key(&self, public_key: PublicKey) {
455 match self.cached_public_key.lock() {
456 Ok(mut guard) => *guard = public_key,
457 Err(poisoned) => *poisoned.into_inner() = public_key,
458 }
459 }
460
461 fn update_cached_trusted_public_keys(&self, public_keys: Vec<PublicKey>) {
462 match self.cached_trusted_public_keys.lock() {
463 Ok(mut guard) => *guard = public_keys,
464 Err(poisoned) => *poisoned.into_inner() = public_keys,
465 }
466 }
467
468 fn cached_public_key(&self) -> PublicKey {
469 match self.cached_public_key.lock() {
470 Ok(guard) => guard.clone(),
471 Err(poisoned) => poisoned.into_inner().clone(),
472 }
473 }
474
475 fn cached_trusted_public_keys(&self) -> Vec<PublicKey> {
476 match self.cached_trusted_public_keys.lock() {
477 Ok(guard) => guard.clone(),
478 Err(poisoned) => poisoned.into_inner().clone(),
479 }
480 }
481
482 fn read_trusted_public_keys(
483 connection: &Connection,
484 ) -> Result<Vec<PublicKey>, AuthorityStoreError> {
485 let mut statement = connection.prepare(
486 r#"
487 SELECT public_key_hex
488 FROM authority_trusted_keys
489 ORDER BY generation ASC, activated_at ASC
490 "#,
491 )?;
492 let rows = statement.query_map([], |row| row.get::<_, String>(0))?;
493 rows.map(|row| {
494 let public_key_hex = row?;
495 PublicKey::from_hex(public_key_hex.trim()).map_err(AuthorityStoreError::from)
496 })
497 .collect()
498 }
499
500 fn read_trusted_key_snapshots(
501 connection: &Connection,
502 ) -> Result<Vec<AuthorityTrustedKeySnapshot>, AuthorityStoreError> {
503 let mut statement = connection.prepare(
504 r#"
505 SELECT public_key_hex, generation, activated_at
506 FROM authority_trusted_keys
507 ORDER BY generation ASC, activated_at ASC, public_key_hex ASC
508 "#,
509 )?;
510 let rows = statement.query_map([], |row| {
511 Ok(AuthorityTrustedKeySnapshot {
512 public_key_hex: row.get(0)?,
513 generation: row.get::<_, i64>(1)?.max(0) as u64,
514 activated_at: row.get::<_, i64>(2)?.max(0) as u64,
515 })
516 })?;
517 rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
518 }
519
520 fn read_cluster_fence_from_connection(
521 connection: &Connection,
522 ) -> Result<AuthorityClusterFence, AuthorityStoreError> {
523 let (leader_url, election_term, updated_at, authority_generation, authority_rotated_at) =
524 connection.query_row(
525 r#"
526 SELECT leader_url, election_term, updated_at, authority_generation, authority_rotated_at
527 FROM authority_cluster_fence
528 WHERE singleton_id = 1
529 "#,
530 [],
531 |row| {
532 Ok((
533 row.get::<_, Option<String>>(0)?,
534 row.get::<_, i64>(1)?,
535 row.get::<_, i64>(2)?,
536 row.get::<_, i64>(3)?,
537 row.get::<_, i64>(4)?,
538 ))
539 },
540 )?;
541 Ok(AuthorityClusterFence {
542 leader_url,
543 election_term: election_term.max(0) as u64,
544 updated_at: updated_at.max(0) as u64,
545 authority_generation: authority_generation.max(0) as u64,
546 authority_rotated_at: authority_rotated_at.max(0) as u64,
547 })
548 }
549
550 fn write_cluster_fence_to_connection(
551 connection: &Connection,
552 leader_url: Option<String>,
553 election_term: u64,
554 ) -> Result<(), AuthorityStoreError> {
555 let (_, authority_generation, authority_rotated_at) =
556 Self::read_public_state_from_connection(connection)?;
557 connection.execute(
558 r#"
559 UPDATE authority_cluster_fence
560 SET
561 leader_url = ?1,
562 election_term = ?2,
563 updated_at = ?3,
564 authority_generation = ?4,
565 authority_rotated_at = ?5
566 WHERE singleton_id = 1
567 "#,
568 params![
569 leader_url,
570 election_term as i64,
571 unix_now() as i64,
572 authority_generation as i64,
573 authority_rotated_at as i64,
574 ],
575 )?;
576 Ok(())
577 }
578}
579
580impl CapabilityAuthority for SqliteCapabilityAuthority {
581 fn authority_public_key(&self) -> PublicKey {
582 self.status()
583 .map(|status| status.public_key)
584 .unwrap_or_else(|_| self.cached_public_key())
585 }
586
587 fn trusted_public_keys(&self) -> Vec<PublicKey> {
588 self.status()
589 .map(|status| status.trusted_public_keys)
590 .unwrap_or_else(|_| self.cached_trusted_public_keys())
591 }
592
593 fn issue_capability(
594 &self,
595 subject: &PublicKey,
596 scope: ChioScope,
597 ttl_seconds: u64,
598 ) -> Result<CapabilityToken, KernelError> {
599 let keypair = self
600 .read_current_keypair()
601 .map_err(|error| KernelError::CapabilityIssuanceFailed(error.to_string()))?;
602 let now = unix_now();
603 let body = CapabilityTokenBody {
604 id: format!("cap-{}", Uuid::now_v7()),
605 issuer: keypair.public_key(),
606 subject: subject.clone(),
607 scope,
608 issued_at: now,
609 expires_at: now.saturating_add(ttl_seconds),
610 delegation_chain: vec![],
611 };
612
613 CapabilityToken::sign(body, &keypair)
614 .map_err(|error| KernelError::CapabilityIssuanceFailed(error.to_string()))
615 }
616}
617
618fn unix_now() -> u64 {
619 std::time::SystemTime::now()
620 .duration_since(std::time::UNIX_EPOCH)
621 .map(|duration| duration.as_secs())
622 .unwrap_or(0)
623}
624
625#[cfg(test)]
626#[allow(clippy::expect_used, clippy::unwrap_used)]
627mod tests {
628 use std::fs;
629 use std::time::{SystemTime, UNIX_EPOCH};
630
631 use super::*;
632 use chio_core::capability::{Operation, ToolGrant};
633 use chio_kernel::LocalCapabilityAuthority;
634
635 fn unique_db_path(prefix: &str) -> std::path::PathBuf {
636 let nonce = SystemTime::now()
637 .duration_since(UNIX_EPOCH)
638 .expect("time before epoch")
639 .as_nanos();
640 std::env::temp_dir().join(format!("{prefix}-{nonce}.sqlite3"))
641 }
642
643 #[test]
644 fn local_capability_authority_signs_capabilities() {
645 let authority = LocalCapabilityAuthority::new(Keypair::generate());
646 let subject = Keypair::generate().public_key();
647 let capability = authority
648 .issue_capability(
649 &subject,
650 ChioScope {
651 grants: vec![ToolGrant {
652 server_id: "srv-a".to_string(),
653 tool_name: "read_file".to_string(),
654 operations: vec![Operation::Invoke],
655 constraints: vec![],
656 max_invocations: None,
657 max_cost_per_invocation: None,
658 max_total_cost: None,
659 dpop_required: None,
660 }],
661 resource_grants: vec![],
662 prompt_grants: vec![],
663 },
664 300,
665 )
666 .unwrap();
667
668 assert_eq!(capability.subject, subject);
669 assert_eq!(capability.issuer, authority.authority_public_key());
670 assert!(capability.id.starts_with("cap-"));
671 assert!(capability.verify_signature().unwrap());
672 }
673
674 #[test]
675 fn sqlite_capability_authority_persists_and_rotates_across_handles() {
676 let path = unique_db_path("chio-authority");
677 let authority_a = SqliteCapabilityAuthority::open(&path).unwrap();
678 let authority_b = SqliteCapabilityAuthority::open(&path).unwrap();
679
680 let before = authority_a.status().unwrap();
681 assert_eq!(before.generation, 1);
682 assert_eq!(authority_b.status().unwrap().public_key, before.public_key);
683
684 let rotated = authority_a.rotate().unwrap();
685 assert_eq!(rotated.generation, 2);
686 assert_ne!(rotated.public_key, before.public_key);
687
688 let observed = authority_b.status().unwrap();
689 assert_eq!(observed.public_key, rotated.public_key);
690 assert_eq!(observed.generation, rotated.generation);
691
692 let _ = fs::remove_file(path);
693 }
694
695 #[test]
696 fn sqlite_capability_authority_issues_with_current_rotated_key() {
697 let path = unique_db_path("chio-authority-issue");
698 let authority = SqliteCapabilityAuthority::open(&path).unwrap();
699 let subject = Keypair::generate().public_key();
700 let first = authority
701 .issue_capability(
702 &subject,
703 ChioScope {
704 grants: vec![ToolGrant {
705 server_id: "srv-a".to_string(),
706 tool_name: "read_file".to_string(),
707 operations: vec![Operation::Invoke],
708 constraints: vec![],
709 max_invocations: None,
710 max_cost_per_invocation: None,
711 max_total_cost: None,
712 dpop_required: None,
713 }],
714 resource_grants: vec![],
715 prompt_grants: vec![],
716 },
717 300,
718 )
719 .unwrap();
720 let rotated = authority.rotate().unwrap();
721 let second = authority
722 .issue_capability(
723 &subject,
724 ChioScope {
725 grants: vec![ToolGrant {
726 server_id: "srv-a".to_string(),
727 tool_name: "read_file".to_string(),
728 operations: vec![Operation::Invoke],
729 constraints: vec![],
730 max_invocations: None,
731 max_cost_per_invocation: None,
732 max_total_cost: None,
733 dpop_required: None,
734 }],
735 resource_grants: vec![],
736 prompt_grants: vec![],
737 },
738 300,
739 )
740 .unwrap();
741
742 assert_ne!(first.issuer, second.issuer);
743 assert_eq!(second.issuer, rotated.public_key);
744 assert!(second.verify_signature().unwrap());
745
746 let _ = fs::remove_file(path);
747 }
748
749 #[test]
750 fn sqlite_capability_authority_snapshot_updates_public_view_without_copying_seed() {
751 let source_path = unique_db_path("chio-authority-source");
752 let follower_path = unique_db_path("chio-authority-follower");
753 let source = SqliteCapabilityAuthority::open(&source_path).unwrap();
754 let follower = SqliteCapabilityAuthority::open(&follower_path).unwrap();
755
756 let follower_local_key = follower.current_keypair().unwrap().public_key();
757 let rotated = source.rotate().unwrap();
758 let snapshot = source.snapshot().unwrap();
759
760 assert!(follower.apply_snapshot(&snapshot).unwrap());
761
762 let follower_status = follower.status().unwrap();
763 assert_eq!(follower_status.public_key, rotated.public_key);
764 assert_eq!(follower_status.generation, rotated.generation);
765 let error = match follower.current_keypair() {
766 Ok(_) => panic!("mismatched follower keypair should be rejected"),
767 Err(error) => error.to_string(),
768 };
769 assert!(error.contains("does not match replicated authority public key"));
770 assert!(error.contains(&follower_local_key.to_hex()));
771
772 let _ = fs::remove_file(source_path);
773 let _ = fs::remove_file(follower_path);
774 }
775
776 #[test]
777 fn sqlite_capability_authority_same_term_snapshot_does_not_clear_fenced_leader() {
778 let path = unique_db_path("chio-authority-fence");
779 let authority = SqliteCapabilityAuthority::open(&path).unwrap();
780
781 assert!(authority
782 .seed_cluster_fence(Some("http://leader-a"), 7)
783 .unwrap());
784 assert!(!authority.seed_cluster_fence(None, 7).unwrap());
785
786 let fence = authority.cluster_fence().unwrap();
787 let status = authority.status().unwrap();
788 assert_eq!(fence.election_term, 7);
789 assert_eq!(fence.leader_url.as_deref(), Some("http://leader-a"));
790 assert_eq!(fence.authority_generation, status.generation);
791 assert_eq!(fence.authority_rotated_at, status.rotated_at);
792
793 let _ = fs::remove_file(path);
794 }
795
796 #[test]
797 fn sqlite_capability_authority_enforce_cluster_fence_rejects_stale_rotation() {
798 let path = unique_db_path("chio-authority-fence-stale-rotation");
799 let authority = SqliteCapabilityAuthority::open(&path).unwrap();
800
801 assert!(authority
802 .seed_cluster_fence(Some("http://leader-a"), 7)
803 .unwrap());
804 let fence = authority.cluster_fence().unwrap();
805 authority.rotate().unwrap();
806
807 let error = authority
808 .enforce_cluster_fence("http://leader-a", 7)
809 .expect_err("stale persisted fence should fail closed")
810 .to_string();
811 assert!(error.contains("persisted authority fence generation"));
812 assert!(error.contains(&fence.authority_generation.to_string()));
813
814 let _ = fs::remove_file(path);
815 }
816
817 #[test]
818 fn sqlite_capability_authority_seed_cluster_fence_refreshes_same_term_authority_state() {
819 let path = unique_db_path("chio-authority-fence-same-term-refresh");
820 let authority = SqliteCapabilityAuthority::open(&path).unwrap();
821
822 assert!(authority
823 .seed_cluster_fence(Some("http://leader-a"), 7)
824 .unwrap());
825 authority.rotate().unwrap();
826
827 assert!(authority
828 .seed_cluster_fence(Some("http://leader-a"), 7)
829 .unwrap());
830 authority
831 .enforce_cluster_fence("http://leader-a", 7)
832 .expect("same-term reseed should refresh authority generation");
833
834 let fence = authority.cluster_fence().unwrap();
835 let status = authority.status().unwrap();
836 assert_eq!(fence.election_term, 7);
837 assert_eq!(fence.leader_url.as_deref(), Some("http://leader-a"));
838 assert_eq!(fence.authority_generation, status.generation);
839 assert_eq!(fence.authority_rotated_at, status.rotated_at);
840
841 let _ = fs::remove_file(path);
842 }
843}