Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions cmux-tui/crates/acpmux/spec/acpmux-protocol.json
Original file line number Diff line number Diff line change
Expand Up @@ -870,6 +870,18 @@
"params": true,
"result": true
},
{
"kind": "request",
"name": "_acpmux/mac_approver_set",
"params": true,
"result": true
},
{
"kind": "request",
"name": "_acpmux/mac_approvers",
"params": true,
"result": true
},
{
"kind": "request",
"name": "_acpmux/new_chat_picks_set",
Expand Down
2 changes: 2 additions & 0 deletions cmux-tui/crates/acpmux/src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,8 @@ pub async fn run(opts: DaemonOptions) -> Result<()> {
hub.watches.open(&home().join("watches.json"));
}
crate::hub::watches::Watches::start(&hub);
// The Macs whose app may allow here over the peer link (`hub/mac_approvers.rs`).
hub.person.macs.open(home().join(crate::hub::mac_approvers::FILE));
if let Some(key) = person_key {
// A Team-signed daemon takes the key only from the signed app's
// `_acpmux/person_enroll`: a same-uid process could restart it with
Expand Down
77 changes: 43 additions & 34 deletions cmux-tui/crates/acpmux/src/hub/approvers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,37 +56,7 @@ impl ApproverSet {
.get("approvers")
.and_then(Value::as_array)
.ok_or_else(|| RpcError::invalid_params("approvers must be a list"))?;
if list.len() > MAX_APPROVERS {
return Err(RpcError::invalid_params(format!("at most {MAX_APPROVERS} approvers")));
}
let mut approvers = Vec::with_capacity(list.len());
for entry in list {
let id = entry.get("id").and_then(Value::as_str).filter(|i| valid_id(i)).ok_or_else(
|| RpcError::invalid_params("approver id must be 1 to 128 of [A-Za-z0-9_.:-]"),
)?;
let public_key = entry
.get("publicKey")
.and_then(Value::as_str)
.and_then(unhex)
.filter(|k| k.len() == 65 && k[0] == 4)
.ok_or_else(|| {
RpcError::invalid_params(
"publicKey must be a 65-byte uncompressed P-256 point as hex",
)
})?;
let name: String = entry
.get("name")
.and_then(Value::as_str)
.unwrap_or_default()
.chars()
.filter(|c| !c.is_control())
.take(64)
.collect();
if approvers.iter().any(|a: &Approver| a.id == id) {
return Err(RpcError::invalid_params(format!("approver {id} is listed twice")));
}
approvers.push(Approver { id: id.to_owned(), public_key, name });
}
let approvers = parse_approvers(list)?;
let count = approvers.len();
*self.inner.lock().unwrap_or_else(std::sync::PoisonError::into_inner) =
(mac.to_owned(), approvers);
Expand Down Expand Up @@ -182,6 +152,45 @@ impl ApproverSet {
}
}

/// The approvers of a list `[{id, publicKey, name}]`, checked whole: at most
/// [`MAX_APPROVERS`], each id once, each key a 65-byte uncompressed P-256 point as hex.
/// The enrolled Macs' file is read with it too (`mac_approvers.rs`).
pub(super) fn parse_approvers(list: &[Value]) -> Result<Vec<Approver>, RpcError> {
if list.len() > MAX_APPROVERS {
return Err(RpcError::invalid_params(format!("at most {MAX_APPROVERS} approvers")));
}
let mut approvers = Vec::with_capacity(list.len());
for entry in list {
let id =
entry.get("id").and_then(Value::as_str).filter(|i| valid_id(i)).ok_or_else(|| {
RpcError::invalid_params("approver id must be 1 to 128 of [A-Za-z0-9_.:-]")
})?;
let public_key = entry
.get("publicKey")
.and_then(Value::as_str)
.and_then(unhex)
.filter(|k| k.len() == 65 && k[0] == 4)
.ok_or_else(|| {
RpcError::invalid_params(
"publicKey must be a 65-byte uncompressed P-256 point as hex",
)
})?;
let name: String = entry
.get("name")
.and_then(Value::as_str)
.unwrap_or_default()
.chars()
.filter(|c| !c.is_control())
.take(64)
.collect();
if approvers.iter().any(|a: &Approver| a.id == id) {
return Err(RpcError::invalid_params(format!("approver {id} is listed twice")));
}
approvers.push(Approver { id: id.to_owned(), public_key, name });
}
Ok(approvers)
}

/// What an approver signs.
pub struct Parts<'a> {
pub instance: &'a str,
Expand Down Expand Up @@ -290,16 +299,16 @@ pub fn fingerprint(key: &[u8]) -> String {
hex(&sha2::Sha256::digest(key)[..8])
}

fn valid_id(s: &str) -> bool {
pub(super) fn valid_id(s: &str) -> bool {
(1..=128).contains(&s.len())
&& s.bytes().all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'.' | b':' | b'-'))
}

fn hex(bytes: &[u8]) -> String {
pub(super) fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}

fn unhex(s: &str) -> Option<Vec<u8>> {
pub(super) fn unhex(s: &str) -> Option<Vec<u8>> {
if !s.len().is_multiple_of(2)
|| !s.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
{
Expand Down
Loading
Loading