Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
ffb21cc
chore(tinybus): move hash module to crate root
senamakel Aug 13, 2026
27efe0b
chore: add hash module
senamakel Aug 13, 2026
37653d7
fix(module): remove unused import of `std::sync::Arc`
senamakel Aug 13, 2026
1cc522b
fix(host): restore module host shutdown on drop
senamakel Aug 13, 2026
14403ca
fix(attest): restore missing attestation verification
senamakel Aug 13, 2026
20840f2
fix(error): derive Clone for BusError
senamakel Aug 13, 2026
f0c5aef
fix(error): derive Clone for BusError
senamakel Aug 13, 2026
fbb71a0
fix(error): derive Clone for BusError
senamakel Aug 13, 2026
13a5eab
fix(error): derive Clone for BusError
senamakel Aug 13, 2026
cbdf9c9
fix(message): restore missing doc comments on public types
senamakel Aug 13, 2026
aed1142
fix(message): set confidential flag on outgoing messages
senamakel Aug 13, 2026
e4cf3ac
fix(message): restore missing doc comments on public types
senamakel Aug 13, 2026
3de7d28
fix(message): restore missing doc comments on public types
senamakel Aug 13, 2026
1494cba
chore: files changed crates/tinybus/src/message/mod.rs
senamakel Aug 13, 2026
bb291ef
fix(message): restore missing doc comments on public items
senamakel Aug 13, 2026
0d264e1
fix(message): restore missing doc comments on public types
senamakel Aug 13, 2026
9277b1c
fix(transport): restore dropped message handling
senamakel Aug 13, 2026
7d20b2e
fix(transport): restore unix socket cleanup on drop
senamakel Aug 13, 2026
1fbddc5
fix(transport): restore unix socket cleanup on drop
senamakel Aug 13, 2026
54fd6ec
fix(transport): restore unix socket cleanup on drop
senamakel Aug 13, 2026
53ac942
fix(transport): restore memory transport after refactor
senamakel Aug 13, 2026
fca5cc6
fix(router): restore handler removal on unsubscribe
senamakel Aug 13, 2026
eeecd1a
fix(router): restore handler removal on unsubscribe
senamakel Aug 13, 2026
2613fc6
fix(router): restore handler removal on unsubscribe
senamakel Aug 13, 2026
28a79ae
fix(router): restore handler removal on unsubscribe
senamakel Aug 13, 2026
93d791b
fix(router): restore handler removal on unsubscribe
senamakel Aug 13, 2026
08ec06e
fix(router): restore handler removal on unsubscribe
senamakel Aug 13, 2026
7947bb2
fix(router): restore handler removal on unsubscribe
senamakel Aug 13, 2026
df4c37a
fix(broker): restore message delivery after subscriber removal
senamakel Aug 13, 2026
4ddb70f
fix(broker): restore message delivery after subscriber removal
senamakel Aug 13, 2026
5fd71dc
fix(broker): restore message delivery after subscriber removal
senamakel Aug 13, 2026
1901c99
fix(broker): restore message delivery after subscriber removal
senamakel Aug 13, 2026
7e37a4d
fix(broker): restore message delivery after subscriber removal
senamakel Aug 13, 2026
3eee0a0
fix(broker): restore message delivery after subscriber removal
senamakel Aug 13, 2026
47f94ce
fix(broker): restore message delivery after subscriber removal
senamakel Aug 13, 2026
dd3eb60
fix(broker): restore message delivery after subscriber removal
senamakel Aug 13, 2026
2ec49c2
chore: remove unused import in lib.rs
senamakel Aug 13, 2026
9c841b5
chore: update lib.rs formatting
senamakel Aug 13, 2026
1dc7916
fix(proxy): restore dropped method call forwarding
senamakel Aug 13, 2026
69bd23b
feat(connection): add attestation query for confidential messages
senamakel Aug 13, 2026
4810be3
fix(broker): restore message delivery after subscriber removal
senamakel Aug 13, 2026
324c4d3
chore: files changed crates/tinybus/src/module/host.rs
senamakel Aug 13, 2026
d762386
fix(host): restore module host shutdown on drop
senamakel Aug 13, 2026
498c14a
fix(broker): restore message delivery after subscriber removal
senamakel Aug 13, 2026
0911619
fix(router): restore handler removal on unsubscribe
senamakel Aug 13, 2026
2aca199
feat(tinybus): add confidential messaging and trust store support
senamakel Aug 13, 2026
0c68549
fix(bus): restore missing newline in usage output
senamakel Aug 13, 2026
5d50141
fix(tinybus): update attach calls and CLI test expectations
senamakel Aug 13, 2026
b1feb12
fix(bus): restore missing newline in usage output
senamakel Aug 13, 2026
1fc86bc
feat(message): add confidential flag to message headers
senamakel Aug 13, 2026
ba4a39f
test(router): cover confidential routing and attestation lifetime
senamakel Aug 13, 2026
ccd5460
test(broker): cover confidential calls and attestation
senamakel Aug 13, 2026
b62740f
chore: apply rustfmt formatting across tinybus crate
senamakel Aug 13, 2026
bd9ab4d
docs(attest): document attestation module usage
senamakel Aug 13, 2026
a4b7057
docs: document confidential message delivery guarantees
senamakel Aug 13, 2026
4bc9be1
docs(protocol): document the confidential message flag
senamakel Aug 13, 2026
c7b1c5a
docs(protocol): document GetAttestation method
senamakel Aug 13, 2026
c3178a5
fix(attest): restore missing attestation verification
senamakel Aug 13, 2026
2c01568
fix(attest): restore missing attestation verification
senamakel Aug 13, 2026
7dc079e
refactor(tinybus): move hash module under module loader
senamakel Aug 13, 2026
92d06e8
fix(attest): restore missing attestation verification
senamakel Aug 13, 2026
d73188c
refactor(transport): remove peer_process attestation hook
senamakel Aug 13, 2026
be34507
refactor(broker): drop transport-level attestation
senamakel Aug 13, 2026
ff2f78c
fix(tests): attest modules directly instead of via trust store
senamakel Aug 13, 2026
ba8221b
fix(broker): restore message delivery after subscriber removal
senamakel Aug 13, 2026
1ee9982
chore(tinybus): remove trust store option from serve command
senamakel Aug 13, 2026
0b5d311
fix(host): drop source field from module attestation
senamakel Aug 13, 2026
99ecf1f
chore(tinybus): remove redundant attestation source field
senamakel Aug 13, 2026
ae17f64
fix(tests): assert attestation stays with the verified peer
senamakel Aug 13, 2026
cf704b7
test(module): cover allowlisted artifact loading with ignored integra…
senamakel Aug 13, 2026
5dedfa1
test(module): use load_file in host allowlist tests
senamakel Aug 13, 2026
c89db91
fix(test): stage modules inside the crate instead of /tmp
senamakel Aug 13, 2026
a08f9c0
docs(attest): document attestation module usage
senamakel Aug 13, 2026
6978e94
docs(protocol): clarify confidential delivery is limited to attested …
senamakel Aug 13, 2026
f7c8a4b
chore(tinybus): remove trailing blank line in transport trait
senamakel Aug 13, 2026
57a6c28
chore(router): gate attestation code behind modules feature
senamakel Aug 13, 2026
18a133a
Merge remote-tracking branch 'upstream/main' into confidential-messages
senamakel Aug 13, 2026
ea2229d
docs(attest): record that bulk streams have no confidential mode
senamakel Aug 13, 2026
59f13e8
fix(error): derive Clone for BusError
senamakel Aug 13, 2026
a9f425b
fix(error): derive Clone for BusError
senamakel Aug 13, 2026
896a9f0
docs(attest): document attestation module usage
senamakel Aug 13, 2026
76d0ce8
fix(error): derive Clone for BusError
senamakel Aug 14, 2026
325d9b3
fix(error): derive Clone for BusError
senamakel Aug 14, 2026
a1bf274
fix(broker): restore message delivery after subscriber removal
senamakel Aug 14, 2026
1d9f58c
docs(attest): document attestation module usage
senamakel Aug 14, 2026
a56cbc4
fix(host_test): restore missing test module
senamakel Aug 14, 2026
0347bc1
fix(broker): restore message delivery after subscriber removal
senamakel Aug 14, 2026
c665d8d
fix(broker): collapse confidential method call condition
senamakel Aug 14, 2026
df8cead
fix(host_test): restore missing test module
senamakel Aug 14, 2026
901a178
fix(host_test): restore missing test module
senamakel Aug 14, 2026
eb7d270
test(host): gate allowlist loader tests to unix
senamakel Aug 14, 2026
a24049f
fix(host_test): restore missing test module
senamakel Aug 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,17 @@ discussion in the pull request:
control story in full.
- **Every call has a deadline.** It cannot be disabled. A call with no deadline
reintroduces the hang that motivated the project.
- **A confidential message goes to a loaded, hash-verified module or to
nobody.** Only a module whose artifact the host hashed against its allowlist
before `dlopen` may receive one; a peer reached across a transport never can,
by design rather than by omission. The message is never fanned out to a
subscriber, never printed by `monitor`, and never carried by a signal. This is
admission control, not isolation — a loaded module is already inside the trust
boundary and could read host memory directly; what the rule buys is that the
bus will not be the delivery mechanism for unverified code. `confidential` is
the one header field the broker does not overwrite on ingress, because it can
only ever restrict the sender's own traffic. See
`docs/modules/attest/README.md`.
- **A misbehaving peer must not affect another peer.** Bounded per-peer queues,
best-effort signal delivery, and an accept loop that survives a bad client are
all this invariant. Any change that lets one peer's slowness reach another's
Expand Down
133 changes: 133 additions & 0 deletions crates/tinybus/src/attest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
//! Recipient attestation: what the broker verified before it will carry a secret.
//!
//! # What a confidential message is for
//!
//! Some payloads are the kind whose disclosure *is* the failure: a private key,
//! a recovery phrase, a bearer token. The recipient of one of those is a
//! **module loaded into the host's address space**, whose artifact the host
//! hashed against the operator's `modules.toml` before `dlopen` ever ran. A
//! secret handed to such a module never crosses a transport, never reaches a
//! separate process, and never touches a socket.
//!
//! Everything else on the bus — services in their own processes, CLI clients,
//! monitors — does not receive secrets. Not "receives them less safely":
//! a confidential message addressed to one is refused. That is the rule, not a
//! platform limitation, which is why there is no peer-identification machinery
//! here and no per-OS code to keep working.
//!
//! # What the check actually establishes
//!
//! An [`Attestation`] records that *this* well-known name is owned by a module
//! whose bytes hashed to *this* SHA-256, and that the operator listed that hash
//! as acceptable. The hash is computed by the host over bytes the host read
//! itself; nothing a module says about itself participates.
//!
//! # What it does not establish, and this matters
//!
//! An in-process module shares the host's address space. It can read host
//! memory directly, so a *malicious loaded module* is not contained by any
//! routing rule — it never needed the bus to reach a secret in the first place.
//! This is the invariant CLAUDE.md already states: in-process modules are
//! inside the trust boundary.
//!
//! What attestation buys is therefore **admission control**, not isolation:
//! only code whose hash an operator allowlisted is loaded at all, and only such
//! code is handed a secret through the bus. The bus's job is to refuse to be
//! the delivery mechanism for anything else. An integration whose compromise
//! must not reach the kernel's secrets belongs in a separate process — where it
//! is, by this design, ineligible to receive them.
//!
//! # Not a signature, yet
//!
//! `modules.toml` is a list of hashes an operator put on disk, so an
//! attestation means "this is the artifact the operator allowlisted", not "a
//! release key vouched for it". Signed release manifests are the natural next
//! layer: verification would produce this same [`Attestation`] and needs no
//! wire-format change.
//!
//! # Feature gating
//!
//! The type and the routing rule that consumes it are always compiled. A slim
//! `--no-default-features` broker still refuses confidential delivery to
//! everything, which is the correct answer for a build that cannot load a
//! module at all — and a silent downgrade is exactly what must not happen.

