use std::sync::Arc;
use koi_certmesh::entropy;
use koi_certmesh::profiles::preset_bools;
use koi_common::encoding::{hex_decode, hex_encode};
use koi_mdns::events::MdnsEvent;
use crate::client::KoiClient;
use crate::format;
const CA_DISCOVERY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
mod color {
use std::io::IsTerminal;
fn enabled() -> bool {
static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ENABLED.get_or_init(|| {
if std::env::var_os("NO_COLOR").is_some() {
return false;
}
if std::env::var("TERM")
.map(|t| t.eq_ignore_ascii_case("dumb"))
.unwrap_or(false)
{
return false;
}
std::io::stdout().is_terminal()
})
}
fn wrap(code: &str, text: &str) -> String {
if enabled() {
format!("\x1b[{code}m{text}\x1b[0m")
} else {
text.to_string()
}
}
pub fn green(text: &str) -> String {
wrap("32", text)
}
pub fn yellow(text: &str) -> String {
wrap("33", text)
}
pub fn red(text: &str) -> String {
wrap("31", text)
}
pub fn dim(text: &str) -> String {
wrap("2", text)
}
}
fn require_daemon(
endpoint: Option<&str>,
explicit_token: Option<&str>,
) -> anyhow::Result<KoiClient> {
crate::commands::require_client(endpoint, explicit_token)
}
#[allow(clippy::too_many_arguments)]
pub fn create(
profile: Option<&str>,
operator: Option<&str>,
enrollment: Option<&str>,
require_approval: Option<bool>,
passphrase: Option<&str>,
json: bool,
endpoint: Option<&str>,
token: Option<&str>,
) -> anyhow::Result<()> {
let client = require_daemon(endpoint, token)?;
if preflight_ca_exists(&client)? {
return Ok(());
}
if json {
let preset_name =
profile.ok_or_else(|| anyhow::anyhow!("--profile is required with --json"))?;
let (preset_open, preset_approval, preset_auto_unlock) = preset_bools(preset_name)
.ok_or_else(|| {
anyhow::anyhow!(
"Unknown --profile '{preset_name}'. \
Choose just-me, team, or organization."
)
})?;
let ca_passphrase = passphrase
.map(ToString::to_string)
.ok_or_else(|| anyhow::anyhow!("--passphrase is required with --json"))?;
let requires_approval = require_approval.unwrap_or(preset_approval);
let enrollment_open = parse_enrollment_open(enrollment)?.unwrap_or(preset_open);
validate_operator(requires_approval, operator)?;
let entropy_seed =
entropy::collect_entropy(entropy::EntropyMode::Manual(ca_passphrase.clone()))?;
let body = serde_json::json!({
"passphrase": ca_passphrase,
"entropy_hex": hex_encode(&entropy_seed),
"operator": operator,
"enrollment_open": enrollment_open,
"requires_approval": requires_approval,
"auto_unlock": preset_auto_unlock,
});
let resp = client.post_json("/v1/certmesh/create", &body)?;
let ca_fingerprint = resp
.get("ca_fingerprint")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
println!(
"{}",
serde_json::json!({
"created": true,
"profile": preset_label(preset_name),
"enrollment_open": enrollment_open,
"requires_approval": requires_approval,
"ca_fingerprint": ca_fingerprint,
})
);
return Ok(());
}
use koi_certmesh::init_ceremony::InitCeremonyRules;
use koi_common::ceremony::CeremonyHost;
#[allow(clippy::disallowed_methods)]
let ceremony_paths =
koi_certmesh::CertmeshPaths::with_data_dir(koi_common::paths::koi_data_dir());
let host = CeremonyHost::new(InitCeremonyRules::new(ceremony_paths.clone()));
let mut initial_data = serde_json::Map::new();
if let Some(p) = profile {
initial_data.insert("profile".into(), serde_json::json!(p));
}
if let Some(op) = operator {
initial_data.insert("operator".into(), serde_json::json!(op));
}
if let Some(pp) = passphrase {
initial_data.insert("passphrase".into(), serde_json::json!(pp));
}
if let Some(enroll) = enrollment {
initial_data.insert("enrollment_open".into(), serde_json::json!(enroll));
}
if let Some(approve) = require_approval {
initial_data.insert(
"requires_approval".into(),
serde_json::json!(if approve { "yes" } else { "no" }),
);
}
let hostname = hostname::get()
.map(|h| h.to_string_lossy().to_string())
.unwrap_or_else(|_| "localhost".to_string());
initial_data.insert("_self_hostname".into(), serde_json::json!(hostname));
let result_bag = super::ceremony_cli::run_ceremony(&host, "init", initial_data)?;
let effective_profile = result_bag
.get("_effective_profile")
.and_then(|v| v.as_str())
.unwrap_or("Just Me")
.to_string();
let body = serde_json::json!({
"passphrase": result_bag.get("passphrase").and_then(|v| v.as_str()).unwrap_or(""),
"entropy_hex": result_bag.get("_entropy_seed").and_then(|v| v.as_str()).unwrap_or(""),
"operator": result_bag.get("operator").and_then(|v| v.as_str()),
"enrollment_open": result_bag.get("_enrollment_open").and_then(|v| v.as_bool()).unwrap_or(true),
"requires_approval": result_bag.get("_requires_approval").and_then(|v| v.as_bool()).unwrap_or(false),
"auto_unlock": result_bag.get("_auto_unlock").and_then(|v| v.as_bool()).unwrap_or(false),
"totp_secret_hex": result_bag.get("_totp_secret_hex").and_then(|v| v.as_str()),
});
println!("\n Creating certificate mesh...\n");
let resp = client.post_json("/v1/certmesh/create", &body)?;
let ca_fingerprint = resp
.get("ca_fingerprint")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
println!(" {} CA keypair generated (ECDSA P-256)", color::green("✓"));
println!(
" {} Private key encrypted (Argon2id + AES-256-GCM)",
color::green("✓")
);
println!(" {} Roster initialized", color::green("✓"));
println!(" {} Audit log started", color::green("✓"));
println!("\n Verifying setup...\n");
if let Ok(status_resp) = client.get_json("/v1/certmesh/status") {
let ca_initialized = status_resp
.get("ca_initialized")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let member_count = status_resp
.get("member_count")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let ca_locked = status_resp
.get("ca_locked")
.and_then(|v| v.as_bool())
.unwrap_or(true);
println!(
" {} CA initialized",
if ca_initialized {
color::green("✓")
} else {
color::red("✗")
}
);
println!(
" {} CA key decrypts successfully",
if !ca_locked {
color::green("✓")
} else {
color::red("✗")
}
);
println!(
" {} Roster reachable ({member_count} member{})",
if member_count > 0 {
color::green("✓")
} else {
color::red("✗")
},
if member_count == 1 { "" } else { "s" }
);
}
let cert_path = ceremony_paths.certs_dir().join(&hostname);
println!();
print_box(
" ",
Some(&color::green("Certificate mesh created")),
&[
String::new(),
format!("Profile: {effective_profile}"),
format!("CA fingerprint: {}", truncate_str(ca_fingerprint, 35)),
format!("Hostname: {}", truncate_str(&hostname, 35)),
format!(
"Certificates: {}",
truncate_str(&cert_path.display().to_string(), 35)
),
String::new(),
],
);
println!();
println!(" What's next:");
println!(
" {} koi certmesh join",
color::dim("• On another machine:")
);
println!(
" {} koi certmesh unlock",
color::dim("• After a daemon restart:")
);
println!(
" {} koi certmesh status",
color::dim("• Check status anytime:")
);
Ok(())
}
fn validate_operator(requires_approval: bool, operator: Option<&str>) -> anyhow::Result<()> {
if requires_approval && operator.is_none() {
anyhow::bail!("This policy requires --operator <name>.");
}
Ok(())
}
fn truncate_str(s: &str, max: usize) -> String {
if s.chars().count() <= max {
s.to_string()
} else {
let truncated: String = s.chars().take(max - 1).collect();
format!("{truncated}…")
}
}
fn visible_width(s: &str) -> usize {
let mut width = 0usize;
let mut in_escape = false;
for ch in s.chars() {
if in_escape {
if ch == 'm' {
in_escape = false;
}
} else if ch == '\x1b' {
in_escape = true;
} else {
width += 1;
}
}
width
}
fn pad_visible(s: &str, target: usize) -> String {
let vw = visible_width(s);
if vw >= target {
s.to_string()
} else {
format!("{}{}", s, " ".repeat(target - vw))
}
}
fn print_box(indent: &str, title: Option<&str>, lines: &[String]) {
let (tl, tr, bl, br, h, v) = ('╭', '╮', '╰', '╯', '─', '│');
let max_content = lines.iter().map(|l| visible_width(l)).max().unwrap_or(0);
let title_width = title.map(|t| visible_width(t) + 6).unwrap_or(0); let inner = max_content.max(title_width).max(20) + 2;
if let Some(t) = title {
let label = format!("{h}{h} {t} ");
let label_vw = visible_width(&label);
let remaining = if inner + 2 > label_vw {
inner + 2 - label_vw
} else {
1
};
println!(
"{indent}{tl}{label}{}{tr}",
std::iter::repeat_n(h, remaining).collect::<String>()
);
} else {
println!(
"{indent}{tl}{}{tr}",
std::iter::repeat_n(h, inner + 2).collect::<String>()
);
}
for line in lines {
let padded = pad_visible(line, inner);
println!("{indent}{v} {padded} {v}");
}
println!(
"{indent}{bl}{}{br}",
std::iter::repeat_n(h, inner + 2).collect::<String>()
);
}
fn preflight_ca_exists(client: &KoiClient) -> anyhow::Result<bool> {
let status = client.get_json("/v1/certmesh/status")?;
let ca_init = status
.get("ca_initialized")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if !ca_init {
return Ok(false);
}
let enrollment_open = status
.get("enrollment_open")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let fingerprint = status
.get("ca_fingerprint")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let member_count = status
.get("member_count")
.and_then(|v| v.as_u64())
.unwrap_or(0);
println!();
println!(
" {} A certificate mesh already exists on this machine.",
color::yellow("⚠")
);
println!();
println!(
" Enrollment: {}",
if enrollment_open { "open" } else { "closed" }
);
println!(" CA fingerprint: {fingerprint}");
println!(" Members: {member_count} active");
println!();
println!(" {}", color::dim("To inspect: koi certmesh status"));
println!(" {}", color::dim("To destroy: koi certmesh destroy"));
println!();
println!(" No changes made.");
Ok(true)
}
fn preset_label(preset_name: &str) -> &'static str {
match preset_name.to_lowercase().as_str() {
"my_team" | "my-team" | "myteam" | "team" | "2" => "My Team",
"my_organization" | "my-organization" | "myorganization" | "organization" | "org" | "3" => {
"My Organization"
}
_ => "Just Me",
}
}
fn parse_enrollment_open(enrollment: Option<&str>) -> anyhow::Result<Option<bool>> {
match enrollment {
None => Ok(None),
Some(value) => match value.to_ascii_lowercase().as_str() {
"open" => Ok(Some(true)),
"closed" | "close" => Ok(Some(false)),
other => anyhow::bail!("Invalid --enrollment value '{other}'. Use 'open' or 'closed'."),
},
}
}
fn extract_totp_secret_from_uri(uri: &str) -> Option<koi_crypto::totp::TotpSecret> {
let query = uri.split('?').nth(1)?;
for param in query.split('&') {
if let Some(val) = param.strip_prefix("secret=") {
let decoded = base32_decode(val)?;
return Some(koi_crypto::totp::TotpSecret::from_bytes(decoded));
}
}
None
}
fn base32_decode(input: &str) -> Option<Vec<u8>> {
const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
let input = input.trim_end_matches('=').as_bytes();
let mut bits: u64 = 0;
let mut bit_count = 0;
let mut result = Vec::new();
for &c in input {
let c = c.to_ascii_uppercase();
let val = ALPHABET.iter().position(|&a| a == c)? as u64;
bits = (bits << 5) | val;
bit_count += 5;
if bit_count >= 8 {
bit_count -= 8;
result.push((bits >> bit_count) as u8);
bits &= (1 << bit_count) - 1;
}
}
Some(result)
}
pub fn status(json: bool, endpoint: Option<&str>, token: Option<&str>) -> anyhow::Result<()> {
let client = require_daemon(endpoint, token)?;
let resp = client.get_json("/v1/certmesh/status")?;
if json {
println!("{}", serde_json::to_string_pretty(&resp)?);
} else {
let ca_init = resp
.get("ca_initialized")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if !ca_init {
println!("Certificate mesh: not initialized");
println!(" Run `koi certmesh create` to set up a CA.");
} else {
match serde_json::from_value::<koi_certmesh::protocol::CertmeshStatus>(resp.clone()) {
Ok(s) => {
println!("Certificate mesh: active");
println!(" CA locked: {}", s.ca_locked);
println!(
" Enrollment: {} ({})",
if s.enrollment_open { "open" } else { "closed" },
if s.requires_approval {
"approval required"
} else {
"no approval"
}
);
println!(" Members: {}", s.member_count);
for m in &s.members {
println!(" {} ({}) - {}", m.hostname, m.role, m.status);
}
}
Err(_) => {
println!("{}", serde_json::to_string_pretty(&resp)?);
}
}
}
}
Ok(())
}
pub fn log(endpoint: Option<&str>, token: Option<&str>) -> anyhow::Result<()> {
let client = require_daemon(endpoint, token)?;
let resp = client.get_json("/v1/certmesh/log")?;
let entries = resp.get("entries").and_then(|v| v.as_str()).unwrap_or("");
if entries.is_empty() {
println!("No audit log entries.");
} else {
print!("{entries}");
}
Ok(())
}
pub fn unlock(endpoint: Option<&str>, token: Option<&str>) -> anyhow::Result<()> {
let client = require_daemon(endpoint, token)?;
eprintln!("Enter the CA passphrase:");
let mut passphrase = String::new();
std::io::stdin().read_line(&mut passphrase)?;
let passphrase = passphrase.trim();
if passphrase.is_empty() {
anyhow::bail!("Passphrase cannot be empty.");
}
let body = serde_json::json!({ "passphrase": passphrase });
client.post_json("/v1/certmesh/unlock", &body)?;
println!("CA unlocked successfully.");
Ok(())
}
pub fn set_hook(
reload: &str,
json: bool,
endpoint: Option<&str>,
token: Option<&str>,
) -> anyhow::Result<()> {
let client = require_daemon(endpoint, token)?;
let hostname = hostname::get()
.map(|h| h.to_string_lossy().to_string())
.unwrap_or_else(|_| "localhost".to_string());
let body = serde_json::json!({
"hostname": hostname,
"reload": reload,
});
let resp = client.put_json("/v1/certmesh/set-hook", &body)?;
if json {
println!("{}", serde_json::to_string_pretty(&resp)?);
} else {
println!("Reload hook set for {hostname}: {reload}");
}
Ok(())
}
pub async fn join(endpoint: Option<&str>, invite: Option<&str>, json: bool) -> anyhow::Result<()> {
let local = require_daemon(None, None)?;
let (invite_secret, pinned_fp) = match invite {
Some(code) => {
let (secret, fp) = koi_certmesh::invite::decode_code(code);
(Some(secret.to_string()), fp.map(str::to_string))
}
None => (None, None),
};
let resolved_endpoint = match endpoint {
Some(ep) => ep.to_string(),
None => discover_ca(pinned_fp.as_deref()).await?,
};
let remote = KoiClient::new(&resolved_endpoint);
let local_hostname = hostname::get()
.map(|h| h.to_string_lossy().to_string())
.unwrap_or_else(|_| "unknown".to_string());
if let Some(ref pin) = pinned_fp {
let status = remote.get_json("/v1/certmesh/status").map_err(|e| {
anyhow::anyhow!("could not preflight the CA at {resolved_endpoint}: {e}")
})?;
let advertised = status
.get("ca_fingerprint")
.and_then(|v| v.as_str())
.ok_or_else(|| {
anyhow::anyhow!(
"CA at {resolved_endpoint} did not report a fingerprint — aborting (the \
invite expects {pin})"
)
})?;
if !koi_crypto::pinning::fingerprints_match(advertised, pin) {
anyhow::bail!(
"CA fingerprint mismatch — refusing to join.\n invite pinned: {pin}\n \
CA advertised: {advertised}\nThe endpoint may be impersonating the CA \
(MITM), or the invite is for a different mesh."
);
}
eprintln!("Preflight OK — CA fingerprint matches the invite pin.");
}
let csr_resp = local.post_json(
"/v1/certmesh/member-csr",
&serde_json::json!({
"hostname": local_hostname,
"sans": [local_hostname, format!("{local_hostname}.local")],
}),
)?;
let csr = csr_resp
.get("csr")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("local daemon did not return a CSR"))?
.to_string();
let mut body = serde_json::Map::new();
body.insert("hostname".into(), serde_json::json!(local_hostname));
body.insert("csr".into(), serde_json::json!(csr));
if let Some(ref secret) = invite_secret {
body.insert("invite_token".into(), serde_json::json!(secret));
} else {
eprintln!("Enter the TOTP code from your authenticator app:");
let mut code = String::new();
std::io::stdin().read_line(&mut code)?;
body.insert(
"auth".into(),
serde_json::json!({ "method": "totp", "code": code.trim() }),
);
}
let resp = remote.post_json("/v1/certmesh/join", &serde_json::Value::Object(body))?;
let service_cert = resp
.get("service_cert")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("CA response missing service_cert"))?;
let ca_cert = resp
.get("ca_cert")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("CA response missing ca_cert"))?;
let mut install_body = serde_json::Map::new();
install_body.insert("hostname".into(), serde_json::json!(local_hostname));
install_body.insert("cert_pem".into(), serde_json::json!(service_cert));
install_body.insert("ca_pem".into(), serde_json::json!(ca_cert));
install_body.insert("ca_endpoint".into(), serde_json::json!(resolved_endpoint));
let install_fp = pinned_fp
.as_deref()
.or_else(|| resp.get("ca_fingerprint").and_then(|v| v.as_str()));
if let Some(fp) = install_fp {
install_body.insert("ca_fingerprint".into(), serde_json::json!(fp));
}
install_body.insert(
"sans".into(),
serde_json::json!([local_hostname, format!("{local_hostname}.local")]),
);
if let Some(policy) = resp.get("policy") {
install_body.insert("policy".into(), policy.clone());
}
let install = local.post_json(
"/v1/certmesh/member-cert",
&serde_json::Value::Object(install_body),
)?;
let cert_path = install
.get("cert_path")
.and_then(|v| v.as_str())
.unwrap_or("(local certs dir)");
if json {
println!(
"{}",
serde_json::json!({
"enrolled": true,
"hostname": local_hostname,
"cert_path": cert_path,
"ca_fingerprint": resp.get("ca_fingerprint").and_then(|v| v.as_str()),
})
);
} else {
println!("Enrolled as: {local_hostname}");
println!("Key + certificate stored locally: {cert_path}");
}
Ok(())
}
pub fn invite(
hostname: &str,
ttl: i64,
json: bool,
endpoint: Option<&str>,
token: Option<&str>,
) -> anyhow::Result<()> {
let client = require_daemon(endpoint, token)?;
let body = serde_json::json!({
"hostname": hostname,
"ttl_mins": ttl,
});
let resp = client.post_json("/v1/certmesh/invite", &body)?;
if json {
println!("{}", serde_json::to_string_pretty(&resp)?);
return Ok(());
}
let token_str = resp.get("token").and_then(|v| v.as_str()).unwrap_or("");
let expires_at = resp
.get("expires_at")
.and_then(|v| v.as_str())
.unwrap_or("(unknown)");
println!("Invite minted for {hostname} (single-use, expires {expires_at}):");
println!();
println!(" {}", color::green(token_str));
println!();
println!("On {hostname}, run:");
println!(" koi certmesh join <ca-endpoint> --invite {token_str}");
Ok(())
}
pub async fn promote(endpoint: Option<&str>, json: bool) -> anyhow::Result<()> {
let _local = require_daemon(None, None)?;
let resolved_endpoint = match endpoint {
Some(ep) => ep.to_string(),
None => discover_ca(None).await?,
};
eprintln!("Enter the TOTP code from your authenticator app:");
let mut code = String::new();
std::io::stdin().read_line(&mut code)?;
let code = code.trim().to_string();
eprintln!("Enter the CA passphrase (used to encrypt the transferred key):");
let mut passphrase = String::new();
std::io::stdin().read_line(&mut passphrase)?;
let passphrase = passphrase.trim().to_string();
if passphrase.is_empty() {
anyhow::bail!("Passphrase cannot be empty.");
}
let client_kp = koi_crypto::key_agreement::EphemeralKeyPair::generate();
let client_pub = client_kp.public_key_bytes();
let client_pub_hex = koi_common::encoding::hex_encode(&client_pub);
let client = KoiClient::new(&resolved_endpoint);
let body = serde_json::json!({
"auth": { "method": "totp", "code": code },
"ephemeral_public": client_pub_hex,
});
let resp = client.post_json("/v1/certmesh/promote", &body)?;
let promote_response: koi_certmesh::protocol::PromoteResponse =
serde_json::from_value(resp.clone())
.map_err(|e| anyhow::anyhow!("Failed to parse promotion response: {e}"))?;
let (ca_key, auth_state, roster) =
koi_certmesh::failover::accept_promotion(&promote_response, client_kp)?;
#[allow(clippy::disallowed_methods)]
let paths = koi_certmesh::CertmeshPaths::with_data_dir(koi_common::paths::koi_data_dir());
let ca_dir = paths.ca_dir();
std::fs::create_dir_all(&ca_dir)?;
let ca_key_der = koi_crypto::keys::ca_keypair_to_der(&ca_key)?;
let (encrypted_key, slot_table, _master_key) =
koi_crypto::unlock_slots::envelope_encrypt_new(&ca_key_der, &passphrase)?;
koi_crypto::keys::save_encrypted_key(&paths.ca_key_path(), &encrypted_key)?;
slot_table.save(&paths.slot_table_path())?;
std::fs::write(paths.ca_cert_path(), &promote_response.ca_cert_pem)?;
let koi_crypto::auth::AuthState::Totp(secret) = &auth_state;
let stored = koi_crypto::auth::store_totp(secret, &passphrase)?;
let auth_json = serde_json::to_string_pretty(&stored)?;
std::fs::write(paths.auth_path(), auth_json)?;
koi_certmesh::roster::save_roster(&roster, &paths.roster_path())?;
let hostname = hostname::get()
.map(|h| h.to_string_lossy().to_string())
.unwrap_or_else(|_| "localhost".to_string());
let mut roster = koi_certmesh::roster::load_roster(&paths.roster_path())?;
if let Some(member) = roster.find_member_mut(&hostname) {
member.role = koi_certmesh::roster::MemberRole::Standby;
koi_certmesh::roster::save_roster(&roster, &paths.roster_path())?;
}
let _ = koi_certmesh::audit::append_entry_to(
&paths.audit_log_path(),
"promoted_to_standby",
&[("hostname", &hostname)],
);
if json {
println!(
"{}",
serde_json::json!({
"promoted": true,
"role": "standby",
"hostname": hostname,
})
);
} else {
print!("{}", format::promote_success(&hostname));
}
Ok(())
}
pub fn open_enrollment(
json: bool,
endpoint: Option<&str>,
token: Option<&str>,
) -> anyhow::Result<()> {
let client = require_daemon(endpoint, token)?;
let resp = client.post_json("/v1/certmesh/open-enrollment", &serde_json::json!({}))?;
if json {
println!("{}", serde_json::to_string_pretty(&resp)?);
} else {
let state = resp
.get("enrollment_state")
.and_then(|v| v.as_str())
.unwrap_or("open");
println!("Enrollment: {state}");
}
Ok(())
}
pub fn close_enrollment(
json: bool,
endpoint: Option<&str>,
token: Option<&str>,
) -> anyhow::Result<()> {
let client = require_daemon(endpoint, token)?;
let resp = client.post_json("/v1/certmesh/close-enrollment", &serde_json::json!({}))?;
if json {
println!("{}", serde_json::to_string_pretty(&resp)?);
} else {
println!("Enrollment: closed");
}
Ok(())
}
pub fn rotate_auth(json: bool, endpoint: Option<&str>, token: Option<&str>) -> anyhow::Result<()> {
let client = require_daemon(endpoint, token)?;
eprintln!("Enter the CA passphrase:");
let mut passphrase = String::new();
std::io::stdin().read_line(&mut passphrase)?;
let passphrase = passphrase.trim();
if passphrase.is_empty() {
anyhow::bail!("Passphrase cannot be empty.");
}
let body = serde_json::json!({ "passphrase": passphrase });
let resp = client.post_json("/v1/certmesh/rotate-auth", &body)?;
let totp_uri = resp
.get("auth_setup")
.and_then(|s| s.get("totp_uri"))
.and_then(|v| v.as_str())
.unwrap_or("");
if json {
println!("{}", serde_json::json!({ "rotated": true }));
} else {
println!("Auth credential rotated successfully.");
if !totp_uri.is_empty() {
if let Some(secret) = extract_totp_secret_from_uri(totp_uri) {
let hostname = hostname::get()
.map(|h| h.to_string_lossy().to_string())
.unwrap_or_else(|_| "localhost".to_string());
let qr = koi_crypto::totp::qr_code_unicode(
&secret,
"Koi Certmesh",
&format!("admin@{hostname}"),
);
println!("\nScan this QR code with your authenticator app:\n");
println!("{qr}");
}
}
}
Ok(())
}
pub fn backup(
path: &std::path::Path,
json: bool,
endpoint: Option<&str>,
token: Option<&str>,
) -> anyhow::Result<()> {
let client = require_daemon(endpoint, token)?;
let ca_passphrase = read_non_empty_line("Enter the CA passphrase:")?;
let backup_passphrase = read_non_empty_line("Enter a backup passphrase:")?;
confirm_passphrase("Confirm the backup passphrase:", &backup_passphrase)?;
let body = serde_json::json!({
"ca_passphrase": ca_passphrase,
"backup_passphrase": backup_passphrase,
});
let resp = client.post_json("/v1/certmesh/backup", &body)?;
let backup_hex = resp
.get("backup_hex")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("backup response missing backup_hex"))?;
let bytes = hex_decode(backup_hex).map_err(|e| anyhow::anyhow!("invalid backup hex: {e}"))?;
std::fs::write(path, bytes)?;
if json {
println!(
"{}",
serde_json::json!({
"backup_saved": true,
"path": path.display().to_string(),
})
);
} else {
println!("Backup saved to {}", path.display());
}
Ok(())
}
pub fn restore(
path: &std::path::Path,
json: bool,
endpoint: Option<&str>,
token: Option<&str>,
) -> anyhow::Result<()> {
let client = require_daemon(endpoint, token)?;
let backup_bytes = std::fs::read(path)?;
let backup_hex = hex_encode(&backup_bytes);
let backup_passphrase = read_non_empty_line("Enter the backup passphrase:")?;
let new_passphrase = read_non_empty_line("Enter a new CA passphrase:")?;
confirm_passphrase("Confirm the new CA passphrase:", &new_passphrase)?;
let body = serde_json::json!({
"backup_hex": backup_hex,
"backup_passphrase": backup_passphrase,
"new_passphrase": new_passphrase,
});
let resp = client.post_json("/v1/certmesh/restore", &body)?;
let restored = resp
.get("restored")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if json {
println!("{}", serde_json::json!({ "restored": restored }));
} else if restored {
println!("Backup restored successfully.");
} else {
println!("Backup restore failed.");
}
Ok(())
}
pub fn revoke(
hostname: &str,
reason: Option<&str>,
json: bool,
endpoint: Option<&str>,
token: Option<&str>,
) -> anyhow::Result<()> {
let client = require_daemon(endpoint, token)?;
let body = serde_json::json!({
"hostname": hostname,
"reason": reason,
});
let resp = client.post_json("/v1/certmesh/revoke", &body)?;
let revoked = resp
.get("revoked")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if json {
println!(
"{}",
serde_json::json!({ "hostname": hostname, "revoked": revoked })
);
} else if revoked {
println!("Member revoked: {hostname}");
} else {
println!("Member could not be revoked: {hostname}");
}
Ok(())
}
pub fn destroy(
json: bool,
yes: bool,
endpoint: Option<&str>,
token: Option<&str>,
) -> anyhow::Result<()> {
let meta = crate::help::get("certmesh destroy")
.ok_or_else(|| anyhow::anyhow!("internal: missing meta for 'certmesh destroy'"))?;
crate::help::confirm::gate_meta(meta, json, yes)?;
let client = require_daemon(endpoint, token)?;
let resp = client.post_json("/v1/certmesh/destroy", &serde_json::json!({}))?;
let destroyed = resp
.get("destroyed")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if json {
println!("{}", serde_json::json!({ "destroyed": destroyed }));
} else if destroyed {
println!("Certificate mesh destroyed. All CA data, certificates, and audit logs have been removed.");
} else {
println!("Certificate mesh could not be destroyed.");
}
Ok(())
}
fn read_non_empty_line(prompt: &str) -> anyhow::Result<String> {
eprintln!("{prompt}");
let mut line = String::new();
std::io::stdin().read_line(&mut line)?;
let trimmed = line.trim().to_string();
if trimmed.is_empty() {
anyhow::bail!("Input cannot be empty.");
}
Ok(trimmed)
}
fn confirm_passphrase(prompt: &str, expected: &str) -> anyhow::Result<()> {
let confirm = read_non_empty_line(prompt)?;
if confirm != expected {
anyhow::bail!("Passphrases do not match.");
}
Ok(())
}
async fn discover_ca(pinned_fp: Option<&str>) -> anyhow::Result<String> {
eprintln!("Searching for certmesh CA on the local network...");
let core = Arc::new(koi_mdns::MdnsCore::new()?);
let handle = core
.subscribe_type(koi_certmesh::CERTMESH_SERVICE_TYPE)
.await?;
let deadline = tokio::time::Instant::now() + CA_DISCOVERY_TIMEOUT;
let mut found: Vec<(String, String, Option<String>)> = Vec::new();
loop {
tokio::select! {
event = handle.recv() => {
match event {
Some(MdnsEvent::Resolved(record)) => {
if let (Some(ip), Some(port)) = (&record.ip, record.port) {
let endpoint = format!("http://{ip}:{port}");
if !found.iter().any(|(ep, _, _)| ep == &endpoint) {
let fp = record.txt.get("fp").cloned();
found.push((endpoint, record.name.clone(), fp));
}
}
}
Some(_) => continue,
None => break,
}
}
_ = tokio::time::sleep_until(deadline) => break,
}
}
let _ = core.shutdown().await;
if let Some(pin) = pinned_fp {
let before = found.len();
found.retain(|(_, _, fp)| match fp {
Some(f) => koi_crypto::pinning::fingerprints_match(f, pin),
None => true,
});
let dropped = before - found.len();
if dropped > 0 {
eprintln!(
"Ignored {dropped} discovered CA(s) whose advertised fingerprint did not match \
the invite."
);
}
}
match found.len() {
0 => {
let hint = if pinned_fp.is_some() {
" matching the invite"
} else {
""
};
anyhow::bail!(
"No certmesh CA{hint} found on the local network.\n\
Specify the endpoint manually: koi certmesh join <endpoint>"
)
}
1 => {
let (endpoint, name, _) = found.into_iter().next().unwrap();
eprintln!("Found CA: {name} at {endpoint}");
Ok(endpoint)
}
_ => {
let mut msg = String::from("Multiple certmesh CAs found:\n");
for (ep, name, _) in &found {
msg.push_str(&format!(" {name} {ep}\n"));
}
msg.push_str("\nSpecify which to join: koi certmesh join <endpoint>");
anyhow::bail!(msg)
}
}
}
const ACME_PORT: u16 = 5643;
fn acme_directory_url(endpoint: &str) -> String {
let host = endpoint
.trim_start_matches("http://")
.trim_start_matches("https://")
.split(['/', ':'])
.next()
.filter(|h| !h.is_empty())
.unwrap_or("localhost");
format!("https://{host}:{ACME_PORT}/acme/directory")
}
fn ca_cert_path_hint() -> String {
#[allow(clippy::disallowed_methods)]
let data_dir = koi_common::paths::koi_data_dir();
data_dir
.join("certmesh")
.join("ca")
.join("ca-cert.pem")
.display()
.to_string()
}
pub fn acme_enable(json: bool, endpoint: Option<&str>, token: Option<&str>) -> anyhow::Result<()> {
let client = require_daemon(endpoint, token)?;
let resp = client.get_json("/v1/certmesh/status")?;
let ca_init = resp
.get("ca_initialized")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let ca_locked = resp
.get("ca_locked")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let fingerprint = resp
.get("ca_fingerprint")
.and_then(|v| v.as_str())
.unwrap_or("(unknown)");
let dir_url = endpoint
.map(acme_directory_url)
.unwrap_or_else(|| format!("https://localhost:{ACME_PORT}/acme/directory"));
let ca_path = ca_cert_path_hint();
if json {
let out = serde_json::json!({
"acme": {
"directory": dir_url,
"ca_initialized": ca_init,
"ca_locked": ca_locked,
"ca_fingerprint": fingerprint,
"ca_cert_path": ca_path,
"enabled": ca_init && !ca_locked,
}
});
println!("{}", serde_json::to_string_pretty(&out)?);
return Ok(());
}
if !ca_init {
println!("ACME server: unavailable — no CA yet.");
println!(" Run `koi certmesh create` to initialize the CA first.");
return Ok(());
}
if ca_locked {
println!("ACME server: waiting — the CA is locked.");
println!(" Run `koi certmesh unlock`, then restart the daemon.");
return Ok(());
}
println!("ACME (RFC 8555) server is active.");
println!();
println!(" Directory URL : {dir_url}");
println!(" CA root cert : {ca_path}");
println!(" CA fingerprint: {fingerprint}");
println!();
println!("Bootstrap (one time): clients must trust the CA root, then point their");
println!("ACME client at the directory above. dns-01 is the only challenge type;");
println!("only names inside the Koi DNS zone are issuable.");
println!();
println!(" Caddy : tls {{ issuer acme {{ dir {dir_url} }} }}");
println!(" (and trust {ca_path} via acme_ca_root / a trusted root)");
println!(" Traefik : certificatesResolvers.koi.acme.caServer={dir_url}");
println!(" certificatesResolvers.koi.acme.caCertificates={ca_path}");
println!(" lego : LEGO_CA_CERTIFICATES={ca_path} lego --server {dir_url} ...");
println!();
println!("See `docs/guides/acme.md` for full recipes.");
Ok(())
}
pub fn acme_status(json: bool, endpoint: Option<&str>, token: Option<&str>) -> anyhow::Result<()> {
let client = require_daemon(endpoint, token)?;
let resp = client.get_json("/v1/certmesh/status")?;
let ca_init = resp
.get("ca_initialized")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let ca_locked = resp
.get("ca_locked")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let enrollment_open = resp
.get("enrollment_open")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let dir_url = endpoint
.map(acme_directory_url)
.unwrap_or_else(|| format!("https://localhost:{ACME_PORT}/acme/directory"));
let acme_members = resp
.get("members")
.and_then(|v| v.as_array())
.map(|arr| arr.len())
.unwrap_or(0);
let serving = ca_init && !ca_locked;
if json {
let out = serde_json::json!({
"acme": {
"serving": serving,
"directory": dir_url,
"mode": if enrollment_open { "open" } else { "closed (EAB required)" },
"member_count": acme_members,
}
});
println!("{}", serde_json::to_string_pretty(&out)?);
return Ok(());
}
println!(
"ACME server: {}",
if serving { "serving" } else { "not serving" }
);
println!(" Directory : {dir_url}");
println!(
" Mode : {}",
if enrollment_open {
"open (free newAccount)"
} else {
"closed (external account binding required)"
}
);
if !serving {
if !ca_init {
println!(" Reason : no CA — run `koi certmesh create`");
} else if ca_locked {
println!(" Reason : CA locked — run `koi certmesh unlock`");
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn preset_labels_match_names() {
assert_eq!(preset_label("just_me"), "Just Me");
assert_eq!(preset_label("team"), "My Team");
assert_eq!(preset_label("org"), "My Organization");
assert_eq!(preset_label("unknown"), "Just Me");
}
#[test]
fn hex_encode_produces_correct_output() {
assert_eq!(hex_encode(&[0x0a, 0xff, 0x00]), "0aff00");
assert_eq!(hex_encode(&[]), "");
}
#[test]
fn base32_decode_valid() {
let decoded = base32_decode("JBSWY3DPEE").unwrap();
assert_eq!(&decoded, b"Hello!");
}
#[test]
fn base32_decode_with_padding() {
let decoded = base32_decode("JBSWY3DPEE======").unwrap();
assert_eq!(&decoded, b"Hello!");
}
#[test]
fn base32_decode_invalid_char() {
let decoded = base32_decode("1!!!invalid");
assert!(decoded.is_none());
}
#[test]
fn extract_totp_secret_from_valid_uri() {
let secret = koi_crypto::totp::generate_secret();
let uri = koi_crypto::totp::build_totp_uri(&secret, "Test", "user");
let extracted = extract_totp_secret_from_uri(&uri);
assert!(extracted.is_some());
assert_eq!(extracted.unwrap().as_bytes(), secret.as_bytes());
}
#[test]
fn extract_totp_secret_from_bad_uri() {
assert!(extract_totp_secret_from_uri("not-a-uri").is_none());
assert!(extract_totp_secret_from_uri("otpauth://totp/x?issuer=x").is_none());
}
#[test]
fn require_daemon_fails_without_endpoint() {
let result = require_daemon(None, None);
let _ = result;
}
#[test]
fn require_daemon_explicit_endpoint_does_not_require_breadcrumb() {
let client = require_daemon(Some("http://10.0.0.1:5641"), None);
assert!(client.is_ok());
let client = require_daemon(Some("http://10.0.0.1:5641"), Some("remote-token"));
assert!(client.is_ok());
}
}