use anyhow::Context;
use mcpmesh_local_api::RosterInstallResult;
use mcpmesh_trust::{DeviceKey, paths};
use crate::{client, config, pairing, roster, util};
pub fn with_daemon<T>(
f: impl AsyncFnOnce(client::ControlClient) -> anyhow::Result<T>,
) -> anyhow::Result<T> {
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
rt.block_on(async move {
let client = client::ensure_daemon().await?;
f(client).await
})
}
const DEFAULT_EXPIRES_SECS: i64 = 90 * 86_400;
fn slug(name: &str) -> String {
let mut s = String::new();
let mut last_dash = true; for c in name.chars() {
if c.is_ascii_alphanumeric() {
s.push(c.to_ascii_lowercase());
last_dash = false;
} else if !last_dash {
s.push('-');
last_dash = true;
}
}
while s.ends_with('-') {
s.pop();
}
if s.is_empty() { "user".to_string() } else { s }
}
pub fn run_join(
org_invite: String,
name: Option<String>,
user_id: Option<String>,
label: String,
json: bool,
) -> anyhow::Result<()> {
use mcpmesh_trust::keys::UserKey;
use mcpmesh_trust::roster::encode_b64u;
use mcpmesh_trust::roster::sign::sign_device_binding;
let invite = roster::enroll::OrgInviteCode::decode(&org_invite)?;
let root_pk = mcpmesh_trust::roster::decode_endpoint_id(&invite.org_root_pk)
.context("org invite carries an invalid org_root_pk")?;
let display_name = name.unwrap_or_else(|| "user".to_string());
let requested_user_id = user_id.unwrap_or_else(|| slug(&display_name));
let user_key_path = paths::default_user_key_path()?;
let (user_key, _created) = UserKey::load_or_generate(&user_key_path)
.map_err(|e| anyhow::anyhow!("user key error at {}: {e}", user_key_path.display()))?;
let device_key = load_device_key()?;
let device_id = device_key.public_bytes();
let binding = sign_device_binding(user_key.signing_key(), &device_id);
let code_fp = pairing::sas::join_code_fingerprint(&user_key.public_bytes(), &device_id);
let join = roster::enroll::JoinCode {
display_name: display_name.clone(),
requested_user_id: requested_user_id.clone(),
user_pk: encode_b64u(&user_key.public_bytes()),
device_endpoint_id: encode_b64u(&device_id),
device_label: label,
binding_sig: encode_b64u(&binding),
}
.encode();
with_daemon(async |mut client| {
client
.org_join(
&invite.org_id,
&invite.org_root_pk,
&requested_user_id,
&user_key_path.to_string_lossy(),
)
.await?;
if let Some(url) = &invite.roster_url {
client.set_roster_url(url).await?;
}
Ok(())
})?;
let fingerprint = pairing::sas::fingerprint_words(&root_pk);
if json {
println!(
"{}",
serde_json::json!({
"org_id": invite.org_id,
"user_id": requested_user_id,
"join_code": join,
"join_code_fingerprint": code_fp,
"org_root_fingerprint": fingerprint,
})
);
return Ok(());
}
println!("Joined org '{}' as '{requested_user_id}'.", invite.org_id);
println!("Org root fingerprint: {fingerprint}");
println!(
" → Confirm this matches what the operator reads back, out-of-band, before they approve you."
);
println!("Send the operator your join code: {join}");
println!("Join code fingerprint: {code_fp}");
println!(
" → Read this back to your operator out-of-band so they confirm they received YOUR join code (not a substituted one)."
);
Ok(())
}
pub fn run_org_create(
name: String,
expires: Option<String>,
roster_url: Option<String>,
json: bool,
) -> anyhow::Result<()> {
use mcpmesh_trust::keys::OrgRootKey;
use mcpmesh_trust::roster::sign::mint_signed;
use mcpmesh_trust::roster::{encode_b64u, mutate};
let key_path = paths::default_org_root_key_path()?;
let (root, created) = OrgRootKey::load_or_generate(&key_path)
.map_err(|e| anyhow::anyhow!("org root key error at {}: {e}", key_path.display()))?;
if !created {
anyhow::bail!(
"this node already holds an org root key ({}); `org create` is one-time per node",
key_path.display()
);
}
let expires_secs = match &expires {
Some(s) => config::parse_duration(s).map_err(|e| anyhow::anyhow!("bad --expires: {e}"))?,
None => DEFAULT_EXPIRES_SECS,
};
let now = util::epoch_now_i64();
let roster = mint_signed(
root.signing_key(),
mutate::empty_roster(&name, 1, now, now.saturating_add(expires_secs)),
);
let org_root_pk = encode_b64u(&root.public_bytes());
let result = install_signed_roster(&roster, Some(org_root_pk.clone()))?;
if let Some(url) = &roster_url {
with_daemon(async |mut client| {
client.set_roster_url(url).await?;
Ok(())
})?;
}
let invite = roster::enroll::OrgInviteCode {
org_id: name.clone(),
org_root_pk,
roster_url: roster_url.clone(),
}
.encode();
let fingerprint = pairing::sas::fingerprint_words(&root.public_bytes());
if json {
println!(
"{}",
serde_json::json!({
"org_id": result.org_id,
"serial": result.serial,
"org_invite": invite,
"org_root_fingerprint": fingerprint,
})
);
return Ok(());
}
println!(
"Created org '{}' (roster serial {}).",
result.org_id, result.serial
);
println!("Invite someone: {invite}");
println!("Org root fingerprint: {fingerprint} (read this aloud when you approve joiners)");
Ok(())
}
fn load_operator_roster() -> anyhow::Result<(
mcpmesh_trust::keys::OrgRootKey,
mcpmesh_trust::roster::Roster,
)> {
let key_path = paths::default_org_root_key_path()?;
if !key_path.exists() {
anyhow::bail!(
"this node is not an org operator (no org root key); run `mcpmesh org create` first"
);
}
let (root, _) = mcpmesh_trust::keys::OrgRootKey::load_or_generate(&key_path)
.map_err(|e| anyhow::anyhow!("org root key error at {}: {e}", key_path.display()))?;
let roster_path = paths::default_roster_path()?;
let bytes = std::fs::read(&roster_path).with_context(|| {
format!(
"no installed roster at {} — run `org create`",
roster_path.display()
)
})?;
let roster: mcpmesh_trust::roster::Roster =
serde_json::from_slice(&bytes).context("parse installed roster")?;
Ok((root, roster))
}
pub fn run_org_approve(
join_code: String,
groups: String,
user_id: Option<String>,
json: bool,
) -> anyhow::Result<()> {
use mcpmesh_trust::roster::sign::{sign, verify_device_binding};
use mcpmesh_trust::roster::{decode_endpoint_id, mutate};
let jc = roster::enroll::JoinCode::decode(&join_code)?;
let user_pk = decode_endpoint_id(&jc.user_pk).context("join code has an invalid user_pk")?;
let device_id = decode_endpoint_id(&jc.device_endpoint_id)
.context("join code has an invalid device endpoint")?;
let sig = mcpmesh_trust::roster::decode_b64u(&jc.binding_sig)
.context("join code has an invalid signature")?;
verify_device_binding(&user_pk, &device_id, &sig).map_err(|_| {
anyhow::anyhow!("join code device binding failed — the code is forged or corrupt")
})?;
let (root, mut roster) = load_operator_roster()?;
let uid = user_id.unwrap_or(jc.requested_user_id);
let groups = split_csv(Some(groups));
let code_fp = pairing::sas::join_code_fingerprint(&user_pk, &device_id);
if !json {
println!(
"Approving join code {code_fp} for '{}' as user '{uid}', groups [{}].",
jc.display_name,
groups.join(", ")
);
println!(
" → Verify {code_fp} matches what the joiner read back to you out-of-band; if it doesn't, \
run `org revoke` on this device."
);
}
roster.serial += 1;
mutate::upsert_member(
&mut roster,
&uid,
&jc.display_name,
&jc.user_pk, &groups,
&jc.device_endpoint_id, &jc.device_label,
)
.map_err(|e| anyhow::anyhow!("roster mutation rejected: {e}"))?;
sign(root.signing_key(), &mut roster).map_err(|e| anyhow::anyhow!("sign roster: {e}"))?;
let result = install_signed_roster(&roster, None)?; if json {
println!(
"{}",
serde_json::json!({
"user_id": uid,
"groups": groups,
"org_id": result.org_id,
"serial": result.serial,
"join_code_fingerprint": code_fp,
})
);
return Ok(());
}
println!(
"Approved '{}' into [{}] (org '{}', serial {}).",
uid,
groups.join(", "),
result.org_id,
result.serial
);
Ok(())
}
pub fn run_org_revoke(target: String, user_key: bool, json: bool) -> anyhow::Result<()> {
use mcpmesh_trust::roster::mutate;
use mcpmesh_trust::roster::sign::sign;
let (root, mut roster) = load_operator_roster()?;
roster.serial += 1;
let mode: &str;
let action: String = if user_key {
mutate::remove_user(&mut roster, &target, false).map_err(|e| anyhow::anyhow!("{e}"))?;
mode = "user-key-rotation";
format!(
"Rotated '{target}': removed from the roster. They re-enroll with a fresh user key \
(same device), then re-approve with the same user_id"
)
} else if let Some((person, device)) = target.split_once('/') {
mutate::revoke_device(&mut roster, person, device).map_err(|e| anyhow::anyhow!("{e}"))?;
mode = "device";
format!("Revoked device '{person}/{device}'")
} else {
mutate::remove_user(&mut roster, &target, true).map_err(|e| anyhow::anyhow!("{e}"))?;
mode = "person";
format!("Revoked person '{target}' (all devices)")
};
sign(root.signing_key(), &mut roster).map_err(|e| anyhow::anyhow!("sign roster: {e}"))?;
let result = install_signed_roster(&roster, None)?;
if json {
println!(
"{}",
serde_json::json!({
"target": target,
"mode": mode,
"org_id": result.org_id,
"serial": result.serial,
"severed": result.severed,
})
);
return Ok(());
}
println!(
"{action} (org '{}', serial {}). Severed {} live session{}.",
result.org_id,
result.serial,
result.severed,
if result.severed == 1 { "" } else { "s" }
);
Ok(())
}
pub fn run_devices_code(label: String, json: bool) -> anyhow::Result<()> {
use mcpmesh_trust::roster::encode_b64u;
let device_id = load_device_key()?.public_bytes();
let code = roster::enroll::DeviceCode {
device_endpoint_id: encode_b64u(&device_id),
device_label: label,
}
.encode();
if json {
println!("{}", serde_json::json!({"device_code": code}));
return Ok(());
}
println!("Give this to an already-enrolled device (`mcpmesh devices add`): {code}");
Ok(())
}
pub fn run_devices_add(device_code: String, json: bool) -> anyhow::Result<()> {
use mcpmesh_trust::keys::UserKey;
use mcpmesh_trust::roster::encode_b64u;
use mcpmesh_trust::roster::sign::sign_device_binding;
let dc = roster::enroll::DeviceCode::decode(&device_code)?;
let new_device_id = mcpmesh_trust::roster::decode_endpoint_id(&dc.device_endpoint_id)
.context("device code has an invalid endpoint id")?;
let cfg = config::Config::load(&paths::default_config_path()?)
.map_err(|e| anyhow::anyhow!("config: {e}"))?;
let user_id = cfg
.identity
.user_id
.clone()
.context("this device is not enrolled (no user_id); run `mcpmesh join` first")?;
let user_key_path = match cfg.identity.user_key.clone() {
Some(p) => p,
None => paths::default_user_key_path()?,
};
if !user_key_path.exists() {
anyhow::bail!(
"this device is not enrolled (no user key at {}); run `mcpmesh join` first",
user_key_path.display()
);
}
let (user_key, _) = UserKey::load_or_generate(&user_key_path)
.map_err(|e| anyhow::anyhow!("user key error at {}: {e}", user_key_path.display()))?;
let user_pk = user_key.public_bytes();
let binding = sign_device_binding(user_key.signing_key(), &new_device_id);
let join = roster::enroll::JoinCode {
display_name: user_id.clone(),
requested_user_id: user_id,
user_pk: encode_b64u(&user_pk),
device_endpoint_id: dc.device_endpoint_id,
device_label: dc.device_label,
binding_sig: encode_b64u(&binding),
}
.encode();
let code_fp = pairing::sas::join_code_fingerprint(&user_pk, &new_device_id);
if json {
println!(
"{}",
serde_json::json!({"join_code": join, "join_code_fingerprint": code_fp})
);
return Ok(());
}
println!("Send the operator this join code to add the device: {join}");
println!("Join code fingerprint: {code_fp}");
println!(
" → Read this back to your operator out-of-band so they confirm they received THIS device's \
join code (not a substituted one)."
);
Ok(())
}
fn install_signed_roster(
roster: &mcpmesh_trust::roster::Roster,
org_root_pk: Option<String>,
) -> anyhow::Result<RosterInstallResult> {
use std::sync::atomic::{AtomicU64, Ordering};
static SEQ: AtomicU64 = AtomicU64::new(0);
let seq = SEQ.fetch_add(1, Ordering::Relaxed);
let temp = paths::config_dir()?.join(format!(
"roster.staging.{}.{}.json",
std::process::id(),
seq
));
let _guard = util::TempPathGuard::new(temp.clone());
if let Some(parent) = temp.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&temp, serde_json::to_vec(roster)?)
.with_context(|| format!("write staged roster {}", temp.display()))?;
let path = temp.to_string_lossy().into_owned();
with_daemon(async move |mut client| Ok(client.roster_install(&path, org_root_pk).await?))
}
pub fn split_csv(value: Option<String>) -> Vec<String> {
value
.map(|s| {
s.split(',')
.map(str::trim)
.filter(|x| !x.is_empty())
.map(String::from)
.collect()
})
.unwrap_or_default()
}
pub fn load_device_key() -> anyhow::Result<DeviceKey> {
let cfg_path = paths::default_config_path()?;
let cfg = config::Config::load(&cfg_path)
.map_err(|e| anyhow::anyhow!("config error in {}: {e}", cfg_path.display()))?;
let key_path = match cfg.identity.device_key.clone() {
Some(p) => p,
None => paths::default_device_key_path()?,
};
let (key, _created) = DeviceKey::load_or_generate(&key_path)
.map_err(|e| anyhow::anyhow!("device key error at {}: {e}", key_path.display()))?;
Ok(key)
}
#[cfg(test)]
mod tests {
use mcpmesh_trust::roster::encode_b64u;
use super::*;
#[test]
fn slug_lowercases_collapses_and_trims() {
assert_eq!(slug("Alice Nguyen"), "alice-nguyen");
assert_eq!(slug(" --Bob!! Q. "), "bob-q");
assert_eq!(slug(""), "user");
assert_eq!(slug("---"), "user");
}
#[test]
fn a_forged_join_code_binding_is_rejected_before_any_roster_access() {
let mallory = mcpmesh_trust::ed25519_dalek::SigningKey::from_bytes(&[7u8; 32]);
let alice_pk = mcpmesh_trust::ed25519_dalek::SigningKey::from_bytes(&[9u8; 32])
.verifying_key()
.to_bytes();
let device_id = [42u8; 32];
let sig = mcpmesh_trust::roster::sign::sign_device_binding(&mallory, &device_id);
let code = roster::enroll::JoinCode {
display_name: "Alice".into(),
requested_user_id: "alice".into(),
user_pk: encode_b64u(&alice_pk),
device_endpoint_id: encode_b64u(&device_id),
device_label: "laptop".into(),
binding_sig: encode_b64u(&sig),
}
.encode();
let err = run_org_approve(code, "team-eng".into(), None, false).unwrap_err();
assert!(
err.to_string().contains("device binding failed"),
"the forged binding must be the failure, not roster/operator state: {err}"
);
}
#[test]
fn a_garbage_device_code_fails_on_decode_not_enrollment_state() {
let err = run_devices_add("garbage".into(), false).unwrap_err();
assert!(
err.to_string().contains("mcpmesh-device:"),
"the decode error names the expected scheme: {err}"
);
}
}