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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 10 additions & 20 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,17 @@ archived by series under [docs/changelog/](docs/changelog/); see the
The control frame of the membership exchange
([ADR 0037](docs/adr/0037-an-applications-users-are-visible-only-to-that-application.md))
is reserved and consumed on receipt, never delivered as text or answered,
ahead of the exchange itself. A build that does not reserve it delivers the
frame to its application as text, so this release is the floor for sharing a
radio neighbourhood cleanly. `offline-protocol-sealed` gains `membership`:
the certificate, the two exchange frames and the end-to-end proof, each at
one fixed size, with their signing payloads and key derivation inputs,
pinned by `app-membership-v1.vectors.json`
ahead of the exchange itself. A build that does not reserve it rejects the
frame with a security warning, or shows it as text when `require_encryption`
is off, so this release is the floor for sharing a radio neighbourhood
cleanly. `offline-protocol-sealed` gains `membership`: the certificate, the
two exchange frames and the end-to-end proof, each at one fixed size, with
their signing payloads and key derivation inputs, pinned by
`app-membership-v1.vectors.json`
([control messages](docs/spec/control-messages.md#application-membership)).
Nothing is gated on membership yet.

- **Every node runs the application membership exchange.** On discovering a
neighbour over BLE or Wi-Fi Direct, a node sends a `__MEMBER__` hello and
answers the neighbour's, always at the same two sizes, whether or not it is
configured
([ADR 0037](docs/adr/0037-an-applications-users-are-visible-only-to-that-application.md)).
`ProtocolConfig.app_publisher_key` and `OfflineProtocol::set_app_membership`
(Rust only for now) make a node a member: two members of one application
learn that they are, and every other pair learns nothing. Nothing is gated
on it yet, and the bindings do not expose it yet. An older build that does
not reserve `__MEMBER__` shows each hello to its application as text.
The end-to-end proof's sealing key now has its own label,
`offline-app-member-e2e-key-v1`, apart from the signing domain.
The end-to-end proof's sealing key has its own label,
`offline-app-member-e2e-key-v1`, apart from the signing domain. No node
sends the frame: the exchange itself is held back, and ADR 0037 is parked.

- **A browser node's start can be aborted.** `OfflineNode.start({ signal })`
ends the worker of a start that has not finished, and with it the
Expand Down
18 changes: 4 additions & 14 deletions crates/offline-protocol-uniffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8853,18 +8853,6 @@ mod tests {
use std::sync::Arc;
use std::thread;

/// The next frame in the Wi-Fi Direct slot that is not a membership
/// frame. Every engine says hello to a neighbour it discovers
/// (ADR 0037), so a test that asks what discovery or a send queued looks
/// past those.
fn next_wifi_direct_frame_past_membership(
engine: &OfflineProtocol,
) -> Option<WifiDirectMessage> {
let marker = offline_protocol_sealed::prefixes::MEMBER.as_bytes();
std::iter::from_fn(|| engine.wifi_direct_get_next_message())
.find(|m| !m.data.windows(marker.len()).any(|w| w == marker))
}

/// Every `DataStore` method the UDL declares must be reachable from
/// React Native, in all four places a method has to appear.
///
Expand Down Expand Up @@ -11575,7 +11563,8 @@ mod tests {
);

a.wifi_direct_status_changed(true).unwrap();
let outbound = next_wifi_direct_frame_past_membership(&a)
let outbound = a
.wifi_direct_get_next_message()
.expect("the key package goes out once the layer is up");
assert_eq!(outbound.recipient_id, b_address);

Expand Down Expand Up @@ -11712,7 +11701,8 @@ mod tests {
None,
)
.unwrap();
let outbound = next_wifi_direct_frame_past_membership(&a)
let outbound = a
.wifi_direct_get_next_message()
.expect("the registered transport queued the message");
assert_eq!(
outbound.recipient_id, b_address,
Expand Down
14 changes: 7 additions & 7 deletions crates/offline-protocol/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ test-utils = []
# keystore. Default on, like `data`, so the FFI entry that exposes them cannot
# become a checksum mismatch; a mobile build that never calls them opts out
# with `default-features = false` and drops the HKDF, HMAC and lock code.
file-store = ["dep:rustix"]
file-store = ["dep:hkdf", "dep:hmac", "dep:rustix"]

[dependencies]
offline-protocol-core = { workspace = true }
Expand Down Expand Up @@ -96,12 +96,12 @@ sha2 = "0.10"
chacha20poly1305 = { workspace = true }
rand_core = { workspace = true }
zeroize = { workspace = true }
# The application membership exchange (ADR 0037) derives its keys with HKDF
# and tags its hello with HMAC, on every build. The built-in sealed file store
# uses both too, for its sealing and naming keys. Both are already in the
# graph through the transport crate.
hkdf = { workspace = true }
hmac = { workspace = true }
# The built-in sealed file store for MLS material derives its sealing and
# naming keys from the operator's store key (HKDF) and names its files with a
# keyed digest (HMAC), so a stolen directory does not confirm which peers and
# groups it holds. Both are already in the graph through the transport crate.
hkdf = { workspace = true, optional = true }
hmac = { workspace = true, optional = true }

# The built-in file stores hold an exclusive `flock` on their directory for
# their lifetime, so a second store over it is refused rather than diverging
Expand Down
26 changes: 0 additions & 26 deletions crates/offline-protocol/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -945,24 +945,6 @@ pub struct ProtocolConfig {

/// Service subscriptions; see [`ServiceSubscriptionConfig`].
pub service_subscriptions: ServiceSubscriptionConfig,

/// This application's publisher public key, an Ed25519 key of 32 bytes,
/// or `None` (the default).
///
/// With a key, this node runs the membership exchange of [ADR 0037] as a
/// member of the application the key and [`Self::app_id`] name: it
/// derives the application key from both, and a neighbour that presents
/// a certificate signed by this key, for its own address, becomes
/// visible. Without one the node still runs the exchange, answering every
/// neighbour with random bytes of the same sizes, so a configured node and
/// an unconfigured one look alike on the air.
///
/// The engine never holds the publisher's private key. A member's own
/// certificate comes from
/// [`crate::OfflineProtocol::set_app_membership`].
///
/// [ADR 0037]: https://github.com/Offline-Protocol/offline-protocol-sdk/blob/main/docs/adr/0037-an-applications-users-are-visible-only-to-that-application.md
pub app_publisher_key: Option<[u8; 32]>,
}

impl ProtocolConfig {
Expand All @@ -988,7 +970,6 @@ impl ProtocolConfig {
data: DataConfig::default(),
custody: CustodyConfig::default(),
service_subscriptions: ServiceSubscriptionConfig::default(),
app_publisher_key: None,
}
}

Expand Down Expand Up @@ -1473,13 +1454,6 @@ impl ProtocolConfigBuilder {
self
}

/// Sets this application's publisher public key
/// ([`ProtocolConfig::app_publisher_key`]).
pub fn app_publisher_key(mut self, key: [u8; 32]) -> Self {
self.config.app_publisher_key = Some(key);
self
}

/// Enables or disables BLE transport.
pub fn ble_enabled(mut self, enabled: bool) -> Self {
self.config.transport.ble_enabled = enabled;
Expand Down
Loading
Loading