use serde::{Deserialize, Serialize};

use crate::name::BusName;

/// The host's own record of a recipient it verified.
///
/// Held by the router against the peer, served by the bus's `GetAttestation`,
/// and checked on every confidential delivery. It carries no path: the hash and
/// the name it was verified for are enough to audit the decision, and a
/// filesystem layout is not something to publish on a bus.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Attestation {
/// The well-known name this artifact was verified *for*.
///
/// Bound to the name rather than floating free, because "some allowlisted
/// artifact is loaded" is not the question a sender is asking. The question
/// is whether the code answering to `…Wallet` is what the operator
/// allowlisted for `…Wallet`.
pub name: BusName,
/// Lowercase hex SHA-256 of the artifact the host read at load time.
pub sha256: String,
}

/// Whether `value` is exactly 64 hex digits.
#[cfg(feature = "modules")]
pub(crate) fn is_hex_sha256(value: &str) -> bool {
value.len() == 64 && value.bytes().all(|byte| byte.is_ascii_hexdigit())
}

/// Parse the flat `key = "value"` subset that `modules.toml` is written in.
///
/// Deliberately not a TOML parser. The file is two columns of ASCII that an
/// operator hand-edits, and pulling a parser into the kernel's dependency graph
/// to read it would be precisely the absorption this project exists to stop.
/// Section headers are skipped rather than rejected, so the allowlist can live
/// inside a larger file.
#[cfg(feature = "modules")]
pub(crate) fn parse_allowlist(source: &str) -> impl Iterator<Item = (String, String)> + '_ {
source.lines().filter_map(|line| {
let line = line.split('#').next()?.trim();
if line.is_empty() || line.starts_with('[') {
return None;
}
let (key, value) = line.split_once('=')?;
Some((
key.trim().trim_matches(['"', '\'']).to_string(),
value.trim().trim_matches(['"', '\'']).to_ascii_lowercase(),
))
})
}

