Skip to main content

chio_store_sqlite/
authority.rs

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
27/// Authority-store schema revision. Bump on every schema-affecting change.
28const AUTHORITY_STORE_SUPPORTED_SCHEMA_VERSION: i32 = 1;
29/// Stable key under which this store records its schema revision in the shared
30/// keyed metadata table, distinct from any co-located store's key.
31const AUTHORITY_STORE_SCHEMA_KEY: &str = "authority";
32/// Tables shipped before schema stamping existed, used to adopt a pre-stamping
33/// authority database rather than reject it as foreign.
34const 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        // Resolve any `file:` URI to its on-disk parent before creating it, so a
49        // URI-configured store creates the real backing directory rather than a
50        // bogus scheme-prefixed one.
51        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        // Cluster snapshots replicate verification history, not signing custody.
172        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}