Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
422b897
fix(noise)!: zero-pad protocol_name <= 32 bytes per Noise § 5.2
jlucaso1 Apr 26, 2026
e76b5fe
test(noise): pin post-init h vector for Noise_XX with WA_CONN_HEADER
jlucaso1 Apr 26, 2026
e1c259b
refactor(consts)!: rename NOISE_START_PATTERN, add IK + XXfallback
jlucaso1 Apr 26, 2026
f48a76c
feat(store): persist cached server cert chain for Noise IK reuse
jlucaso1 Apr 26, 2026
428d92e
refactor(noise)!: split into XX/IK/XXfallback state machines
jlucaso1 Apr 26, 2026
b3b2abb
feat(handshake): wire XX/IK/XXfallback dispatch + invalidation policy
jlucaso1 Apr 26, 2026
bdfacce
test(handshake): assert XX and IK share first-frame prologue
jlucaso1 Apr 26, 2026
d2d442c
test(handshake): in-process integration test of XX/IK/XXfallback orch…
jlucaso1 Apr 26, 2026
70cad9d
docs(handshake): document Noise pattern selection + invalidation policy
jlucaso1 Apr 26, 2026
1ea478b
fix(handshake): tighten classification + new StreamClosed variant
jlucaso1 Apr 26, 2026
8286b8c
fix(sqlite): degrade corrupt server_cert_chain blob instead of failin…
jlucaso1 Apr 26, 2026
657e1e9
docs+test: address review nits in noise lib doc + commands tests
jlucaso1 Apr 26, 2026
ce22c51
fix(handshake): gate IK + cert-chain persistence on registration
jlucaso1 Apr 27, 2026
6ba485a
review: cert chain validity, IK no-rewrite test, post-pair regression…
jlucaso1 Apr 27, 2026
71a8702
fix(handshake): only invalidate IK cache for pre-fallback failures
jlucaso1 Apr 27, 2026
2255869
docs(noise): record Ed25519 cert verification as intentional non-goal
jlucaso1 Apr 27, 2026
a21c797
docs: trim verbose comments and doc-strings across the noise stack
jlucaso1 Apr 27, 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
66 changes: 66 additions & 0 deletions agent_docs/protocol_architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,69 @@ wacore/src/iq/
```

Each feature file contains: constants, enums (`StringEnum`), request/response structs (`ProtocolNode`), `IqSpec` impls, and unit tests.

## Noise Handshake Patterns

Three Noise patterns coexist, mirroring WA Web's `WAWebOpenChatSocket`:

| Pattern | When | State machine | Cost |
| -------------- | ------------------------------------------------------------- | --------------------------- | --------------------------------- |
| **XX** | First connect / pairing / forced fallback | `XxHandshakeState` | 1.5 RTT |
| **IK** | Reconnect with valid cached `serverStaticPub` | `IkHandshakeState` | 1 RTT, ships 0-RTT login payload |
| **XXfallback** | Server rejects in-flight IK (reply has `static != null`) | `XxFallbackHandshakeState` | 1 RTT (reuses already-sent eph.) |

### Selection (`src/handshake.rs::select_pattern`)

```text
ik_failures >= 1 ───────────────────────────────────────► XX
no cached server_cert_chain ─────────────────────────────► XX
leaf.not_after < now OR intermediate.not_after < now ────► XX
otherwise ──────────────────────────────────────────────► IK with leaf.key
```

The counter `Client.ik_handshake_failures: AtomicU32` is per-process and
not persisted (matches WA Web's `K = 0` reset on process start).

### Invalidation policy

| Error | `ik_handshake_failures` | `server_cert_chain` |
| -------------------------------------------------- | ----------------------- | ------------------------------------------------ |
| Transient (timeout, disconnect, transport) | unchanged | unchanged |
| Crypto-fatal during IK (cert MAC, decrypt, proto) | `+= 1` | cleared via `DeviceCommand::ClearServerCertChain`|
| XX or XX-fallback failure | unchanged | unchanged (XX never reads the cache) |
| Any successful handshake | reset to `0` | repopulated (XX, XX-fallback) or kept (IK Continue)|

Distinguishing transient from crypto-fatal is via `HandshakeError::is_transient()`
and `HandshakeError::is_crypto_fatal()`. Getting the classification wrong leads
to either oscillating back to XX needlessly or looping on a stale cache.

### Persisted state (`Device.server_cert_chain`)

`CachedServerCertChain { intermediate, leaf }` with each cert reduced to
`{ key: [u8; 32], not_before: i64, not_after: i64 }`. Mirrors the
storage layout WA Web uses in `PrefsInfoStore.js:setCertificateChain` —
only those fields end up on disk.

Reception-time validation in `verify_server_cert` checks structural
shape, the intermediate issuer-serial pin (must equal `WA_CERT_ISSUER_SERIAL`),
the chain link (`leaf.issuer_serial == intermediate.serial`), and that
`leaf.key` equals the `static` decrypted from the Noise transcript. Full
XEd25519 signature verification against `WA_CERT_PUB_KEY` (matching
`WAVerifyChainCertificateWA6`) is **not yet implemented** and is a
deliberate follow-up — the AEAD MAC of the Noise channel already
authenticates the chain bytes as coming from a server in possession of
the static private key, so this gap is defense in depth. Same gap exists
in whatsmeow and Baileys today.

### Logs (matching WA Web's `[socket]` lines)

```text
[socket] doFullHandshake: openChatSocket send hello
[socket] resumeNoiseHandshake started
[socket] resumeNoiseHandshake send hello
[socket] resumeNoiseHandshake rcv hello
[socket] resumeNoiseHandshake deriving secrets
[socket] resumeNoiseHandshake failed: serverStaticCiphertext not null —
doFallbackHandshake continuing handshake with given server hello
[socket] continueFullHandshakeCore client finish and deriving secrets
```
14 changes: 11 additions & 3 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,14 @@ pub struct Client {
/// Uses an AtomicBool instead of probing the noise_socket mutex to avoid
/// TOCTOU races where `try_lock()` fails due to contention, not disconnection.
is_connected: Arc<AtomicBool>,

/// Per-process counter of consecutive Noise IK handshake failures, scoped
/// to the lifetime of this `Client`. Mirrors `K` in WA Web's
/// `WAWebOpenChatSocket` (`ChatSocket.js`): on the first failure within a
/// process, the next connect skips IK and falls back to XX so a stale
/// cached `serverStaticPublic` doesn't trap us in a loop. Reset to 0 on
/// any successful handshake (XX, IK, or XXfallback).
pub(crate) ik_handshake_failures: Arc<AtomicU32>,
/// Terminal shutdown (process-wide). Fired ONLY by `disconnect()`.
/// Long-lived subscribers that must outlive reconnect cycles (saver,
/// device registry cleanup) subscribe here.
Expand Down Expand Up @@ -732,6 +740,7 @@ impl Client {
is_connecting: Arc::new(AtomicBool::new(false)),
is_running: Arc::new(AtomicBool::new(false)),
is_connected: Arc::new(AtomicBool::new(false)),
ik_handshake_failures: Arc::new(AtomicU32::new(0)),
shutdown_notifier: wacore::runtime::ShutdownNotifier::new(),
connection_shutdown: std::sync::Mutex::new(wacore::runtime::ShutdownNotifier::new()),
last_data_received_ms: Arc::new(AtomicU64::new(0)),
Expand Down Expand Up @@ -1197,11 +1206,10 @@ impl Client {
})??;
debug!("Version fetch and transport connection established.");

let device_snapshot = self.persistence_manager.get_device_snapshot().await;

let noise_socket = match handshake::do_handshake(
self.runtime.clone(),
&device_snapshot,
&self.persistence_manager,
&self.ik_handshake_failures,
transport.clone(),
&mut transport_events,
)
Expand Down
Loading
Loading