#[cfg(all(test, feature = "modules"))]
mod tests {
use super::*;

#[test]
fn an_allowlist_reads_entries_and_ignores_comments_and_sections() {
let source = "# a comment\n[section]\n\"clock.so\" = \"AABB\" # trailing\n\n";
let entries: Vec<_> = parse_allowlist(source).collect();
assert_eq!(entries, vec![("clock.so".to_string(), "aabb".to_string())]);
}

#[test]
fn an_allowlist_line_without_an_assignment_is_skipped_rather_than_guessed_at() {
assert_eq!(parse_allowlist("garbage\n").count(), 0);
}

#[test]
fn only_a_full_length_hex_digest_counts_as_a_hash() {
assert!(is_hex_sha256(
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
));
// Too short, and a plausible-looking typo that must not be accepted as
// a digest — the allowlist is the only thing standing between an
// arbitrary artifact and a private key.
assert!(!is_hex_sha256("e3b0c442"));
assert!(!is_hex_sha256(&"z".repeat(64)));
}
}
27 changes: 22 additions & 5 deletions crates/tinybus/src/bin/tinybus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ enum Command {
/// Positional arguments as a JSON array. Defaults to `[]`.
#[arg(default_value = "[]")]
args: String,
/// Send the body confidentially: the bus refuses to deliver it unless
/// it has verified the destination's artifact itself.
#[arg(long)]
confidential: bool,
},

