Problem
The hybrid (caBLE) transport in transport/cable handles the common QR-initiated flow, but four behaviours required by the CTAP hybrid / caBLE v2 spec are missing or broken, across tunnel connect handling and L2CAP framing.
Why it matters
These break interop with real authenticators and platforms. Redirecting tunnel servers are unreachable, dead linking records are retried forever, clean shutdown and late linking updates are lost, and some direct-BLE messages get corrupted.
What to do
1. Follow HTTP redirects on tunnel connect
tunnel::connect() (libwebauthn/src/transport/cable/tunnel.rs) calls tokio_tungstenite::connect_async(), which doesn't follow redirects, so any 3xx becomes ConnectionFailed. The spec requires following them.
2. Handle HTTP 410 Gone on state-assisted contact
connect() collapses every non-101 status into ConnectionFailed. For CableTunnelConnectionType::KnownDevice (/cable/contact/{contact_id}), a 410 means the linking record is permanently gone, but nothing deletes it, so the dead link is retried forever.
3. Send Shutdown on close and linger for late linking data
CableChannel::close() (channel.rs) is an empty TODO and Drop just aborts. We handle inbound CableTunnelMessageType::Shutdown (protocol.rs, RecvOutcome::PeerShutdown) but never send one, and tearing down right after the CTAP response drops any post-transaction Update linking message.
4. Fix ambiguous CRLF framing over L2CAP
l2cap.rs ends each message with EOM = [0x0D, 0x0A] and split_next_message() splits at the first CRLF, but binary Noise/AES-GCM payloads can contain 0x0D 0x0A. For a 700-1400 byte ciphertext that is a ~1-2% chance of an internal CRLF corrupting the frame. The split_handles_binary_payload test only covers payloads with no internal CRLF.
Problem
The hybrid (caBLE) transport in
transport/cablehandles the common QR-initiated flow, but four behaviours required by the CTAP hybrid / caBLE v2 spec are missing or broken, across tunnel connect handling and L2CAP framing.Why it matters
These break interop with real authenticators and platforms. Redirecting tunnel servers are unreachable, dead linking records are retried forever, clean shutdown and late linking updates are lost, and some direct-BLE messages get corrupted.
What to do
1. Follow HTTP redirects on tunnel connect
tunnel::connect()(libwebauthn/src/transport/cable/tunnel.rs) callstokio_tungstenite::connect_async(), which doesn't follow redirects, so any 3xx becomesConnectionFailed. The spec requires following them.Locationtarget before the WebSocket upgrade, with a redirect cap.Sec-WebSocket-Protocol: fido.cable, andX-caBLE-Client-Payloadfor known-device connections, on the redirected request.2. Handle HTTP 410 Gone on state-assisted contact
connect()collapses every non-101 status intoConnectionFailed. ForCableTunnelConnectionType::KnownDevice(/cable/contact/{contact_id}), a 410 means the linking record is permanently gone, but nothing deletes it, so the dead link is retried forever.TransportErrorvariant exists yet (transport/error.rs).CableKnownDeviceInfoStore::delete_known_deviceand stop retrying.connect_data_channel()inconnection_stages.rsneeds store access (today it is only in the post-handshakeconnection()path).3. Send Shutdown on close and linger for late linking data
CableChannel::close()(channel.rs) is an empty TODO andDropjust aborts. We handle inboundCableTunnelMessageType::Shutdown(protocol.rs,RecvOutcome::PeerShutdown) but never send one, and tearing down right after the CTAP response drops any post-transactionUpdatelinking message.close(), send aShutdownframe over the encrypted channel before dropping it.Updatelinking data.4. Fix ambiguous CRLF framing over L2CAP
l2cap.rsends each message withEOM = [0x0D, 0x0A]andsplit_next_message()splits at the first CRLF, but binary Noise/AES-GCM payloads can contain0x0D 0x0A. For a 700-1400 byte ciphertext that is a ~1-2% chance of an internal CRLF corrupting the frame. Thesplit_handles_binary_payloadtest only covers payloads with no internal CRLF.0x0D 0x0A.