Commit Graph
21 Commits
Author SHA1 Message Date
riccardom 89ec00eb20 [client] pqkem: address review findings on convergence, strict parsing and status
Bot review (CodeRabbit, cubic) on PR #7098 surfaced several real defects:

- Strict() parsed the raw env value instead of the normalized one, so a
  mixed-case NB_PQ_MLKEM_STRICT such as "tRuE" silently disabled fail-closed
  mode. Parse the lower-cased value, matching Enabled().
- OnDataPathMessage labeled a data-path answer as "signal" in logs, mislabeling
  every rotation answer. Use the data-path label.
- A failed sendAnswer aborted the whole connection setup, skipping the relay/ICE
  listeners; a transient signalling failure now still brings the local transport
  up (the peer retries the answer).
- processOffer left the reserved exchange slot in place when Respond failed, so
  every retransmission of that offer was dropped forever. Clear the reservation
  on a pre-commit error so a retry can derive again.
- Dropped a no-op time.Since that implied a convergence-latency metric that was
  never recorded, and the now-unused startedAt field.
- The strict-mode status line asserted active blocking even for a peer that is
  simply offline; reword it to state that no PSK is established yet.
- conn_test used t.Fatalf for conditions under test; use assert.
2026-10-05 16:02:33 +02:00
riccardom e0523efe5a [client] pqkem: make exchange logs discriminate offer/answer, role, transport, kind
The per-exchange lifecycle logs were all at LevelTrace (below the default field
level) and did not distinguish a signalling re-bootstrap from a data-path rekey,
which made diagnosing exchange activity in the field guesswork (e.g. telling an
ICE-retry-driven re-bootstrap storm apart from normal KEM rotation).

Promote the two pivotal events — an offer going out and a PSK converging — to
LevelDebug and tag every lifecycle log with four dimensions:
  - msg:  offer vs answer (implicit in the message)
  - role: initiator vs responder for this peer
  - via:  signal vs data-path (threaded into processOffer/processAnswer)
  - kind: bootstrap (a new connection / reconnect) vs rotation (a rekey)

kind is derived from the exchange's AckID (zero = bootstrap) on the responder
side and from viaSignal on the initiator side. No behavioural change.
2026-09-11 15:03:47 +02:00
riccardom aca68853d8 [client] pqkem: enforce exchange roles and re-bootstrap stale signal offers
Two convergence bugs surfaced by the security review:

- Role guard (finding B): processOffer accepted an offer even when we are the
  KEM initiator for the peer, and processAnswer accepted an answer when we are
  the responder. The KEM is unidirectional (initiator offers, responder
  answers), so a role-violating message is anomalous — a desync, a duplicate,
  or an injected/spoofed data-path packet. Processing it derived and committed
  a fresh PSK, overwriting a live one and silently dropping any in-flight
  exchange (whose retry loop then exited without raising a failure or
  re-bootstrapping). Reject offers when we are the initiator and answers when
  we are not; this drops only anomalous traffic and leaves the normal flow
  untouched.

- Re-bootstrap on signal re-negotiation (finding A): SignalOffer was idempotent
  in stateAwaitingRekey too, replaying the frozen bootstrap offer. After the
  responder restarted and lost its state it derived a different PSK from fresh
  material, which our awaitingRekey side then rejected — a permanent desync with
  no recovery (in strict mode the peer stays blocked). Make the idempotency
  apply only while a bootstrap is still in flight (awaitingAnswer); once a PSK
  is derived, a fresh signal offer starts a new exchange so both sides converge.
  The controller-double-offer case the idempotency guarded is already covered by
  ShouldSendBootstrapOffer. Reusing the cached offer also reused the same
  ephemeral keys across exchanges, reducing forward secrecy.

Both paths have a failing-without-the-fix regression test.
2026-09-11 15:03:47 +02:00
riccardom 11555c3139 Addresses CI fixes 2026-09-11 14:48:54 +02:00
riccardom 1dc48bcc06 Addresses CI fixes 2026-09-11 14:48:54 +02:00
riccardom 3ed4b4fb43 Revert "Introduces a forced WG handshake on initial MLKEM bootstrap."
This reverts commit b5a72eca65.
2026-09-11 14:48:54 +02:00
riccardom a78d587e7a Introduces a forced WG handshake on initial MLKEM bootstrap.
To ensure two peers agree on a key, we need asymmetry. one peer is
the controller ("initiator") the other is the "responder".

