Authorize remote modules with bearer proofs
This commit is contained in:
parent
72cd018a3e
commit
7e39a19c18
8 changed files with 572 additions and 120 deletions
|
|
@ -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
|
||||
|
|
|
|||
20
README.md
20
README.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in a new issue