/// Emit a signal.
Expand Down Expand Up @@ -222,13 +226,18 @@ async fn run(cli: Cli) -> Result<()> {
interface,
member,
args,
confidential,
} => {
let connection = connect(&address).await?;
let args: serde_json::Value = serde_json::from_str(&args)?;
let proxy = connection
.proxy(&destination, &path, &interface)?
.with_timeout(timeout);
let reply: serde_json::Value = proxy.call(&member, args).await?;
let reply: serde_json::Value = if confidential {
proxy.call_confidential(&member, args).await?
} else {
proxy.call(&member, args).await?
};
println!("{}", serde_json::to_string_pretty(&reply)?);
Ok(())
}
Expand Down Expand Up @@ -499,10 +508,16 @@ fn render(message: &tinybus::Message) -> String {
.map(|i| i.to_string())
.unwrap_or_default();
let member = h.member.as_ref().map(|m| m.to_string()).unwrap_or_default();
format!(
"{kind:<6} {sender:<10} {path} {interface}.{member} {}",
message.body
)
// The monitor is a terminal, a scrollback buffer and often a pasted bug
// report. A confidential body must not reach any of them, and the routing
// rules mean one should never arrive here in the first place — so this is
// the second lock on a door that is already shut.
let body = if h.confidential {
"<confidential>".to_string()
} else {
message.body.to_string()
};
format!("{kind:<6} {sender:<10} {path} {interface}.{member} {body}")
}

#[cfg(test)]
Expand Down Expand Up @@ -651,6 +666,7 @@ mod tests {
address: Some(address.clone()),
timeout: 1,
command: Command::Call {
confidential: false,
destination: DESTINATION.into(),
path: PATH.into(),
interface: INTERFACE.into(),
Expand Down Expand Up @@ -781,6 +797,7 @@ mod tests {
interface: INTERFACE.into(),
member: "Echo".into(),
args: "not json".into(),
confidential: false,
},
Command::Emit {
path: PATH.into(),
Expand Down
Loading
Loading