Otherwise imagine two offers in parallel driving two answers at the same time

   A                   B
   | <----B-OFFER----- |
   | -----A-OFFER----> |
   |                   |
   |                   |
   ---------------------------------
  |****** ICE + WG Handshake ****** |
   ---------------------------------
   |                   |
   | <----B-ANSWER---- |
   | -----A-ANSWER---> |

PSK is derived on receive of offer, so A and B derive different PSKs.
When WG handshake takes place it picks misaligned PSKs.

So we impair the two nodes and only the offer of one of the two (the controller/initiator)
carries the KEM material.

This means that if the responder OFFER/ANSWER comes first, when the controller/initiator's one
completes (and the genuine PSK is shared between A and B, we need to force a new WG handshake with
the proper keys.
2026-09-11 14:48:54 +02:00
riccardom cfaf4f1bab Allow non strict mode 2026-09-11 14:48:54 +02:00
riccardom e1d5551ba5 Discriminate initial from rekey failure 2026-09-11 14:48:54 +02:00
riccardom 562c02fa42 Adds log tracepoints
- Add a trace slog level (NB_PQ_MLKEM_LOG_LEVEL=trace) and move the verbose
  per-exchange lifecycle logs (offer/answer/PSK/ack/rotation) to it, so debug
  stays quiet and troubleshooting is opt-in.
- Stop logging the raw preshared key; drop the temporary pqkem-dbg OnRemoteOffer/
  OnRemoteAnswer probes.
- Demote the per-handshake conn log to trace.
2026-09-11 14:48:54 +02:00
riccardom 7c276ce727 pqkem: apply derived PSK at WG peer-config time (pull) + keep push for rekey 2026-09-11 14:48:54 +02:00
riccardom bac6af349b Homogeneous logs prefix 2026-09-11 14:48:54 +02:00
riccardom 3270fc5e18 Bit of renaming
peer -> peerAddrs
have types for remoteID and localID
t.Close log error
Manager SetTransport -> Start
2026-09-11 14:48:54 +02:00
riccardom ef29e996ce Race fix 2026-09-11 14:48:54 +02:00
riccardom 0aeed6ae5b Removes confirm. Uses next offer to deliver confirmation/ack of previous round
We clock the next Offer initiation to the OnDataPathRekeyed, so we have 2 minutes
ahead of us to do our attempts and stuff before to give up.
On failure, we will know because we will not receive a new answer.. but more importantly
the wg handshake will fail :D
2026-09-11 14:48:54 +02:00
riccardom cec4bd49b4 Leave signal offer/answer as a pull/push operation not as an actual transport 2026-09-11 14:48:54 +02:00
riccardom 2ed0d3bf03 Assume two transports: initial "signal" (control plane) one (no data path established yet) + data path one
Define OnDataPathRekeyed event to transition from control plane path to data plane path over the WG tunnel.

Keep confirm ALWAYS on NEW established WG tunnel (posthandshake with rekeying). We keep an active method
irrelevant of the WG handshake (we might decide that the indirect wg handshake is sufficient in the future).

Optimistic commit on responder(when sending answer), while on initiator we set it on getting the answer
2026-09-11 14:48:54 +02:00
riccardom f8bb816dea Epurate wg refs 2026-09-11 14:48:54 +02:00
riccardom 661926ce3a Collapse Driver and Manager in one.
- Have just one manager => one lock
 - Session state is needed in driver to => we have it available now.
 - Isomorphically align to rosenpass components and functionality

File	Role	                                  rosenpass equivalent
kem.go	primitive pure X25519MLKEM768	          crypto.go/handshake
message.go	Offer/Answer/Confirm + Encode/Decode  messages.go
manager.go	Manager stateful, single lock	      server logic
callbacks.go	WGCallbackHandler (seam output)	  Handler
Transport (interfaccia)	seam trasporto pluggable  Conn
2026-09-11 14:48:54 +02:00
riccardom 0a3dc2b43f [squash] isInitial and answered can be inferred without state variables 2026-09-11 14:48:54 +02:00
riccardom fa3c3ad214 Manages convergence 2026-09-11 14:48:54 +02:00