1use std::fs;
2use std::path::{Path, PathBuf};
3use std::sync::Mutex;
4
5use crate::budget_store::{
6 budget_snapshot_anchor_chain_digest, BudgetSnapshotAnchorCommitment,
7 SignedBudgetSnapshotAnchorCommitment,
8};
9use chio_core::capability::{
10 scope::ChioScope,
11 token::{CapabilityToken, CapabilityTokenBody},
12};
13use chio_core::crypto::{Keypair, PublicKey, Signature};
14use chio_kernel::{
15 ensure_capability_issuance_supported, AuthoritySnapshot, AuthorityStatus, AuthorityStoreError,
16 AuthorityTrustedKeySnapshot, CapabilityAuthority, KernelError,
17};
18use rusqlite::{params, Connection, OptionalExtension};
19use uuid::Uuid;
20
21pub struct SqliteCapabilityAuthority {
22 path: PathBuf,
23 cached_public_key: Mutex<PublicKey>,
24 cached_trusted_public_keys: Mutex<Vec<PublicKey>>,
25}
26
27const AUTHORITY_STORE_SUPPORTED_SCHEMA_VERSION: i32 = 1;
29const AUTHORITY_STORE_SCHEMA_KEY: &str = "authority";
32const AUTHORITY_STORE_LEGACY_ANCHOR_TABLES: &[&str] = &["authority_state"];
35
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct AuthorityClusterFence {
38 pub leader_url: Option<String>,
39 pub election_term: u64,
40 pub updated_at: u64,
41 pub authority_generation: u64,
42 pub authority_rotated_at: u64,
43}
44
45impl SqliteCapabilityAuthority {
46 pub fn open(path: impl AsRef<Path>) -> Result<Self, AuthorityStoreError> {
47 let path = path.as_ref().to_path_buf();
48 if let Some(parent) = crate::sqlite_parent_dir_to_create(&path) {
52 fs::create_dir_all(&parent)?;
53 }
54
55 let bootstrap = Keypair::generate();
56 let connection = Self::open_connection(&path)?;
57 connection.execute(
58 r#"
59 INSERT INTO authority_state (singleton_id, seed_hex, public_key_hex, generation, rotated_at)
60 VALUES (1, ?1, ?2, 1, ?3)
61 ON CONFLICT(singleton_id) DO NOTHING
62 "#,
63 params![
64 bootstrap.seed_hex(),
65 bootstrap.public_key().to_hex(),
66 unix_now() as i64
67 ],
68 )?;
69 let current_public_key = connection
70 .query_row(
71 r#"
72 SELECT seed_hex
73 FROM authority_state
74 WHERE singleton_id = 1
75 "#,
76 [],
77 |row| row.get::<_, String>(0),
78 )
79 .map(|seed_hex| Keypair::from_seed_hex(seed_hex.trim()))
80 .map_err(AuthorityStoreError::from)??;
81 connection.execute(
82 r#"
83 UPDATE authority_state
84 SET public_key_hex = COALESCE(NULLIF(public_key_hex, ''), ?1)
85 WHERE singleton_id = 1
86 "#,
87 params![current_public_key.public_key().to_hex()],
88 )?;
89 connection.execute(
90 r#"
91 INSERT INTO authority_trusted_keys (public_key_hex, generation, activated_at)
92 VALUES (?1, 1, ?2)
93 ON CONFLICT(public_key_hex) DO NOTHING
94 "#,
95 params![current_public_key.public_key().to_hex(), unix_now() as i64],
96 )?;
97 let status = Self::read_status_from_connection(&connection)?;
98 Ok(Self {
99 path,
100 cached_public_key: Mutex::new(status.public_key),
101 cached_trusted_public_keys: Mutex::new(status.trusted_public_keys),
102 })
103 }
104
105 pub fn status(&self) -> Result<AuthorityStatus, AuthorityStoreError> {
106 let connection = Self::open_connection(&self.path)?;
107 let status = Self::read_status_from_connection(&connection)?;
108 self.update_cached_public_key(status.public_key.clone());
109 self.update_cached_trusted_public_keys(status.trusted_public_keys.clone());
110 Ok(status)
111 }
112
113 pub fn rotate(&self) -> Result<AuthorityStatus, AuthorityStoreError> {
114 let connection = Self::open_connection(&self.path)?;
115 let status_before = Self::read_status_from_connection(&connection)?;
116 let keypair = Keypair::generate();
117 let rotated_at = unix_now();
118 let next_generation = status_before.generation.saturating_add(1);
119
120 connection.execute(
121 r#"
122 UPDATE authority_state
123 SET seed_hex = ?1, public_key_hex = ?2, generation = ?3, rotated_at = ?4
124 WHERE singleton_id = 1
125 "#,
126 params![
127 keypair.seed_hex(),
128 keypair.public_key().to_hex(),
129 next_generation as i64,
130 rotated_at as i64,
131 ],
132 )?;
133 connection.execute(
134 r#"
135 INSERT INTO authority_trusted_keys (public_key_hex, generation, activated_at)
136 VALUES (?1, ?2, ?3)
137 ON CONFLICT(public_key_hex) DO NOTHING
138 "#,
139 params![
140 keypair.public_key().to_hex(),
141 next_generation as i64,
142 rotated_at as i64
143 ],
144 )?;
145
146 let status = Self::read_status_from_connection(&connection)?;
147 self.update_cached_public_key(status.public_key.clone());
148 self.update_cached_trusted_public_keys(status.trusted_public_keys.clone());
149 Ok(status)
150 }
151
152 pub fn snapshot(&self) -> Result<AuthoritySnapshot, AuthorityStoreError> {
153 let connection = Self::open_connection(&self.path)?;
154 let status = Self::read_status_from_connection(&connection)?;
155 Ok(AuthoritySnapshot {
156 public_key_hex: status.public_key.to_hex(),
157 generation: status.generation,
158 rotated_at: status.rotated_at,
159 trusted_keys: Self::read_trusted_key_snapshots(&connection)?,
160 })
161 }
162
163 pub fn apply_snapshot(
164 &self,
165 snapshot: &AuthoritySnapshot,
166 ) -> Result<bool, AuthorityStoreError> {
167 let connection = Self::open_connection(&self.path)?;
168 let local_snapshot = self.snapshot()?;
169 let remote_public_key = PublicKey::from_hex(snapshot.public_key_hex.trim())?;
170
171 connection.execute(
173 r#"
174 INSERT INTO authority_trusted_keys (public_key_hex, generation, activated_at)
175 VALUES (?1, ?2, ?3)
176 ON CONFLICT(public_key_hex) DO UPDATE SET
177 generation = MAX(generation, excluded.generation),
178 activated_at = MIN(activated_at, excluded.activated_at)
179 "#,
180 params![
181 remote_public_key.to_hex(),
182 snapshot.generation as i64,
183 snapshot.rotated_at as i64
184 ],
185 )?;
186 for trusted_key in &snapshot.trusted_keys {
187 connection.execute(
188 r#"
189 INSERT INTO authority_trusted_keys (public_key_hex, generation, activated_at)
190 VALUES (?1, ?2, ?3)
191 ON CONFLICT(public_key_hex) DO UPDATE SET
192 generation = MAX(generation, excluded.generation),
193 activated_at = MIN(activated_at, excluded.activated_at)
194 "#,
195 params![
196 trusted_key.public_key_hex,
197 trusted_key.generation as i64,
198 trusted_key.activated_at as i64
199 ],
200 )?;
201 }
202
203 let should_replace = snapshot.generation > local_snapshot.generation
204 || (snapshot.generation == local_snapshot.generation
205 && (snapshot.rotated_at, snapshot.public_key_hex.as_str())
206 > (
207 local_snapshot.rotated_at,
208 local_snapshot.public_key_hex.as_str(),
209 ));
210 if should_replace {
211 connection.execute(
212 r#"
213 UPDATE authority_state
214 SET public_key_hex = ?1, generation = ?2, rotated_at = ?3
215 WHERE singleton_id = 1
216 "#,
217 params![
218 remote_public_key.to_hex(),
219 snapshot.generation as i64,
220 snapshot.rotated_at as i64,
221 ],
222 )?;
223 }
224
225 let status = Self::read_status_from_connection(&connection)?;
226 self.update_cached_public_key(status.public_key);
227 self.update_cached_trusted_public_keys(status.trusted_public_keys);
228 Ok(should_replace)
229 }
230
231 pub fn current_keypair(&self) -> Result<Keypair, AuthorityStoreError> {
232 self.read_current_keypair()
233 }
234
235 pub fn local_keypair(&self) -> Result<Keypair, AuthorityStoreError> {
236 let connection = Self::open_connection(&self.path)?;
237 Self::read_keypair_from_connection(&connection)
238 }
239
240 pub fn commit_budget_snapshot_anchor_set(
241 &self,
242 anchor_set_digest: &str,
243 leader_url: &str,
244 election_term: u64,
245 committed_at: u64,
246 ) -> Result<Vec<SignedBudgetSnapshotAnchorCommitment>, AuthorityStoreError> {
247 if anchor_set_digest.len() != 64
248 || !anchor_set_digest
249 .bytes()
250 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
251 || leader_url.is_empty()
252 || election_term == 0
253 {
254 return Err(AuthorityStoreError::Fence(
255 "budget snapshot anchor commitment identity is invalid".to_string(),
256 ));
257 }
258 let election_term_sqlite = i64::try_from(election_term).map_err(|_| {
259 AuthorityStoreError::Fence(
260 "budget snapshot anchor election term exceeds SQLite range".to_string(),
261 )
262 })?;
263 let committed_at_sqlite = i64::try_from(committed_at).map_err(|_| {
264 AuthorityStoreError::Fence(
265 "budget snapshot anchor commit time exceeds SQLite range".to_string(),
266 )
267 })?;
268 let mut connection = Self::open_connection(&self.path)?;
269 let transaction = connection.transaction()?;
270 let keypair = Self::read_keypair_from_connection(&transaction)?;
271 let signer_public_key = keypair.public_key().to_hex();
272 let latest = transaction
273 .query_row(
274 r#"
275 SELECT commit_sequence, anchor_set_digest, leader_url, election_term,
276 signer_public_key
277 FROM authority_budget_anchor_commits
278 ORDER BY commit_sequence DESC LIMIT 1
279 "#,
280 [],
281 |row| {
282 Ok((
283 row.get::<_, i64>(0)?,
284 row.get::<_, String>(1)?,
285 row.get::<_, String>(2)?,
286 row.get::<_, i64>(3)?,
287 row.get::<_, String>(4)?,
288 ))
289 },
290 )
291 .optional()?;
292 let already_committed = latest
293 .as_ref()
294 .is_some_and(|(_, digest, leader, term, signer)| {
295 digest == anchor_set_digest
296 && leader == leader_url
297 && *term == election_term_sqlite
298 && signer == &signer_public_key
299 });
300 if !already_committed {
301 let (previous_sequence, previous_chain_digest) = transaction
302 .query_row(
303 r#"
304 SELECT commit_sequence, chain_digest
305 FROM authority_budget_anchor_commits
306 ORDER BY commit_sequence DESC LIMIT 1
307 "#,
308 [],
309 |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
310 )
311 .optional()?
312 .unwrap_or((
313 0,
314 "0000000000000000000000000000000000000000000000000000000000000000".to_string(),
315 ));
316 let commit_sequence = u64::try_from(previous_sequence)
317 .ok()
318 .and_then(|value| value.checked_add(1))
319 .ok_or_else(|| {
320 AuthorityStoreError::Fence(
321 "budget snapshot anchor commitment sequence overflowed".to_string(),
322 )
323 })?;
324 let mut body = BudgetSnapshotAnchorCommitment {
325 schema: "chio.budget-snapshot-anchor-commitment.v1".to_string(),
326 commit_sequence,
327 previous_chain_digest,
328 chain_digest: String::new(),
329 anchor_set_digest: anchor_set_digest.to_string(),
330 leader_url: leader_url.to_string(),
331 election_term,
332 committed_at,
333 signer_public_key,
334 };
335 body.chain_digest = budget_snapshot_anchor_chain_digest(&body)
336 .map_err(|error| AuthorityStoreError::Fence(error.to_string()))?;
337 let signature = keypair
338 .sign_canonical(&body)
339 .map_err(|error| AuthorityStoreError::Fence(error.to_string()))?
340 .0;
341 let commit_sequence_sqlite = i64::try_from(commit_sequence).map_err(|_| {
342 AuthorityStoreError::Fence(
343 "budget snapshot anchor commitment sequence exceeds SQLite range".to_string(),
344 )
345 })?;
346 transaction.execute(
347 r#"
348 INSERT INTO authority_budget_anchor_commits (
349 commit_sequence, previous_chain_digest, chain_digest,
350 anchor_set_digest, leader_url, election_term, committed_at,
351 signer_public_key, signature
352 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
353 "#,
354 params![
355 commit_sequence_sqlite,
356 &body.previous_chain_digest,
357 &body.chain_digest,
358 &body.anchor_set_digest,
359 &body.leader_url,
360 election_term_sqlite,
361 committed_at_sqlite,
362 &body.signer_public_key,
363 signature.to_hex(),
364 ],
365 )?;
366 }
367 let chain = Self::read_budget_anchor_commit_chain(&transaction)?;
368 transaction.commit()?;
369 Ok(chain)
370 }
371
372 pub fn cluster_fence(&self) -> Result<AuthorityClusterFence, AuthorityStoreError> {
373 let connection = Self::open_connection(&self.path)?;
374 Self::read_cluster_fence_from_connection(&connection)
375 }
376
377 pub fn seed_cluster_fence(
378 &self,
379 leader_url: Option<&str>,
380 election_term: u64,
381 ) -> Result<bool, AuthorityStoreError> {
382 let connection = Self::open_connection(&self.path)?;
383 let current = Self::read_cluster_fence_from_connection(&connection)?;
384 let (_, authority_generation, authority_rotated_at) =
385 Self::read_public_state_from_connection(&connection)?;
386 let next_leader = leader_url.map(ToOwned::to_owned);
387 let same_term_same_leader = election_term == current.election_term
388 && current.leader_url.as_deref() == next_leader.as_deref();
389 let fence_authority_state_is_stale = (current.election_term > 0
390 || current.leader_url.is_some())
391 && (current.authority_generation != authority_generation
392 || current.authority_rotated_at != authority_rotated_at);
393 let should_update = election_term > current.election_term
394 || (election_term == current.election_term
395 && current.leader_url.is_none()
396 && next_leader.is_some())
397 || (same_term_same_leader && fence_authority_state_is_stale);
398 if should_update {
399 Self::write_cluster_fence_to_connection(&connection, next_leader, election_term)?;
400 }
401 Ok(should_update)
402 }
403
404 pub fn enforce_cluster_fence(
405 &self,
406 leader_url: &str,
407 election_term: u64,
408 ) -> Result<(), AuthorityStoreError> {
409 let connection = Self::open_connection(&self.path)?;
410 let current = Self::read_cluster_fence_from_connection(&connection)?;
411 let (_, authority_generation, authority_rotated_at) =
412 Self::read_public_state_from_connection(&connection)?;
413 if (current.election_term > 0 || current.leader_url.is_some())
414 && (current.authority_generation != authority_generation
415 || current.authority_rotated_at != authority_rotated_at)
416 {
417 return Err(AuthorityStoreError::Fence(format!(
418 "persisted authority fence generation `{}` rotated_at `{}` does not match current authority generation `{authority_generation}` rotated_at `{authority_rotated_at}`",
419 current.authority_generation, current.authority_rotated_at
420 )));
421 }
422 if election_term < current.election_term {
423 return Err(AuthorityStoreError::Fence(format!(
424 "stale authority term `{election_term}` is below persisted term `{}`",
425 current.election_term
426 )));
427 }
428 if election_term == current.election_term
429 && current
430 .leader_url
431 .as_deref()
432 .is_some_and(|current_leader| current_leader != leader_url)
433 {
434 return Err(AuthorityStoreError::Fence(format!(
435 "authority term `{election_term}` is already fenced to leader `{}`",
436 current.leader_url.unwrap_or_default()
437 )));
438 }
439 Self::write_cluster_fence_to_connection(
440 &connection,
441 Some(leader_url.to_string()),
442 election_term,
443 )
444 }
445
446 fn open_connection(path: &Path) -> Result<Connection, AuthorityStoreError> {
447 let connection = Connection::open(path)?;
448 crate::check_schema_version(
449 &connection,
450 AUTHORITY_STORE_SCHEMA_KEY,
451 AUTHORITY_STORE_SUPPORTED_SCHEMA_VERSION,
452 AUTHORITY_STORE_LEGACY_ANCHOR_TABLES,
453 )
454 .map_err(|error| AuthorityStoreError::Schema(error.to_string()))?;
455 connection.execute_batch(
456 r#"
457 PRAGMA journal_mode = WAL;
458 PRAGMA synchronous = FULL;
459 PRAGMA busy_timeout = 5000;
460
461 CREATE TABLE IF NOT EXISTS authority_state (
462 singleton_id INTEGER PRIMARY KEY CHECK (singleton_id = 1),
463 seed_hex TEXT NOT NULL,
464 public_key_hex TEXT,
465 generation INTEGER NOT NULL,
466 rotated_at INTEGER NOT NULL
467 );
468
469 CREATE TABLE IF NOT EXISTS authority_trusted_keys (
470 public_key_hex TEXT PRIMARY KEY,
471 generation INTEGER NOT NULL,
472 activated_at INTEGER NOT NULL
473 );
474
475 CREATE TABLE IF NOT EXISTS authority_cluster_fence (
476 singleton_id INTEGER PRIMARY KEY CHECK (singleton_id = 1),
477 leader_url TEXT,
478 election_term INTEGER NOT NULL,
479 updated_at INTEGER NOT NULL,
480 authority_generation INTEGER NOT NULL DEFAULT 0,
481 authority_rotated_at INTEGER NOT NULL DEFAULT 0
482 );
483
484 INSERT INTO authority_cluster_fence (singleton_id, leader_url, election_term, updated_at)
485 VALUES (1, NULL, 0, 0)
486 ON CONFLICT(singleton_id) DO NOTHING;
487
488 CREATE TABLE IF NOT EXISTS authority_budget_anchor_commits (
489 commit_sequence INTEGER PRIMARY KEY CHECK (commit_sequence > 0),
490 previous_chain_digest TEXT NOT NULL,
491 chain_digest TEXT NOT NULL UNIQUE,
492 anchor_set_digest TEXT NOT NULL,
493 leader_url TEXT NOT NULL CHECK (leader_url <> ''),
494 election_term INTEGER NOT NULL CHECK (election_term > 0),
495 committed_at INTEGER NOT NULL CHECK (committed_at >= 0),
496 signer_public_key TEXT NOT NULL CHECK (signer_public_key <> ''),
497 signature TEXT NOT NULL CHECK (signature <> '')
498 );
499
500 CREATE TRIGGER IF NOT EXISTS authority_budget_anchor_commits_immutable
501 BEFORE UPDATE ON authority_budget_anchor_commits
502 BEGIN
503 SELECT RAISE(ABORT, 'budget anchor commitment is immutable');
504 END;
505
506 CREATE TRIGGER IF NOT EXISTS authority_budget_anchor_commits_no_delete
507 BEFORE DELETE ON authority_budget_anchor_commits
508 BEGIN
509 SELECT RAISE(ABORT, 'budget anchor commitment is immutable');
510 END;
511 "#,
512 )?;
513 if !Self::table_has_column(&connection, "authority_state", "public_key_hex")? {
514 connection.execute(
515 "ALTER TABLE authority_state ADD COLUMN public_key_hex TEXT",
516 [],
517 )?;
518 }
519 if !Self::table_has_column(
520 &connection,
521 "authority_cluster_fence",
522 "authority_generation",
523 )? {
524 connection.execute(
525 "ALTER TABLE authority_cluster_fence ADD COLUMN authority_generation INTEGER NOT NULL DEFAULT 0",
526 [],
527 )?;
528 }
529 if !Self::table_has_column(
530 &connection,
531 "authority_cluster_fence",
532 "authority_rotated_at",
533 )? {
534 connection.execute(
535 "ALTER TABLE authority_cluster_fence ADD COLUMN authority_rotated_at INTEGER NOT NULL DEFAULT 0",
536 [],
537 )?;
538 }
539 crate::stamp_schema_version(
540 &connection,
541 AUTHORITY_STORE_SCHEMA_KEY,
542 AUTHORITY_STORE_SUPPORTED_SCHEMA_VERSION,
543 )
544 .map_err(|error| AuthorityStoreError::Schema(error.to_string()))?;
545 Ok(connection)
546 }
547
548 fn read_status_from_connection(
549 connection: &Connection,
550 ) -> Result<AuthorityStatus, AuthorityStoreError> {
551 let (public_key, generation, rotated_at) =
552 Self::read_public_state_from_connection(connection)?;
553 Ok(AuthorityStatus {
554 public_key,
555 generation,
556 rotated_at,
557 trusted_public_keys: Self::read_trusted_public_keys(connection)?,
558 })
559 }
560
561 fn read_keypair_from_connection(
562 connection: &Connection,
563 ) -> Result<Keypair, AuthorityStoreError> {
564 let seed_hex = connection.query_row(
565 r#"
566 SELECT seed_hex
567 FROM authority_state
568 WHERE singleton_id = 1
569 "#,
570 [],
571 |row| row.get::<_, String>(0),
572 )?;
573 Keypair::from_seed_hex(seed_hex.trim()).map_err(Into::into)
574 }
575
576 fn read_budget_anchor_commit_chain(
577 connection: &Connection,
578 ) -> Result<Vec<SignedBudgetSnapshotAnchorCommitment>, AuthorityStoreError> {
579 let mut statement = connection.prepare(
580 r#"
581 SELECT commit_sequence, previous_chain_digest, chain_digest,
582 anchor_set_digest, leader_url, election_term, committed_at,
583 signer_public_key, signature
584 FROM authority_budget_anchor_commits
585 ORDER BY commit_sequence
586 "#,
587 )?;
588 let rows = statement.query_map([], |row| {
589 Ok((
590 row.get::<_, i64>(0)?,
591 row.get::<_, String>(1)?,
592 row.get::<_, String>(2)?,
593 row.get::<_, String>(3)?,
594 row.get::<_, String>(4)?,
595 row.get::<_, i64>(5)?,
596 row.get::<_, i64>(6)?,
597 row.get::<_, String>(7)?,
598 row.get::<_, String>(8)?,
599 ))
600 })?;
601 rows.map(|row| {
602 let (sequence, previous, chain, anchors, leader, term, at, signer, signature) = row?;
603 Ok(SignedBudgetSnapshotAnchorCommitment {
604 body: BudgetSnapshotAnchorCommitment {
605 schema: "chio.budget-snapshot-anchor-commitment.v1".to_string(),
606 commit_sequence: sequence.max(0) as u64,
607 previous_chain_digest: previous,
608 chain_digest: chain,
609 anchor_set_digest: anchors,
610 leader_url: leader,
611 election_term: term.max(0) as u64,
612 committed_at: at.max(0) as u64,
613 signer_public_key: signer,
614 },
615 signature: Signature::from_hex(&signature)?,
616 })
617 })
618 .collect()
619 }
620
621 fn read_public_state_from_connection(
622 connection: &Connection,
623 ) -> Result<(PublicKey, u64, u64), AuthorityStoreError> {
624 let (seed_hex, public_key_hex, generation, rotated_at) = connection.query_row(
625 r#"
626 SELECT seed_hex, public_key_hex, generation, rotated_at
627 FROM authority_state
628 WHERE singleton_id = 1
629 "#,
630 [],
631 |row| {
632 Ok((
633 row.get::<_, String>(0)?,
634 row.get::<_, Option<String>>(1)?,
635 row.get::<_, i64>(2)?,
636 row.get::<_, i64>(3)?,
637 ))
638 },
639 )?;
640 let public_key = match public_key_hex
641 .as_deref()
642 .map(str::trim)
643 .filter(|value| !value.is_empty())
644 {
645 Some(public_key_hex) => PublicKey::from_hex(public_key_hex)?,
646 None => Keypair::from_seed_hex(seed_hex.trim())?.public_key(),
647 };
648 Ok((
649 public_key,
650 generation.max(0) as u64,
651 rotated_at.max(0) as u64,
652 ))
653 }
654
655 fn read_current_keypair(&self) -> Result<Keypair, AuthorityStoreError> {
656 let connection = Self::open_connection(&self.path)?;
657 let keypair = Self::read_keypair_from_connection(&connection)?;
658 let status = Self::read_status_from_connection(&connection)?;
659 self.update_cached_public_key(status.public_key.clone());
660 self.update_cached_trusted_public_keys(status.trusted_public_keys.clone());
661 if keypair.public_key() != status.public_key {
662 return Err(AuthorityStoreError::Fence(format!(
663 "local signing seed public key {} does not match replicated authority public key {}",
664 keypair.public_key().to_hex(),
665 status.public_key.to_hex(),
666 )));
667 }
668 Ok(keypair)
669 }
670
671 fn table_has_column(
672 connection: &Connection,
673 table: &str,
674 column: &str,
675 ) -> Result<bool, AuthorityStoreError> {
676 let pragma = format!("PRAGMA table_info({table})");
677 let mut statement = connection.prepare(&pragma)?;
678 let mut rows = statement.query([])?;
679 while let Some(row) = rows.next()? {
680 if row.get::<_, String>(1)? == column {
681 return Ok(true);
682 }
683 }
684 Ok(false)
685 }
686
687 fn update_cached_public_key(&self, public_key: PublicKey) {
688 match self.cached_public_key.lock() {
689 Ok(mut guard) => *guard = public_key,
690 Err(poisoned) => {
691 tracing::error!(
692 "cached_public_key mutex is poisoned - recovering possibly-stale key material"
693 );
694 *poisoned.into_inner() = public_key;
695 }
696 }
697 }
698
699 fn update_cached_trusted_public_keys(&self, public_keys: Vec<PublicKey>) {
700 match self.cached_trusted_public_keys.lock() {
701 Ok(mut guard) => *guard = public_keys,
702 Err(poisoned) => {
703 tracing::error!(
704 "cached_trusted_public_keys mutex is poisoned - recovering possibly-stale key material"
705 );
706 *poisoned.into_inner() = public_keys;
707 }
708 }
709 }
710
711 fn cached_public_key(&self) -> PublicKey {
712 match self.cached_public_key.lock() {
713 Ok(guard) => guard.clone(),
714 Err(poisoned) => {
715 tracing::error!(
716 "cached_public_key mutex is poisoned - reading possibly-stale key material"
717 );
718 poisoned.into_inner().clone()
719 }
720 }
721 }
722
723 fn cached_trusted_public_keys(&self) -> Vec<PublicKey> {
724 match self.cached_trusted_public_keys.lock() {
725 Ok(guard) => guard.clone(),
726 Err(poisoned) => {
727 tracing::error!(
728 "cached_trusted_public_keys mutex is poisoned - reading possibly-stale key material"
729 );
730 poisoned.into_inner().clone()
731 }
732 }
733 }
734
735 fn read_trusted_public_keys(
736 connection: &Connection,
737 ) -> Result<Vec<PublicKey>, AuthorityStoreError> {
738 let mut statement = connection.prepare(
739 r#"
740 SELECT public_key_hex
741 FROM authority_trusted_keys
742 ORDER BY generation ASC, activated_at ASC
743 "#,
744 )?;
745 let rows = statement.query_map([], |row| row.get::<_, String>(0))?;
746 rows.map(|row| {
747 let public_key_hex = row?;
748 PublicKey::from_hex(public_key_hex.trim()).map_err(AuthorityStoreError::from)
749 })
750 .collect()
751 }
752
753 fn read_trusted_key_snapshots(
754 connection: &Connection,
755 ) -> Result<Vec<AuthorityTrustedKeySnapshot>, AuthorityStoreError> {
756 let mut statement = connection.prepare(
757 r#"
758 SELECT public_key_hex, generation, activated_at
759 FROM authority_trusted_keys
760 ORDER BY generation ASC, activated_at ASC, public_key_hex ASC
761 "#,
762 )?;
763 let rows = statement.query_map([], |row| {
764 Ok(AuthorityTrustedKeySnapshot {
765 public_key_hex: row.get(0)?,
766 generation: row.get::<_, i64>(1)?.max(0) as u64,
767 activated_at: row.get::<_, i64>(2)?.max(0) as u64,
768 })
769 })?;
770 rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
771 }
772
773 fn read_cluster_fence_from_connection(
774 connection: &Connection,
775 ) -> Result<AuthorityClusterFence, AuthorityStoreError> {
776 let (leader_url, election_term, updated_at, authority_generation, authority_rotated_at) =
777 connection.query_row(
778 r#"
779 SELECT leader_url, election_term, updated_at, authority_generation, authority_rotated_at
780 FROM authority_cluster_fence
781 WHERE singleton_id = 1
782 "#,
783 [],
784 |row| {
785 Ok((
786 row.get::<_, Option<String>>(0)?,
787 row.get::<_, i64>(1)?,
788 row.get::<_, i64>(2)?,
789 row.get::<_, i64>(3)?,
790 row.get::<_, i64>(4)?,
791 ))
792 },
793 )?;
794 Ok(AuthorityClusterFence {
795 leader_url,
796 election_term: election_term.max(0) as u64,
797 updated_at: updated_at.max(0) as u64,
798 authority_generation: authority_generation.max(0) as u64,
799 authority_rotated_at: authority_rotated_at.max(0) as u64,
800 })
801 }
802
803 fn write_cluster_fence_to_connection(
804 connection: &Connection,
805 leader_url: Option<String>,
806 election_term: u64,
807 ) -> Result<(), AuthorityStoreError> {
808 let (_, authority_generation, authority_rotated_at) =
809 Self::read_public_state_from_connection(connection)?;
810 connection.execute(
811 r#"
812 UPDATE authority_cluster_fence
813 SET
814 leader_url = ?1,
815 election_term = ?2,
816 updated_at = ?3,
817 authority_generation = ?4,
818 authority_rotated_at = ?5
819 WHERE singleton_id = 1
820 "#,
821 params![
822 leader_url,
823 election_term as i64,
824 unix_now() as i64,
825 authority_generation as i64,
826 authority_rotated_at as i64,
827 ],
828 )?;
829 Ok(())
830 }
831}
832
833impl CapabilityAuthority for SqliteCapabilityAuthority {
834 fn authority_public_key(&self) -> PublicKey {
835 self.status()
836 .map(|status| status.public_key)
837 .unwrap_or_else(|_| self.cached_public_key())
838 }
839
840 fn trusted_public_keys(&self) -> Vec<PublicKey> {
841 self.status()
842 .map(|status| status.trusted_public_keys)
843 .unwrap_or_else(|_| self.cached_trusted_public_keys())
844 }
845
846 fn issue_capability(
847 &self,
848 subject: &PublicKey,
849 scope: ChioScope,
850 ttl_seconds: u64,
851 ) -> Result<CapabilityToken, KernelError> {
852 ensure_capability_issuance_supported(&scope)?;
853 let keypair = self
854 .read_current_keypair()
855 .map_err(|error| KernelError::CapabilityIssuanceFailed(error.to_string()))?;
856 let now = unix_now();
857 let body = CapabilityTokenBody {
858 id: format!("cap-{}", Uuid::now_v7()),
859 issuer: keypair.public_key(),
860 subject: subject.clone(),
861 scope,
862 issued_at: now,
863 expires_at: now.saturating_add(ttl_seconds),
864 delegation_chain: vec![],
865 aggregate_invocation_budget: None,
866 };
867
868 CapabilityToken::sign(body, &keypair)
869 .map_err(|error| KernelError::CapabilityIssuanceFailed(error.to_string()))
870 }
871}
872
873fn unix_now() -> u64 {
874 std::time::SystemTime::now()
875 .duration_since(std::time::UNIX_EPOCH)
876 .map(|duration| duration.as_secs())
877 .unwrap_or(0)
878}
879
880#[cfg(test)]
881#[allow(clippy::expect_used, clippy::unwrap_used)]
882mod tests {
883 use std::fs;
884 use std::time::{SystemTime, UNIX_EPOCH};
885
886 use super::*;
887 use chio_core::capability::scope::{Operation, ToolGrant};
888 use chio_kernel::LocalCapabilityAuthority;
889
890 fn unique_db_path(prefix: &str) -> std::path::PathBuf {
891 let nonce = SystemTime::now()
892 .duration_since(UNIX_EPOCH)
893 .expect("time before epoch")
894 .as_nanos();
895 std::env::temp_dir().join(format!("{prefix}-{nonce}.sqlite3"))
896 }
897
898 #[test]
899 fn local_capability_authority_signs_capabilities() {
900 let authority = LocalCapabilityAuthority::new(Keypair::generate());
901 let subject = Keypair::generate().public_key();
902 let capability = authority
903 .issue_capability(
904 &subject,
905 ChioScope {
906 grants: vec![ToolGrant {
907 server_id: "srv-a".to_string(),
908 tool_name: "read_file".to_string(),
909 operations: vec![Operation::Invoke],
910 constraints: vec![],
911 max_invocations: None,
912 max_cost_per_invocation: None,
913 max_total_cost: None,
914 dpop_required: None,
915 }],
916 resource_grants: vec![],
917 prompt_grants: vec![],
918 },
919 300,
920 )
921 .unwrap();
922
923 assert_eq!(capability.subject, subject);
924 assert_eq!(capability.issuer, authority.authority_public_key());
925 assert!(capability.id.starts_with("cap-"));
926 assert!(capability.verify_signature().unwrap());
927 }
928
929 #[test]
930 fn sqlite_capability_authority_persists_and_rotates_across_handles() {
931 let path = unique_db_path("chio-authority");
932 let authority_a = SqliteCapabilityAuthority::open(&path).unwrap();
933 let authority_b = SqliteCapabilityAuthority::open(&path).unwrap();
934
935 let before = authority_a.status().unwrap();
936 assert_eq!(before.generation, 1);
937 assert_eq!(authority_b.status().unwrap().public_key, before.public_key);
938
939 let rotated = authority_a.rotate().unwrap();
940 assert_eq!(rotated.generation, 2);
941 assert_ne!(rotated.public_key, before.public_key);
942
943 let observed = authority_b.status().unwrap();
944 assert_eq!(observed.public_key, rotated.public_key);
945 assert_eq!(observed.generation, rotated.generation);
946
947 let _ = fs::remove_file(path);
948 }
949
950 #[test]
951 fn sqlite_capability_authority_issues_with_current_rotated_key() {
952 let path = unique_db_path("chio-authority-issue");
953 let authority = SqliteCapabilityAuthority::open(&path).unwrap();
954 let subject = Keypair::generate().public_key();
955 let first = authority
956 .issue_capability(
957 &subject,
958 ChioScope {
959 grants: vec![ToolGrant {
960 server_id: "srv-a".to_string(),
961 tool_name: "read_file".to_string(),
962 operations: vec![Operation::Invoke],
963 constraints: vec![],
964 max_invocations: None,
965 max_cost_per_invocation: None,
966 max_total_cost: None,
967 dpop_required: None,
968 }],
969 resource_grants: vec![],
970 prompt_grants: vec![],
971 },
972 300,
973 )
974 .unwrap();
975 let rotated = authority.rotate().unwrap();
976 let second = authority
977 .issue_capability(
978 &subject,
979 ChioScope {
980 grants: vec![ToolGrant {
981 server_id: "srv-a".to_string(),
982 tool_name: "read_file".to_string(),
983 operations: vec![Operation::Invoke],
984 constraints: vec![],
985 max_invocations: None,
986 max_cost_per_invocation: None,
987 max_total_cost: None,
988 dpop_required: None,
989 }],
990 resource_grants: vec![],
991 prompt_grants: vec![],
992 },
993 300,
994 )
995 .unwrap();
996
997 assert_ne!(first.issuer, second.issuer);
998 assert_eq!(second.issuer, rotated.public_key);
999 assert!(second.verify_signature().unwrap());
1000
1001 let _ = fs::remove_file(path);
1002 }
1003
1004 #[test]
1005 fn sqlite_capability_authority_snapshot_updates_public_view_without_copying_seed() {
1006 let source_path = unique_db_path("chio-authority-source");
1007 let follower_path = unique_db_path("chio-authority-follower");
1008 let source = SqliteCapabilityAuthority::open(&source_path).unwrap();
1009 let follower = SqliteCapabilityAuthority::open(&follower_path).unwrap();
1010
1011 let follower_local_key = follower.current_keypair().unwrap().public_key();
1012 let rotated = source.rotate().unwrap();
1013 let snapshot = source.snapshot().unwrap();
1014
1015 assert!(follower.apply_snapshot(&snapshot).unwrap());
1016
1017 let follower_status = follower.status().unwrap();
1018 assert_eq!(follower_status.public_key, rotated.public_key);
1019 assert_eq!(follower_status.generation, rotated.generation);
1020 let error = match follower.current_keypair() {
1021 Ok(_) => panic!("mismatched follower keypair should be rejected"),
1022 Err(error) => error.to_string(),
1023 };
1024 assert!(error.contains("does not match replicated authority public key"));
1025 assert!(error.contains(&follower_local_key.to_hex()));
1026
1027 let _ = fs::remove_file(source_path);
1028 let _ = fs::remove_file(follower_path);
1029 }
1030
1031 #[test]
1032 fn sqlite_capability_authority_same_term_snapshot_does_not_clear_fenced_leader() {
1033 let path = unique_db_path("chio-authority-fence");
1034 let authority = SqliteCapabilityAuthority::open(&path).unwrap();
1035
1036 assert!(authority
1037 .seed_cluster_fence(Some("http://leader-a"), 7)
1038 .unwrap());
1039 assert!(!authority.seed_cluster_fence(None, 7).unwrap());
1040
1041 let fence = authority.cluster_fence().unwrap();
1042 let status = authority.status().unwrap();
1043 assert_eq!(fence.election_term, 7);
1044 assert_eq!(fence.leader_url.as_deref(), Some("http://leader-a"));
1045 assert_eq!(fence.authority_generation, status.generation);
1046 assert_eq!(fence.authority_rotated_at, status.rotated_at);
1047
1048 let _ = fs::remove_file(path);
1049 }
1050
1051 #[test]
1052 fn sqlite_capability_authority_enforce_cluster_fence_rejects_stale_rotation() {
1053 let path = unique_db_path("chio-authority-fence-stale-rotation");
1054 let authority = SqliteCapabilityAuthority::open(&path).unwrap();
1055
1056 assert!(authority
1057 .seed_cluster_fence(Some("http://leader-a"), 7)
1058 .unwrap());
1059 let fence = authority.cluster_fence().unwrap();
1060 authority.rotate().unwrap();
1061
1062 let error = authority
1063 .enforce_cluster_fence("http://leader-a", 7)
1064 .expect_err("stale persisted fence should fail closed")
1065 .to_string();
1066 assert!(error.contains("persisted authority fence generation"));
1067 assert!(error.contains(&fence.authority_generation.to_string()));
1068
1069 let _ = fs::remove_file(path);
1070 }
1071
1072 #[test]
1073 fn sqlite_capability_authority_seed_cluster_fence_refreshes_same_term_authority_state() {
1074 let path = unique_db_path("chio-authority-fence-same-term-refresh");
1075 let authority = SqliteCapabilityAuthority::open(&path).unwrap();
1076
1077 assert!(authority
1078 .seed_cluster_fence(Some("http://leader-a"), 7)
1079 .unwrap());
1080 authority.rotate().unwrap();
1081
1082 assert!(authority
1083 .seed_cluster_fence(Some("http://leader-a"), 7)
1084 .unwrap());
1085 authority
1086 .enforce_cluster_fence("http://leader-a", 7)
1087 .expect("same-term reseed should refresh authority generation");
1088
1089 let fence = authority.cluster_fence().unwrap();
1090 let status = authority.status().unwrap();
1091 assert_eq!(fence.election_term, 7);
1092 assert_eq!(fence.leader_url.as_deref(), Some("http://leader-a"));
1093 assert_eq!(fence.authority_generation, status.generation);
1094 assert_eq!(fence.authority_rotated_at, status.rotated_at);
1095
1096 let _ = fs::remove_file(path);
1097 }
1098}