Skip to main content

koi_certmesh/
core_admin.rs

1//! Auth-credential rotation, encrypted backup/restore, and revocation.
2//!
3//! Part of the inherent impl CertmeshCore, split from lib.rs (certmesh M2).
4//! As a child module of the crate root, 'use super::*' inherits lib.rs's
5//! imports, sibling modules, and crate-private state/helpers as in the original.
6use super::*;
7
8impl CertmeshCore {
9    /// Rotate the auth credential - generates new credential, persists, returns setup info.
10    ///
11    /// If `method` is `None`, keeps the current method. If `Some("totp")`,
12    /// switches to that method.
13    pub async fn rotate_auth(
14        &self,
15        passphrase: &str,
16        method: Option<&str>,
17    ) -> Result<koi_crypto::auth::AuthSetup, CertmeshError> {
18        // Verify CA is unlocked
19        let ca_guard = self.state.ca.lock().await;
20        if ca_guard.is_none() {
21            return Err(if self.state.paths.is_ca_initialized() {
22                CertmeshError::CaLocked
23            } else {
24                CertmeshError::CaNotInitialized
25            });
26        }
27        drop(ca_guard);
28
29        let current_method = self
30            .state
31            .auth
32            .lock()
33            .await
34            .as_ref()
35            .map(|a| a.method_name())
36            .unwrap_or("totp");
37        let target = method.unwrap_or(current_method);
38
39        let (new_state, stored, setup) = match target {
40            "totp" => {
41                let new_secret = koi_crypto::totp::generate_secret();
42                let stored = koi_crypto::auth::store_totp(&new_secret, passphrase)?;
43                let uri =
44                    koi_crypto::totp::build_totp_uri(&new_secret, "Koi Certmesh", "enrollment");
45                let setup = koi_crypto::auth::AuthSetup::Totp { totp_uri: uri };
46                (AuthState::Totp(new_secret), stored, setup)
47            }
48            other => {
49                return Err(CertmeshError::Internal(format!(
50                    "unknown auth method: {other}"
51                )));
52            }
53        };
54
55        let json = serde_json::to_string_pretty(&stored)
56            .map_err(|e| CertmeshError::Internal(format!("auth serialize: {e}")))?;
57        let auth_path = self.state.paths.auth_path();
58        tokio::task::spawn_blocking(move || std::fs::write(&auth_path, &json))
59            .await
60            .map_err(|e| CertmeshError::Internal(format!("file I/O: {e}")))?
61            .map_err(CertmeshError::Io)?;
62        *self.state.auth.lock().await = Some(new_state);
63
64        tracing::info!(method = target, "auth credential rotated");
65        let _ = audit::append_entry_to(
66            &self.state.paths.audit_log_path(),
67            "auth_rotated",
68            &[("method", target)],
69        );
70        Ok(setup)
71    }
72
73    // ── Phase 5 - Backup/Restore/Revocation ───────────────────────
74
75    /// Create an encrypted backup bundle for the certmesh state.
76    pub async fn backup(
77        &self,
78        ca_passphrase: &str,
79        backup_passphrase: &str,
80    ) -> Result<Vec<u8>, CertmeshError> {
81        if !self.state.paths.is_ca_initialized() {
82            return Err(CertmeshError::CaNotInitialized);
83        }
84
85        let ca_state = ca::load_ca(ca_passphrase, &self.state.paths)?;
86
87        // Load auth state for backup
88        let auth_path = self.state.paths.auth_path();
89        let json = std::fs::read_to_string(&auth_path)
90            .map_err(|e| CertmeshError::Internal(format!("cannot read auth.json: {e}")))?;
91        let stored: koi_crypto::auth::StoredAuth = serde_json::from_str(&json)
92            .map_err(|e| CertmeshError::Internal(format!("auth.json parse error: {e}")))?;
93        let auth_state = stored
94            .unlock(ca_passphrase)
95            .map_err(|e| CertmeshError::Internal(format!("auth unlock failed: {e}")))?;
96
97        let roster = self.state.roster.lock().await;
98        let roster_json = serde_json::to_string(&*roster)
99            .map_err(|e| CertmeshError::Internal(format!("roster serialization failed: {e}")))?;
100
101        let audit_log =
102            audit::read_log_from(&self.state.paths.audit_log_path()).map_err(CertmeshError::Io)?;
103
104        let ca_key_pem = ca_state
105            .key
106            .private_key_pem()
107            .map_err(|e| CertmeshError::Crypto(e.to_string()))?
108            .to_string();
109        let payload = backup::BackupPayload::new(
110            ca_key_pem,
111            ca_state.cert_pem.clone(),
112            auth_state.method_name().to_string(),
113            auth_state.to_backup_bytes(),
114            roster_json,
115            audit_log,
116        );
117
118        let bundle = backup::encode_backup(&payload, backup_passphrase)?;
119        let _ = audit::append_entry_to(&self.state.paths.audit_log_path(), "backup_created", &[]);
120        Ok(bundle)
121    }
122
123    /// Restore certmesh state from an encrypted backup bundle.
124    pub async fn restore(
125        &self,
126        backup_bytes: &[u8],
127        backup_passphrase: &str,
128        new_passphrase: &str,
129    ) -> Result<(), CertmeshError> {
130        let payload = backup::decode_backup(backup_bytes, backup_passphrase)?;
131
132        let ca_key = koi_crypto::keys::ca_keypair_from_pem(&payload.ca_key_pem)?;
133        let ca_key_der = koi_crypto::keys::ca_keypair_to_der(&ca_key)?;
134        let (encrypted_key, slot_table, _master_key) =
135            koi_crypto::unlock_slots::envelope_encrypt_new(&ca_key_der, new_passphrase)?;
136        std::fs::create_dir_all(self.state.paths.ca_dir())?;
137        koi_crypto::keys::save_encrypted_key(&self.state.paths.ca_key_path(), &encrypted_key)?;
138        slot_table.save(&self.state.paths.slot_table_path())?;
139        std::fs::write(self.state.paths.ca_cert_path(), &payload.ca_cert_pem)?;
140
141        let auth_state = AuthState::from_backup(&payload.auth_method, payload.auth_data)
142            .map_err(|e| CertmeshError::Internal(format!("auth restore failed: {e}")))?;
143
144        // Persist restored auth credential
145        let AuthState::Totp(secret) = &auth_state;
146        let stored = koi_crypto::auth::store_totp(secret, new_passphrase)?;
147        let auth_json = serde_json::to_string_pretty(&stored)
148            .map_err(|e| CertmeshError::Internal(format!("auth serialize: {e}")))?;
149        std::fs::write(self.state.paths.auth_path(), auth_json)?;
150
151        if let Some(parent) = self.state.paths.roster_path().parent() {
152            std::fs::create_dir_all(parent)?;
153        }
154        std::fs::write(self.state.paths.roster_path(), &payload.roster_json)?;
155        if let Some(parent) = self.state.paths.audit_log_path().parent() {
156            std::fs::create_dir_all(parent)?;
157        }
158        std::fs::write(self.state.paths.audit_log_path(), &payload.audit_log)?;
159
160        let restored_roster: Roster = serde_json::from_str(&payload.roster_json)
161            .map_err(|e| CertmeshError::Internal(format!("roster deserialization failed: {e}")))?;
162
163        let ca_state = ca::load_ca(new_passphrase, &self.state.paths)?;
164        *self.state.ca.lock().await = Some(ca_state);
165        *self.state.auth.lock().await = Some(auth_state);
166        *self.state.roster.lock().await = restored_roster;
167
168        let _ = audit::append_entry_to(&self.state.paths.audit_log_path(), "backup_restored", &[]);
169        Ok(())
170    }
171
172    /// Revoke a member and persist the revocation list.
173    pub async fn revoke_member(
174        &self,
175        hostname: &str,
176        operator: Option<String>,
177        reason: Option<String>,
178    ) -> Result<(), CertmeshError> {
179        // Membership change → commit_roster bumps `seq` so the revocation
180        // propagates in the next trust bundle (ADR-017 F4/F8).
181        self.state
182            .commit_roster(|roster| {
183                roster
184                    .revoke_member(hostname, operator.clone(), reason.clone())
185                    .map_err(CertmeshError::NotFound)
186            })
187            .await?;
188
189        let _ = self.state.event_tx.send(CertmeshEvent::MemberRevoked {
190            hostname: hostname.to_string(),
191        });
192
193        let _ = audit::append_entry_to(
194            &self.state.paths.audit_log_path(),
195            "member_revoked",
196            &[
197                ("hostname", hostname),
198                ("operator", operator.as_deref().unwrap_or("unknown")),
199                ("reason", reason.as_deref().unwrap_or("none")),
200            ],
201        );
202        Ok(())
203    }
204}