koi_certmesh/
core_admin.rs1use super::*;
7
8impl CertmeshCore {
9 pub async fn rotate_auth(
14 &self,
15 passphrase: &str,
16 method: Option<&str>,
17 ) -> Result<koi_crypto::auth::AuthSetup, CertmeshError> {
18 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 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 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 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 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 pub async fn revoke_member(
174 &self,
175 hostname: &str,
176 operator: Option<String>,
177 reason: Option<String>,
178 ) -> Result<(), CertmeshError> {
179 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}