Authorize remote modules with bearer proofs

This commit is contained in:
Eric Wendland 2026-05-19 19:16:30 +02:00
commit 7e39a19c18
8 changed files with 572 additions and 120 deletions

View file

@ -179,8 +179,10 @@ Roadmap items should be actionable and checkable:
Bearer access metadata can be created/listed/revoked as resource-scoped auth
ops and must not allow trust graph mutation capabilities. Bearer
challenge/proof/verify commands exist for resource-scoped possession checks.
Payload encryption, key envelopes, and wiring bearer proofs into remote module
authorization are still roadmap work.
Remote module authorization paths can accept optional bearer proofs without
enrolling the caller as a trusted node. Payload encryption, key envelopes, and
separating public bearer ids from private bearer tokens are still roadmap
work.
- SSH revocations can be exported as JSONL, OpenSSH KRL specification text, or
binary OpenSSH KRL files generated through `ssh-keygen`. JSONL and OpenSSH KRL
specification imports are supported; binary KRL import is unsupported because

View file

@ -105,7 +105,8 @@ The bootstrap implementation provides:
- `geth auth grant <subject> <resource> <capability> [--grant-id <id>]`
- `geth auth revoke <resource> <grant-id>`
- local filesystem CAS commands: `add`, `get`, `fetch`, `hash`, `has`, `pin`,
`unpin`, `cleanup`, `providers`, `list`
`unpin`, `cleanup`, `providers`, `list`; remote fetch accepts
`--bearer-secret <secret>`
- local CAS tree objects describe file trees and are stored as CAS blobs
- local file-root commands: `geth cas root add/list/scan`
- local file conflict metadata commands:
@ -117,9 +118,11 @@ The bootstrap implementation provides:
`geth db sync <node-id> <name>`
- local SQLite-backed KV commands: `geth kv create/set/get`; `kv set` accepts
`--subject <principal>` to exercise local capability checks for non-local
callers; `geth kv sync <node-id> <name>` pulls authorized remote updates
callers; `geth kv sync <node-id> <name> [--bearer-secret <secret>]` pulls
authorized remote updates
- local JSON document commands: `geth document create/status/set/get`; `geth
document sync <node-id> <name>` pulls authorized remote JSON state
document sync <node-id> <name> [--bearer-secret <secret>]` pulls authorized
remote JSON state
- local daemon-lifetime pubsub snapshots: `geth pubsub pub/sub`; `geth pubsub
pub <topic> <message> --node <node-id>` publishes to an authorized peer;
`geth pubsub sub <topic> --node <node-id>` reads an authorized peer snapshot
@ -129,15 +132,15 @@ The bootstrap implementation provides:
- `geth ssh cert approve <request-id> --ca-key <path> [--sign] [--subject <principal>]`
- `geth ssh cert import <request-id> --cert <path> [--subject <principal>]`
- `geth ssh cert list [--subject <principal>]`
- `geth ssh cert sync <node-id>`
- `geth ssh cert sync <node-id> [--bearer-secret <secret>]`
- `geth ssh revocation add <kind> <target> [--subject <principal>]`
- `geth ssh revocation list [--subject <principal>]`
- `geth ssh revocation export --out <path> [--format jsonl|openssh-krl-spec|openssh-krl] [--subject <principal>]`
- `geth ssh revocation import <path> [--format jsonl|openssh-krl-spec] [--subject <principal>]`
- `geth ssh revocation sync <node-id>`
- SSH proxy authorization probe: `geth ssh proxy <node-id>`
- `geth ssh revocation sync <node-id> [--bearer-secret <secret>]`
- SSH proxy authorization probe: `geth ssh proxy <node-id> [--bearer-secret <secret>]`
- pipe registry/connect commands: `geth pipe listen <name>` and
`geth pipe connect <name> [--node <node-id>]`
`geth pipe connect <name> [--node <node-id>] [--bearer-secret <secret>]`
`geth peer export/import/list` is for untrusted peer-card exchange. Peer cards
include the Iroh EndpointID plus currently known relay/direct addresses.
@ -153,6 +156,9 @@ hash to the requested BLAKE3 CAS hash before storing them locally. Successful
fetches record the serving peer as a local provider, visible with
`geth cas providers <hash>`. This is the bootstrap transfer path; future work
will move provider/fetch behavior to `iroh-blobs`.
Remote resource commands that accept `--bearer-secret` can also authorize with a
resource-scoped bearer proof. This does not enroll the caller as a trusted node;
it only unlocks the requested capability on that one resource.
`geth ssh cert sync <node-id>` requires `ssh_cert.sync` on `resource:ssh:certs`
at the peer. `geth ssh revocation sync <node-id>` requires
`ssh_revocation.sync` on `resource:ssh:revocations`. Both commands merge

View file

@ -251,6 +251,8 @@ pub enum CasCommand {
Fetch {
node: String,
hash: String,
#[arg(long)]
bearer_secret: Option<String>,
},
Hash {
path: PathBuf,
@ -335,6 +337,8 @@ pub enum KvCommand {
Sync {
node: String,
name: String,
#[arg(long)]
bearer_secret: Option<String>,
},
}
@ -345,11 +349,15 @@ pub enum PubsubCommand {
message: String,
#[arg(long)]
node: Option<String>,
#[arg(long)]
bearer_secret: Option<String>,
},
Sub {
topic: String,
#[arg(long)]
node: Option<String>,
#[arg(long)]
bearer_secret: Option<String>,
},
}
@ -362,6 +370,8 @@ pub enum PipeCommand {
target: String,
#[arg(long)]
node: Option<String>,
#[arg(long)]
bearer_secret: Option<String>,
},
}
@ -386,22 +396,40 @@ pub enum DbCommand {
name: String,
#[arg(long, default_value_t = 100)]
limit: u32,
#[arg(long)]
bearer_secret: Option<String>,
},
}
#[derive(Debug, Subcommand)]
pub enum DocumentCommand {
Create { name: String },
Status { name: String },
Set { name: String, state_json: String },
Get { name: String },
Sync { node: String, name: String },
Create {
name: String,
},
Status {
name: String,
},
Set {
name: String,
state_json: String,
},
Get {
name: String,
},
Sync {
node: String,
name: String,
#[arg(long)]
bearer_secret: Option<String>,
},
}
#[derive(Debug, Subcommand)]
pub enum SshCommand {
Proxy {
node: String,
#[arg(long)]
bearer_secret: Option<String>,
},
Cert {
#[command(subcommand)]
@ -463,6 +491,8 @@ pub enum SshCertCommand {
},
Sync {
node: String,
#[arg(long)]
bearer_secret: Option<String>,
},
}
@ -499,6 +529,8 @@ pub enum SshRevocationCommand {
},
Sync {
node: String,
#[arg(long)]
bearer_secret: Option<String>,
},
}
@ -671,9 +703,14 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
hash: hash.into(),
out,
},
CasCommand::Fetch { node, hash } => ControlRequest::CasFetch {
CasCommand::Fetch {
node,
hash,
bearer_secret,
} => ControlRequest::CasFetch {
node,
hash: hash.into(),
bearer_secret,
},
CasCommand::Hash { path } => ControlRequest::CasHash { path },
CasCommand::Has { hash } => ControlRequest::CasHas { hash: hash.into() },
@ -731,23 +768,49 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
subject,
},
KvCommand::Get { name, key } => ControlRequest::KvGet { name, key },
KvCommand::Sync { node, name } => ControlRequest::KvSync { node, name },
KvCommand::Sync {
node,
name,
bearer_secret,
} => ControlRequest::KvSync {
node,
name,
bearer_secret,
},
},
Command::Pubsub { command } => match command {
PubsubCommand::Pub {
topic,
message,
node,
bearer_secret,
} => ControlRequest::PubsubPub {
topic,
message,
node,
bearer_secret,
},
PubsubCommand::Sub {
topic,
node,
bearer_secret,
} => ControlRequest::PubsubSub {
topic,
node,
bearer_secret,
},
PubsubCommand::Sub { topic, node } => ControlRequest::PubsubSub { topic, node },
},
Command::Pipe { command } => match command {
PipeCommand::Listen { name } => ControlRequest::PipeListen { name },
PipeCommand::Connect { target, node } => ControlRequest::PipeConnect { target, node },
PipeCommand::Connect {
target,
node,
bearer_secret,
} => ControlRequest::PipeConnect {
target,
node,
bearer_secret,
},
},
Command::Db { command } => match command {
DbCommand::Add { name, path } => ControlRequest::DbAdd { name, path },
@ -761,7 +824,17 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
after_db_version,
limit,
},
DbCommand::Sync { node, name, limit } => ControlRequest::DbSync { node, name, limit },
DbCommand::Sync {
node,
name,
limit,
bearer_secret,
} => ControlRequest::DbSync {
node,
name,
limit,
bearer_secret,
},
},
Command::Document { command } => match command {
DocumentCommand::Create { name } => ControlRequest::DocumentCreate { name },
@ -770,10 +843,24 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
ControlRequest::DocumentSet { name, state_json }
}
DocumentCommand::Get { name } => ControlRequest::DocumentGet { name },
DocumentCommand::Sync { node, name } => ControlRequest::DocumentSync { node, name },
DocumentCommand::Sync {
node,
name,
bearer_secret,
} => ControlRequest::DocumentSync {
node,
name,
bearer_secret,
},
},
Command::Ssh { command } => match command {
SshCommand::Proxy { node } => ControlRequest::SshProxyConnect { node },
SshCommand::Proxy {
node,
bearer_secret,
} => ControlRequest::SshProxyConnect {
node,
bearer_secret,
},
SshCommand::Cert { command } => match command {
SshCertCommand::Request {
public_key,
@ -820,7 +907,13 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
subject,
},
SshCertCommand::List { subject } => ControlRequest::SshCertList { subject },
SshCertCommand::Sync { node } => ControlRequest::SshCertSync { node },
SshCertCommand::Sync {
node,
bearer_secret,
} => ControlRequest::SshCertSync {
node,
bearer_secret,
},
},
SshCommand::Revocation { command } => match command {
SshRevocationCommand::Add {
@ -857,7 +950,13 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
format,
subject,
},
SshRevocationCommand::Sync { node } => ControlRequest::SshRevocationSync { node },
SshRevocationCommand::Sync {
node,
bearer_secret,
} => ControlRequest::SshRevocationSync {
node,
bearer_secret,
},
},
},
Command::Init | Command::Daemon { .. } => bail!("command is handled directly"),

View file

@ -52,6 +52,7 @@ pub enum ControlRequest {
CasFetch {
node: String,
hash: BlobHash,
bearer_secret: Option<String>,
},
CasHash {
path: PathBuf,
@ -182,6 +183,7 @@ pub enum ControlRequest {
},
SshCertSync {
node: String,
bearer_secret: Option<String>,
},
SshRevocationAdd {
kind: String,
@ -205,9 +207,11 @@ pub enum ControlRequest {
},
SshRevocationSync {
node: String,
bearer_secret: Option<String>,
},
SshProxyConnect {
node: String,
bearer_secret: Option<String>,
},
DbAdd {
name: String,
@ -225,6 +229,7 @@ pub enum ControlRequest {
node: String,
name: String,
limit: u32,
bearer_secret: Option<String>,
},
KvCreate {
name: String,
@ -242,6 +247,7 @@ pub enum ControlRequest {
KvSync {
node: String,
name: String,
bearer_secret: Option<String>,
},
DocumentCreate {
name: String,
@ -259,15 +265,18 @@ pub enum ControlRequest {
DocumentSync {
node: String,
name: String,
bearer_secret: Option<String>,
},
PubsubPub {
topic: String,
message: String,
node: Option<String>,
bearer_secret: Option<String>,
},
PubsubSub {
topic: String,
node: Option<String>,
bearer_secret: Option<String>,
},
PipeListen {
name: String,
@ -275,6 +284,7 @@ pub enum ControlRequest {
PipeConnect {
target: String,
node: Option<String>,
bearer_secret: Option<String>,
},
ModuleStub {
module: String,
@ -666,48 +676,57 @@ pub enum PeerControlRequest {
peer_card: PeerCard,
hash: BlobHash,
nonce: String,
bearer_proof: Option<BearerProof>,
},
SshCertSync {
peer_card: PeerCard,
since_ms: i64,
nonce: String,
bearer_proof: Option<BearerProof>,
},
SshRevocationSync {
peer_card: PeerCard,
since_ms: i64,
nonce: String,
bearer_proof: Option<BearerProof>,
},
KvSync {
peer_card: PeerCard,
name: String,
since_ms: i64,
nonce: String,
bearer_proof: Option<BearerProof>,
},
PubsubPublish {
peer_card: PeerCard,
topic: String,
message: String,
nonce: String,
bearer_proof: Option<BearerProof>,
},
PubsubSubscribe {
peer_card: PeerCard,
topic: String,
nonce: String,
bearer_proof: Option<BearerProof>,
},
PipeConnect {
peer_card: PeerCard,
target: String,
nonce: String,
bearer_proof: Option<BearerProof>,
},
SshProxyConnect {
peer_card: PeerCard,
nonce: String,
bearer_proof: Option<BearerProof>,
},
DocumentSync {
peer_card: PeerCard,
name: String,
since_ms: i64,
nonce: String,
bearer_proof: Option<BearerProof>,
},
DbSync {
peer_card: PeerCard,
@ -715,6 +734,7 @@ pub enum PeerControlRequest {
after_db_version: Option<i64>,
limit: u32,
nonce: String,
bearer_proof: Option<BearerProof>,
},
}
@ -1000,6 +1020,7 @@ mod tests {
let request = ControlRequest::CasFetch {
node: "node:peer".to_owned(),
hash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".into(),
bearer_secret: Some("bearer:test".to_owned()),
};
assert_eq!(
decode_request(&encode_request(&request).expect("encode")).expect("decode"),
@ -1050,6 +1071,7 @@ mod tests {
let request = ControlRequest::SshCertSync {
node: "node:ca".to_owned(),
bearer_secret: None,
};
assert_eq!(
decode_request(&encode_request(&request).expect("encode")).expect("decode"),
@ -1058,6 +1080,7 @@ mod tests {
let request = ControlRequest::SshRevocationSync {
node: "node:ca".to_owned(),
bearer_secret: None,
};
assert_eq!(
decode_request(&encode_request(&request).expect("encode")).expect("decode"),
@ -1107,6 +1130,7 @@ mod tests {
let request = ControlRequest::KvSync {
node: "node:peer".to_owned(),
name: "prefs".to_owned(),
bearer_secret: None,
};
assert_eq!(
decode_request(&encode_request(&request).expect("encode")).expect("decode"),
@ -1148,6 +1172,7 @@ mod tests {
let request = ControlRequest::PipeConnect {
target: "inbox".to_owned(),
node: Some("node:peer".to_owned()),
bearer_secret: None,
};
assert_eq!(
decode_request(&encode_request(&request).expect("encode")).expect("decode"),
@ -1158,6 +1183,7 @@ mod tests {
topic: "presence/test".to_owned(),
message: "online".to_owned(),
node: Some("node:peer".to_owned()),
bearer_secret: None,
};
assert_eq!(
decode_request(&encode_request(&request).expect("encode")).expect("decode"),
@ -1167,6 +1193,7 @@ mod tests {
let request = ControlRequest::PubsubSub {
topic: "presence/test".to_owned(),
node: Some("node:peer".to_owned()),
bearer_secret: None,
};
assert_eq!(
decode_request(&encode_request(&request).expect("encode")).expect("decode"),
@ -1231,6 +1258,7 @@ mod tests {
let request = ControlRequest::SshProxyConnect {
node: "node:peer".to_owned(),
bearer_secret: None,
};
assert_eq!(
decode_request(&encode_request(&request).expect("encode")).expect("decode"),
@ -1271,6 +1299,7 @@ mod tests {
node: "node:peer".to_owned(),
name: "notes".to_owned(),
limit: 50,
bearer_secret: None,
};
assert_eq!(
decode_request(&encode_request(&request).expect("encode")).expect("decode"),
@ -1426,6 +1455,7 @@ mod tests {
after_db_version: Some(7),
limit: 10,
nonce: "nonce".to_owned(),
bearer_proof: None,
};
assert_eq!(
decode_peer_request(&encode_peer_request(&request).expect("encode")).expect("decode"),
@ -1552,6 +1582,7 @@ mod tests {
},
topic: "presence/test".to_owned(),
nonce: "nonce".to_owned(),
bearer_proof: None,
};
assert_eq!(
decode_peer_request(&encode_peer_request(&request).expect("encode")).expect("decode"),
@ -1618,6 +1649,13 @@ mod tests {
},
},
nonce: "nonce".to_owned(),
bearer_proof: Some(BearerProof {
secret: "bearer:test".into(),
resource: "resource:ssh-proxy:local".into(),
capabilities: vec!["ssh_proxy.connect".into()],
nonce: "nonce".to_owned(),
response: "response".to_owned(),
}),
};
assert_eq!(
decode_peer_request(&encode_peer_request(&request).expect("encode")).expect("decode"),

View file

@ -255,42 +255,52 @@ pub async fn handle_request_async(
ControlRequest::CasFetch {
node: peer_node,
hash,
} => cas_fetch_from_peer(node, &peer_node, hash).await,
ControlRequest::SshCertSync { node: peer_node } => {
ssh_cert_sync_from_peer(node, &peer_node).await
}
ControlRequest::SshRevocationSync { node: peer_node } => {
ssh_revocation_sync_from_peer(node, &peer_node).await
}
bearer_secret,
} => cas_fetch_from_peer(node, &peer_node, hash, bearer_secret).await,
ControlRequest::SshCertSync {
node: peer_node,
bearer_secret,
} => ssh_cert_sync_from_peer(node, &peer_node, bearer_secret).await,
ControlRequest::SshRevocationSync {
node: peer_node,
bearer_secret,
} => ssh_revocation_sync_from_peer(node, &peer_node, bearer_secret).await,
ControlRequest::KvSync {
node: peer_node,
name,
} => kv_sync_from_peer(node, &peer_node, &name).await,
bearer_secret,
} => kv_sync_from_peer(node, &peer_node, &name, bearer_secret).await,
ControlRequest::DbSync {
node: peer_node,
name,
limit,
} => db_sync_from_peer(node, &peer_node, &name, limit).await,
bearer_secret,
} => db_sync_from_peer(node, &peer_node, &name, limit, bearer_secret).await,
ControlRequest::PubsubPub {
topic,
message,
node: Some(peer_node),
} => pubsub_publish_to_peer(node, &peer_node, topic, message).await,
bearer_secret,
} => pubsub_publish_to_peer(node, &peer_node, topic, message, bearer_secret).await,
ControlRequest::PubsubSub {
topic,
node: Some(peer_node),
} => pubsub_subscribe_from_peer(node, &peer_node, topic).await,
bearer_secret,
} => pubsub_subscribe_from_peer(node, &peer_node, topic, bearer_secret).await,
ControlRequest::PipeConnect {
target,
node: Some(peer_node),
} => pipe_connect_to_peer(node, &peer_node, target).await,
ControlRequest::SshProxyConnect { node: peer_node } => {
ssh_proxy_connect_to_peer(node, &peer_node).await
}
bearer_secret,
} => pipe_connect_to_peer(node, &peer_node, target, bearer_secret).await,
ControlRequest::SshProxyConnect {
node: peer_node,
bearer_secret,
} => ssh_proxy_connect_to_peer(node, &peer_node, bearer_secret).await,
ControlRequest::DocumentSync {
node: peer_node,
name,
} => document_sync_from_peer(node, &peer_node, &name).await,
bearer_secret,
} => document_sync_from_peer(node, &peer_node, &name, bearer_secret).await,
other => handle_request(node, other),
}
}
@ -699,6 +709,7 @@ async fn cas_fetch_from_peer(
node: &LocalNode,
peer_node: &str,
hash: BlobHash,
bearer_secret: Option<String>,
) -> Result<ControlResponse, NodeError> {
let store = Store::open(&node.paths.metadata_db())?;
let stored = store
@ -733,6 +744,7 @@ async fn cas_fetch_from_peer(
peer_card: self_card,
hash: hash.clone(),
nonce: nonce.clone(),
bearer_proof: bearer_proof(bearer_secret, "resource:cas:local", "cas.fetch", &nonce),
};
let conn = endpoint
@ -843,6 +855,7 @@ async fn cas_fetch_from_peer(
async fn ssh_cert_sync_from_peer(
node: &LocalNode,
peer_node: &str,
bearer_secret: Option<String>,
) -> Result<ControlResponse, NodeError> {
let since_ms = load_live_sync_cursor(
&Store::open(&node.paths.metadata_db())?,
@ -853,7 +866,13 @@ async fn ssh_cert_sync_from_peer(
PeerControlRequest::SshCertSync {
peer_card,
since_ms,
nonce,
nonce: nonce.clone(),
bearer_proof: bearer_proof(
bearer_secret,
"resource:ssh:certs",
"ssh_cert.sync",
&nonce,
),
}
})
.await?;
@ -913,6 +932,7 @@ async fn ssh_cert_sync_from_peer(
async fn ssh_revocation_sync_from_peer(
node: &LocalNode,
peer_node: &str,
bearer_secret: Option<String>,
) -> Result<ControlResponse, NodeError> {
let since_ms = load_live_sync_cursor(
&Store::open(&node.paths.metadata_db())?,
@ -926,7 +946,13 @@ async fn ssh_revocation_sync_from_peer(
|peer_card, nonce| PeerControlRequest::SshRevocationSync {
peer_card,
since_ms,
nonce,
nonce: nonce.clone(),
bearer_proof: bearer_proof(
bearer_secret,
"resource:ssh:revocations",
"ssh_revocation.sync",
&nonce,
),
},
)
.await?;
@ -980,6 +1006,7 @@ async fn kv_sync_from_peer(
node: &LocalNode,
peer_node: &str,
name: &str,
bearer_secret: Option<String>,
) -> Result<ControlResponse, NodeError> {
geth_kv::validate_kv_name(name).map_err(|_| NodeError::InvalidKvName(name.to_owned()))?;
let stream = format!("kv:{name}");
@ -990,7 +1017,13 @@ async fn kv_sync_from_peer(
peer_card,
name: name.to_owned(),
since_ms,
nonce,
nonce: nonce.clone(),
bearer_proof: bearer_proof(
bearer_secret,
&format!("resource:kv:{name}"),
"kv.read",
&nonce,
),
}
})
.await?;
@ -1065,6 +1098,7 @@ async fn pubsub_publish_to_peer(
peer_node: &str,
topic: String,
message: String,
bearer_secret: Option<String>,
) -> Result<ControlResponse, NodeError> {
geth_pubsub::validate_topic(&topic)?;
geth_pubsub::validate_message(&message)?;
@ -1073,7 +1107,13 @@ async fn pubsub_publish_to_peer(
peer_card,
topic: topic.clone(),
message: message.clone(),
nonce,
nonce: nonce.clone(),
bearer_proof: bearer_proof(
bearer_secret,
&format!("resource:pubsub:{topic}"),
"pubsub.publish",
&nonce,
),
}
})
.await?;
@ -1114,13 +1154,20 @@ async fn pubsub_subscribe_from_peer(
node: &LocalNode,
peer_node: &str,
topic: String,
bearer_secret: Option<String>,
) -> Result<ControlResponse, NodeError> {
geth_pubsub::validate_topic(&topic)?;
let response = request_peer_control(node, peer_node, "pubsub-subscribe", |peer_card, nonce| {
PeerControlRequest::PubsubSubscribe {
peer_card,
topic: topic.clone(),
nonce,
nonce: nonce.clone(),
bearer_proof: bearer_proof(
bearer_secret,
&format!("resource:pubsub:{topic}"),
"pubsub.subscribe",
&nonce,
),
}
})
.await?;
@ -1159,13 +1206,20 @@ async fn pipe_connect_to_peer(
node: &LocalNode,
peer_node: &str,
target: String,
bearer_secret: Option<String>,
) -> Result<ControlResponse, NodeError> {
geth_pipe::validate_pipe_name(&target)?;
let response = request_peer_control(node, peer_node, "pipe-connect", |peer_card, nonce| {
PeerControlRequest::PipeConnect {
peer_card,
target: target.clone(),
nonce,
nonce: nonce.clone(),
bearer_proof: bearer_proof(
bearer_secret,
&format!("resource:pipe:{target}"),
"pipe.connect",
&nonce,
),
}
})
.await?;
@ -1206,10 +1260,20 @@ async fn pipe_connect_to_peer(
async fn ssh_proxy_connect_to_peer(
node: &LocalNode,
peer_node: &str,
bearer_secret: Option<String>,
) -> Result<ControlResponse, NodeError> {
let response =
request_peer_control(node, peer_node, "ssh-proxy-connect", |peer_card, nonce| {
PeerControlRequest::SshProxyConnect { peer_card, nonce }
PeerControlRequest::SshProxyConnect {
peer_card,
nonce: nonce.clone(),
bearer_proof: bearer_proof(
bearer_secret,
"resource:ssh-proxy:local",
"ssh_proxy.connect",
&nonce,
),
}
})
.await?;
match response {
@ -1242,6 +1306,7 @@ async fn document_sync_from_peer(
node: &LocalNode,
peer_node: &str,
name: &str,
bearer_secret: Option<String>,
) -> Result<ControlResponse, NodeError> {
geth_document::validate_document_name(name)
.map_err(|_| NodeError::InvalidDocumentName(name.to_owned()))?;
@ -1253,7 +1318,13 @@ async fn document_sync_from_peer(
peer_card,
name: name.to_owned(),
since_ms,
nonce,
nonce: nonce.clone(),
bearer_proof: bearer_proof(
bearer_secret,
&format!("resource:document:{name}"),
"document.read",
&nonce,
),
}
})
.await?;
@ -1347,6 +1418,7 @@ async fn db_sync_from_peer(
peer_node: &str,
name: &str,
limit: u32,
bearer_secret: Option<String>,
) -> Result<ControlResponse, NodeError> {
geth_db::validate_db_name(name).map_err(|_| NodeError::InvalidDbName(name.to_owned()))?;
let store = Store::open(&node.paths.metadata_db())?;
@ -1362,7 +1434,13 @@ async fn db_sync_from_peer(
name: name.to_owned(),
after_db_version,
limit,
nonce,
nonce: nonce.clone(),
bearer_proof: bearer_proof(
bearer_secret,
&format!("resource:db:{name}"),
"db.sync",
&nonce,
),
}
})
.await?;
@ -1585,6 +1663,123 @@ fn can_sync_resource(
.allowed)
}
fn bearer_proof(
bearer_secret: Option<String>,
resource: &str,
capability: &str,
nonce: &str,
) -> Option<BearerProof> {
bearer_secret.map(|secret| {
let secret = geth_types::SecretId::new(secret);
let resource = ResourceId::new(resource.to_owned());
let capabilities = vec![Capability::new(capability.to_owned())];
let response = geth_secrets::bearer_response(&secret, &resource, &capabilities, nonce);
BearerProof {
secret,
resource,
capabilities,
nonce: nonce.to_owned(),
response,
}
})
}
fn explain_peer_or_bearer(
store: &Store,
peer_node: &str,
resource: &str,
capability: &str,
nonce: &str,
bearer_proof: Option<&BearerProof>,
) -> Result<AuthExplanation, NodeError> {
let ops = load_auth_ops_for_resource(store, resource)?;
let resource_id = ResourceId::new(resource.to_owned());
let capability_id = Capability::new(capability.to_owned());
let peer_explanation = geth_auth::explain_auth_ops(
&ops,
PrincipalId::new(peer_node.to_owned()),
resource_id.clone(),
capability_id.clone(),
);
if peer_explanation.allowed {
return Ok(peer_explanation);
}
let Some(proof) = bearer_proof else {
return Ok(peer_explanation);
};
let bearer_subject = format!("bearer:{}", proof.secret);
let denied = |reason: String| AuthExplanation {
subject: bearer_subject.clone(),
resource: resource.to_owned(),
capability: capability.to_owned(),
allowed: false,
reason,
evaluated_ops: peer_explanation.evaluated_ops,
};
if proof.resource != resource_id {
return Ok(denied(format!(
"bearer proof resource {} does not match requested resource {resource}",
proof.resource
)));
}
if proof.nonce != nonce {
return Ok(denied(
"bearer proof nonce does not match request".to_owned(),
));
}
if !proof
.capabilities
.iter()
.any(|granted| geth_auth::capability_allows(granted, &capability_id))
{
return Ok(denied(
"bearer proof does not include the requested capability".to_owned(),
));
}
if !geth_secrets::verify_bearer_response(
&proof.secret,
&proof.resource,
&proof.capabilities,
nonce,
&proof.response,
) {
return Ok(denied("bearer proof response is invalid".to_owned()));
}
let now = UnixMillis(geth_store::now_ms());
let Some(access) = load_bearer_access(store)?.into_iter().find(|access| {
access.secret == proof.secret
&& access.resource == proof.resource
&& access.expires_at.is_none_or(|expires| expires.0 > now.0)
}) else {
return Ok(denied(
"bearer access is not active for requested resource".to_owned(),
));
};
if !access
.capabilities
.iter()
.any(|granted| geth_auth::capability_allows(granted, &capability_id))
{
return Ok(denied(
"bearer access lacks requested capability".to_owned(),
));
}
Ok(AuthExplanation {
subject: bearer_subject,
resource: resource.to_owned(),
capability: capability.to_owned(),
allowed: true,
reason:
"bearer proof allows this resource-scoped capability without granting node identity"
.to_owned(),
evaluated_ops: peer_explanation.evaluated_ops,
})
}
async fn request_peer_control(
node: &LocalNode,
peer_node: &str,
@ -1754,7 +1949,7 @@ async fn run_live_sync_once(node: &LocalNode) -> Result<(), NodeError> {
"ssh-certs",
remote_watermarks.as_ref(),
)? {
if let Err(error) = ssh_cert_sync_from_peer(node, &peer.peer_id).await {
if let Err(error) = ssh_cert_sync_from_peer(node, &peer.peer_id, None).await {
tracing::debug!(peer = %peer.peer_id, %error, "SSH cert live sync failed");
}
}
@ -1764,7 +1959,7 @@ async fn run_live_sync_once(node: &LocalNode) -> Result<(), NodeError> {
"ssh-revocations",
remote_watermarks.as_ref(),
)? {
if let Err(error) = ssh_revocation_sync_from_peer(node, &peer.peer_id).await {
if let Err(error) = ssh_revocation_sync_from_peer(node, &peer.peer_id, None).await {
tracing::debug!(peer = %peer.peer_id, %error, "SSH revocation live sync failed");
}
}
@ -1772,7 +1967,7 @@ async fn run_live_sync_once(node: &LocalNode) -> Result<(), NodeError> {
let stream = format!("kv:{}", kv.name);
if should_live_sync_stream(&store, &peer.peer_id, &stream, remote_watermarks.as_ref())?
{
if let Err(error) = kv_sync_from_peer(node, &peer.peer_id, &kv.name).await {
if let Err(error) = kv_sync_from_peer(node, &peer.peer_id, &kv.name, None).await {
tracing::debug!(peer = %peer.peer_id, kv = %kv.name, %error, "KV live sync failed");
}
}
@ -1782,7 +1977,7 @@ async fn run_live_sync_once(node: &LocalNode) -> Result<(), NodeError> {
if should_live_sync_stream(&store, &peer.peer_id, &stream, remote_watermarks.as_ref())?
{
if let Err(error) =
document_sync_from_peer(node, &peer.peer_id, &document.name).await
document_sync_from_peer(node, &peer.peer_id, &document.name, None).await
{
tracing::debug!(peer = %peer.peer_id, document = %document.name, %error, "document live sync failed");
}
@ -1792,7 +1987,9 @@ async fn run_live_sync_once(node: &LocalNode) -> Result<(), NodeError> {
let stream = format!("db:{}", db.name);
if should_live_sync_stream(&store, &peer.peer_id, &stream, remote_watermarks.as_ref())?
{
if let Err(error) = db_sync_from_peer(node, &peer.peer_id, &db.name, 100).await {
if let Err(error) =
db_sync_from_peer(node, &peer.peer_id, &db.name, 100, None).await
{
tracing::debug!(peer = %peer.peer_id, db = %db.name, %error, "DB live sync failed");
}
}
@ -1919,6 +2116,7 @@ async fn handle_iroh_control_connection(
peer_card,
hash,
nonce,
bearer_proof,
} => {
peer_card.validate_candidate()?;
ensure_peer_card_matches_endpoint(&peer_card, &remote_endpoint_id)?;
@ -1935,12 +2133,14 @@ async fn handle_iroh_control_connection(
})?;
let resource = "resource:cas:local".to_owned();
let capability = "cas.fetch".to_owned();
let explanation = geth_auth::explain_auth_ops(
&load_auth_ops_for_resource(&store, &resource)?,
PrincipalId::new(peer_card.node_id.to_string()),
ResourceId::new(resource),
Capability::new(capability),
);
let explanation = explain_peer_or_bearer(
&store,
peer_card.node_id.as_str(),
&resource,
&capability,
&nonce,
bearer_proof.as_ref(),
)?;
if explanation.allowed {
match LocalCas::new(node.paths.cas_dir()).read_bytes(&hash) {
Ok(content) => {
@ -1987,6 +2187,7 @@ async fn handle_iroh_control_connection(
peer_card,
since_ms,
nonce,
bearer_proof,
} => {
peer_card.validate_candidate()?;
ensure_peer_card_matches_endpoint(&peer_card, &remote_endpoint_id)?;
@ -2004,12 +2205,14 @@ async fn handle_iroh_control_connection(
let resource = "resource:ssh:certs".to_owned();
let capability = "ssh_cert.sync".to_owned();
let high_water_ms = geth_store::now_ms();
let explanation = geth_auth::explain_auth_ops(
&load_auth_ops_for_resource(&store, &resource)?,
PrincipalId::new(peer_card.node_id.to_string()),
ResourceId::new(resource),
Capability::new(capability),
);
let explanation = explain_peer_or_bearer(
&store,
peer_card.node_id.as_str(),
&resource,
&capability,
&nonce,
bearer_proof.as_ref(),
)?;
let (requests, certificates) = if explanation.allowed {
(
store
@ -2045,6 +2248,7 @@ async fn handle_iroh_control_connection(
peer_card,
since_ms,
nonce,
bearer_proof,
} => {
peer_card.validate_candidate()?;
ensure_peer_card_matches_endpoint(&peer_card, &remote_endpoint_id)?;
@ -2062,12 +2266,14 @@ async fn handle_iroh_control_connection(
let resource = "resource:ssh:revocations".to_owned();
let capability = "ssh_revocation.sync".to_owned();
let high_water_ms = geth_store::now_ms();
let explanation = geth_auth::explain_auth_ops(
&load_auth_ops_for_resource(&store, &resource)?,
PrincipalId::new(peer_card.node_id.to_string()),
ResourceId::new(resource),
Capability::new(capability),
);
let explanation = explain_peer_or_bearer(
&store,
peer_card.node_id.as_str(),
&resource,
&capability,
&nonce,
bearer_proof.as_ref(),
)?;
let revocations = if explanation.allowed {
store
.list_ssh_revocations_since(since_ms)?
@ -2096,6 +2302,7 @@ async fn handle_iroh_control_connection(
name,
since_ms,
nonce,
bearer_proof,
} => {
geth_kv::validate_kv_name(&name).map_err(|_| NodeError::InvalidKvName(name.clone()))?;
peer_card.validate_candidate()?;
@ -2114,12 +2321,14 @@ async fn handle_iroh_control_connection(
if let Some(kv) = store.get_kv_store_by_name(&name)? {
let capability = "kv.read".to_owned();
let high_water_ms = geth_store::now_ms();
let explanation = geth_auth::explain_auth_ops(
&load_auth_ops_for_resource(&store, &kv.resource_id)?,
PrincipalId::new(peer_card.node_id.to_string()),
ResourceId::new(kv.resource_id.clone()),
Capability::new(capability),
);
let explanation = explain_peer_or_bearer(
&store,
peer_card.node_id.as_str(),
&kv.resource_id,
&capability,
&nonce,
bearer_proof.as_ref(),
)?;
let entries = if explanation.allowed {
store
.list_kv_entries_since(&kv.kv_id, since_ms)?
@ -2158,6 +2367,7 @@ async fn handle_iroh_control_connection(
topic,
message,
nonce,
bearer_proof,
} => {
geth_pubsub::validate_topic(&topic)?;
geth_pubsub::validate_message(&message)?;
@ -2176,12 +2386,14 @@ async fn handle_iroh_control_connection(
})?;
let resource = format!("resource:pubsub:{topic}");
let capability = "pubsub.publish".to_owned();
let explanation = geth_auth::explain_auth_ops(
&load_auth_ops_for_resource(&store, &resource)?,
PrincipalId::new(peer_card.node_id.to_string()),
ResourceId::new(resource),
Capability::new(capability),
);
let explanation = explain_peer_or_bearer(
&store,
peer_card.node_id.as_str(),
&resource,
&capability,
&nonce,
bearer_proof.as_ref(),
)?;
let published = if explanation.allowed {
Some(record_pubsub_message(&node, topic, message)?)
} else {
@ -2204,6 +2416,7 @@ async fn handle_iroh_control_connection(
peer_card,
topic,
nonce,
bearer_proof,
} => {
geth_pubsub::validate_topic(&topic)?;
peer_card.validate_candidate()?;
@ -2221,12 +2434,14 @@ async fn handle_iroh_control_connection(
})?;
let resource = format!("resource:pubsub:{topic}");
let capability = "pubsub.subscribe".to_owned();
let explanation = geth_auth::explain_auth_ops(
&load_auth_ops_for_resource(&store, &resource)?,
PrincipalId::new(peer_card.node_id.to_string()),
ResourceId::new(resource),
Capability::new(capability),
);
let explanation = explain_peer_or_bearer(
&store,
peer_card.node_id.as_str(),
&resource,
&capability,
&nonce,
bearer_proof.as_ref(),
)?;
let messages = if explanation.allowed {
pubsub_messages_for_topic(&node, &topic)?
} else {
@ -2250,6 +2465,7 @@ async fn handle_iroh_control_connection(
peer_card,
target,
nonce,
bearer_proof,
} => {
geth_pipe::validate_pipe_name(&target)?;
peer_card.validate_candidate()?;
@ -2267,12 +2483,14 @@ async fn handle_iroh_control_connection(
})?;
let resource = format!("resource:pipe:{target}");
let capability = "pipe.connect".to_owned();
let explanation = geth_auth::explain_auth_ops(
&load_auth_ops_for_resource(&store, &resource)?,
PrincipalId::new(peer_card.node_id.to_string()),
ResourceId::new(resource),
Capability::new(capability),
);
let explanation = explain_peer_or_bearer(
&store,
peer_card.node_id.as_str(),
&resource,
&capability,
&nonce,
bearer_proof.as_ref(),
)?;
let connection = if explanation.allowed {
Some(record_pipe_connection(
&node,
@ -2295,7 +2513,11 @@ async fn handle_iroh_control_connection(
note: "pipe connect authenticated endpoint/card binding and required pipe.connect on the remote pipe resource; byte streams are not implemented yet".to_owned(),
}
}
PeerControlRequest::SshProxyConnect { peer_card, nonce } => {
PeerControlRequest::SshProxyConnect {
peer_card,
nonce,
bearer_proof,
} => {
peer_card.validate_candidate()?;
ensure_peer_card_matches_endpoint(&peer_card, &remote_endpoint_id)?;
let discovered = DiscoveredPeer::candidate(
@ -2311,12 +2533,14 @@ async fn handle_iroh_control_connection(
})?;
let resource = "resource:ssh-proxy:local".to_owned();
let capability = "ssh_proxy.connect".to_owned();
let explanation = geth_auth::explain_auth_ops(
&load_auth_ops_for_resource(&store, &resource)?,
PrincipalId::new(peer_card.node_id.to_string()),
ResourceId::new(resource),
Capability::new(capability),
);
let explanation = explain_peer_or_bearer(
&store,
peer_card.node_id.as_str(),
&resource,
&capability,
&nonce,
bearer_proof.as_ref(),
)?;
let connection = if explanation.allowed {
Some(SshProxyConnection {
target_node: NodeId::new(node.node_id.clone()),
@ -2346,6 +2570,7 @@ async fn handle_iroh_control_connection(
name,
since_ms,
nonce,
bearer_proof,
} => {
geth_document::validate_document_name(&name)
.map_err(|_| NodeError::InvalidDocumentName(name.clone()))?;
@ -2365,12 +2590,14 @@ async fn handle_iroh_control_connection(
if let Some(document) = store.get_document_resource_by_name(&name)? {
let capability = "document.read".to_owned();
let high_water_ms = geth_store::now_ms();
let explanation = geth_auth::explain_auth_ops(
&load_auth_ops_for_resource(&store, &document.resource_id)?,
PrincipalId::new(peer_card.node_id.to_string()),
ResourceId::new(document.resource_id.clone()),
Capability::new(capability),
);
let explanation = explain_peer_or_bearer(
&store,
peer_card.node_id.as_str(),
&document.resource_id,
&capability,
&nonce,
bearer_proof.as_ref(),
)?;
let state = if explanation.allowed && document.updated_at_ms >= since_ms {
Some(document_state_from_stored(&document))
} else {
@ -2402,6 +2629,7 @@ async fn handle_iroh_control_connection(
after_db_version,
limit,
nonce,
bearer_proof,
} => {
geth_db::validate_db_name(&name).map_err(|_| NodeError::InvalidDbName(name.clone()))?;
peer_card.validate_candidate()?;
@ -2419,12 +2647,14 @@ async fn handle_iroh_control_connection(
})?;
if let Some(db) = store.get_db_resource_by_name(&name)? {
let capability = "db.sync".to_owned();
let explanation = geth_auth::explain_auth_ops(
&load_auth_ops_for_resource(&store, &db.resource_id)?,
PrincipalId::new(peer_card.node_id.to_string()),
ResourceId::new(db.resource_id.clone()),
Capability::new(capability),
);
let explanation = explain_peer_or_bearer(
&store,
peer_card.node_id.as_str(),
&db.resource_id,
&capability,
&nonce,
bearer_proof.as_ref(),
)?;
let sync_result: Result<_, String> = if explanation.allowed {
let path = Path::new(&db.path);
match geth_db::extract_crsqlite_changes(path, after_db_version, limit) {
@ -3731,6 +3961,7 @@ pub fn handle_request(
topic,
message,
node: None,
..
} => {
geth_pubsub::validate_topic(&topic)?;
geth_pubsub::validate_message(&message)?;
@ -3738,7 +3969,9 @@ pub fn handle_request(
Ok(ControlResponse::PubsubPublished { message })
}
ControlRequest::PubsubPub { node: Some(_), .. } => Err(NodeError::IrohEndpointUnavailable),
ControlRequest::PubsubSub { topic, node: None } => {
ControlRequest::PubsubSub {
topic, node: None, ..
} => {
geth_pubsub::validate_topic(&topic)?;
let messages = pubsub_messages_for_topic(node, &topic)?;
Ok(ControlResponse::PubsubMessages {
@ -3764,7 +3997,9 @@ pub fn handle_request(
runtime.listeners.insert(name, listener.clone());
Ok(ControlResponse::PipeListening { listener })
}
ControlRequest::PipeConnect { target, node: None } => {
ControlRequest::PipeConnect {
target, node: None, ..
} => {
geth_pipe::validate_pipe_name(&target)?;
let connection = record_pipe_connection(
node,
@ -4899,6 +5134,7 @@ mod tests {
ControlRequest::CasFetch {
node: right_card.node_id.to_string(),
hash: right_blob.hash.clone(),
bearer_secret: None,
},
)
.await
@ -4927,6 +5163,7 @@ mod tests {
ControlRequest::KvSync {
node: right_card.node_id.to_string(),
name: "prefs".to_owned(),
bearer_secret: None,
},
)
.await
@ -4951,6 +5188,7 @@ mod tests {
topic: "presence/test".to_owned(),
message: "hello".to_owned(),
node: Some(right_card.node_id.to_string()),
bearer_secret: None,
},
)
.await
@ -4974,6 +5212,7 @@ mod tests {
ControlRequest::PubsubSub {
topic: "presence/test".to_owned(),
node: Some(right_card.node_id.to_string()),
bearer_secret: None,
},
)
.await
@ -4997,6 +5236,7 @@ mod tests {
ControlRequest::PipeConnect {
target: "inbox".to_owned(),
node: Some(right_card.node_id.to_string()),
bearer_secret: None,
},
)
.await
@ -5019,6 +5259,7 @@ mod tests {
&left,
ControlRequest::SshProxyConnect {
node: right_card.node_id.to_string(),
bearer_secret: None,
},
)
.await
@ -5042,6 +5283,7 @@ mod tests {
ControlRequest::DocumentSync {
node: right_card.node_id.to_string(),
name: "notes".to_owned(),
bearer_secret: None,
},
)
.await
@ -5066,6 +5308,7 @@ mod tests {
node: right_card.node_id.to_string(),
name: "notes".to_owned(),
limit: 10,
bearer_secret: None,
},
)
.await
@ -5088,6 +5331,45 @@ mod tests {
other => panic!("unexpected denied DB sync response: {other:?}"),
}
let bearer_secret = match handle_request(
&right,
ControlRequest::SecretBearerCreate {
resource: "resource:cas:local".to_owned(),
capabilities: vec!["cas.fetch".to_owned()],
expires_at_ms: None,
},
)
.expect("create remote CAS bearer access")
{
ControlResponse::SecretBearerCreated { access } => access.secret.to_string(),
other => panic!("unexpected bearer create response: {other:?}"),
};
let bearer_fetch = handle_request_async(
&left,
ControlRequest::CasFetch {
node: right_card.node_id.to_string(),
hash: right_blob.hash.clone(),
bearer_secret: Some(bearer_secret),
},
)
.await
.expect("bearer cas fetch");
match bearer_fetch {
ControlResponse::CasFetched {
allowed,
hash,
size_bytes,
reason,
..
} => {
assert!(allowed);
assert_eq!(hash, right_blob.hash);
assert_eq!(size_bytes, right_blob.size_bytes);
assert!(reason.contains("bearer proof"));
}
other => panic!("unexpected bearer CAS fetch response: {other:?}"),
}
handle_request(
&right,
ControlRequest::AuthGrant {
@ -5198,6 +5480,7 @@ mod tests {
ControlRequest::CasFetch {
node: right_card.node_id.to_string(),
hash: right_blob.hash.clone(),
bearer_secret: None,
},
)
.await
@ -5237,6 +5520,7 @@ mod tests {
ControlRequest::KvSync {
node: right_card.node_id.to_string(),
name: "prefs".to_owned(),
bearer_secret: None,
},
)
.await
@ -5277,6 +5561,7 @@ mod tests {
topic: "presence/test".to_owned(),
message: "hello".to_owned(),
node: Some(right_card.node_id.to_string()),
bearer_secret: None,
},
)
.await
@ -5302,6 +5587,7 @@ mod tests {
ControlRequest::PubsubSub {
topic: "presence/test".to_owned(),
node: None,
bearer_secret: None,
},
)
.expect("right pubsub sub after remote publish");
@ -5318,6 +5604,7 @@ mod tests {
ControlRequest::PubsubSub {
topic: "presence/test".to_owned(),
node: Some(right_card.node_id.to_string()),
bearer_secret: None,
},
)
.await
@ -5344,6 +5631,7 @@ mod tests {
ControlRequest::PipeConnect {
target: "inbox".to_owned(),
node: Some(right_card.node_id.to_string()),
bearer_secret: None,
},
)
.await
@ -5368,6 +5656,7 @@ mod tests {
&left,
ControlRequest::SshProxyConnect {
node: right_card.node_id.to_string(),
bearer_secret: None,
},
)
.await
@ -5398,6 +5687,7 @@ mod tests {
ControlRequest::DocumentSync {
node: right_card.node_id.to_string(),
name: "notes".to_owned(),
bearer_secret: None,
},
)
.await
@ -5437,6 +5727,7 @@ mod tests {
node: right_card.node_id.to_string(),
name: "notes".to_owned(),
limit: 10,
bearer_secret: None,
},
)
.await
@ -5472,6 +5763,7 @@ mod tests {
&left,
ControlRequest::SshCertSync {
node: right_card.node_id.to_string(),
bearer_secret: None,
},
)
.await
@ -5517,6 +5809,7 @@ mod tests {
&left,
ControlRequest::SshCertSync {
node: right_card.node_id.to_string(),
bearer_secret: None,
},
)
.await
@ -5552,6 +5845,7 @@ mod tests {
&left,
ControlRequest::SshRevocationSync {
node: right_card.node_id.to_string(),
bearer_secret: None,
},
)
.await

View file

@ -1368,6 +1368,7 @@ fn pubsub_pub_sub_uses_lossy_in_memory_runtime() {
topic: "presence/laptop".to_owned(),
message: "online".to_owned(),
node: None,
bearer_secret: None,
},
)
.expect("publish");
@ -1384,6 +1385,7 @@ fn pubsub_pub_sub_uses_lossy_in_memory_runtime() {
geth_control::ControlRequest::PubsubSub {
topic: "presence/laptop".to_owned(),
node: None,
bearer_secret: None,
},
)
.expect("subscribe snapshot");
@ -1407,6 +1409,7 @@ fn pubsub_pub_sub_uses_lossy_in_memory_runtime() {
geth_control::ControlRequest::PubsubSub {
topic: "presence/laptop".to_owned(),
node: None,
bearer_secret: None,
},
)
.expect("subscribe reopened snapshot");
@ -1424,6 +1427,7 @@ fn pubsub_pub_sub_uses_lossy_in_memory_runtime() {
topic: "presence/laptop".to_owned(),
message: String::new(),
node: None,
bearer_secret: None,
},
)
.is_err()
@ -1457,6 +1461,7 @@ fn pipe_listen_connect_uses_local_runtime_registry() {
geth_control::ControlRequest::PipeConnect {
target: "inbox".to_owned(),
node: None,
bearer_secret: None,
},
)
.expect("connect pipe");
@ -1479,6 +1484,7 @@ fn pipe_listen_connect_uses_local_runtime_registry() {
geth_control::ControlRequest::PipeConnect {
target: "inbox".to_owned(),
node: None,
bearer_secret: None,
},
)
.expect("connect after reopen");

View file

@ -271,8 +271,11 @@ as resource-scoped auth operations and rejects trust-mutation capabilities such
as `auth.delegate`, `auth.revoke`, and `node.enroll`. Bearer challenge/proof
commands derive deterministic BLAKE3 keyed responses from the bearer secret,
resource, nonce, and requested capabilities, then verify them against active
resource-scoped bearer grants. The daemon does not yet store payload key
material, encrypt resource data, or distribute key envelopes.
resource-scoped bearer grants. Remote resource operations can carry optional
bearer proofs over the protected Iroh control path; a valid proof authorizes
only the requested resource capability and does not create node trust. The daemon
does not yet store payload key material, encrypt resource data, distribute key
envelopes, or separate public bearer ids from private bearer tokens.
## Multi-User Direction

View file

@ -223,8 +223,12 @@ resource-scoped capability decisions.
- `[x]` `geth secret bearer challenge/prove/verify` exercises
resource-scoped bearer challenge-response proofs.
- `[x]` Tests verify valid bearer proofs and capability-scoped proof denial.
- `[ ]` Future completion wires bearer proof verification into remote module
authorization paths.
- `[x]` Remote module authorization paths accept optional bearer proofs for
the requested resource capability without granting node identity.
- `[x]` Tests verify remote CAS fetch succeeds through a bearer proof before
the caller has a node grant.
- `[ ]` Future completion avoids sending bearer secret identifiers as proof
material by separating public bearer ids from private bearer tokens.
- `[~]` SSH certificate and revocation lifecycle.
Acceptance criteria: