From david at bamsoftware.com Mon Feb 14 17:46:18 2022 From: david at bamsoftware.com (David Fifield) Date: Mon, 14 Feb 2022 18:46:18 -0700 Subject: [noise] Champa tunnel; per-packet Noise Message-ID: <20220215014618.zgw76htnxoy3muqf@bamsoftware.com> Last year we had a discussion about different ways to layer Noise with a reliable stream protocl built upon an unreliable datagram protocol. https://moderncrypto.org/mail-archive/noise/2021/002109.html 1. "Noise over streams". Use the insecure datagram protocol to build a stream abstraction, then encrypt successive chunks of the stream as Noise messages (with, e.g., length prefixes). Like TLS over TCP. 2. "Streams over Noise". Encrypt each datagram separately as a Noise message, with an attached explicit nonce, as in https://noiseprotocol.org/noise.html#out-of-order-transport-messages. Build the stream abstraction over these cryptographically protected datagrams. Like QUIC. The conversation last time was in the context of a DNS tunnel that used approach (1). Because of the limited space for encoding messages in DNS queries, it is relatively costly to attach a nonce and authentication tag to every packet. More about the crypto layering in the DNS tunnel: https://www.bamsoftware.com/software/dnstt/protocol.html#crypto Now I'm working on another tunnel, Champa, which uses an AMP cache as a proxy. An AMP cache can be thought of as an HTTP proxy that requires a specific, restricted HTML encoding. The information capacity per transaction is a lot less restricted than DNS, but otherwise the communication model is similar: you don't get a long-lived, reliable underlying channel; instead you need to make a session abstraction out of a sequence of independent HTTP request–response pairs. To do this, I'm using the same idea as before: have the HTTP requests and responses contain packets of a session/reliability protocol, encapsulated and encoded to conform with AMP cache requirements. I had initially implemented Champa using layering approach (1), like the DNS tunnel. But now I've rearchitected it to use approach (2), and this is what I'd like to invite comment on. The existing protocol layering was: Noise confidentiality and integrity KCP/smux sequencing/reliability AMP/HTML network The new changes push Noise lower in the stack, closer to the "network" layer: KCP/smux sequencing/reliability Noise confidentiality and integrity AMP/HTML network Here are the code changes: https://repo.or.cz/champa.git/log/3e594d4520c601c991c9121481bd7941cc142b11?hp=136966fc0b0f41897bb64945d43487c0d2b19fd9 https://repo.or.cz/champa.git/commitdiff/3e594d4520c601c991c9121481bd7941cc142b11?hp=136966fc0b0f41897bb64945d43487c0d2b19fd9 The main data structure change is the removal of a noise.socket type and its replacement by a noise.Session type. https://repo.or.cz/champa.git/commitdiff/3e594d4520c601c991c9121481bd7941cc142b11 https://repo.or.cz/champa.git/blob/3e594d4520c601c991c9121481bd7941cc142b11:/noise/session.go Where noise.socket could synchronously exchange handshake messages over an already established stream, noise.Session manages different message types (Init, Resp, Transport, as in Wireguard) and breaks the handshake into steps per message received or sent. The initiator calls InitiateHandshake to get an Init message and a half-constructed "PreSession". After receiving the responder's Resp message, the initiator calls FinishHandshake on the PreSession to promote it to a full Session. pre, initMsg, err := InitiateHandshake(nil, pubkey) // err check // send initMsg to the responder // receive respMsg from the responder session, err := pre.FinishHandshake(respMsg) // err check The responder calls AcceptHandshake to immediately get a full Session and a Resp message to send back to the initiator: // receive initMsg from the initiator session, respMsg, err := AcceptHandshake(nil, initMsg, privkey) // err check // send respMsg to the initiator The Encrypt and Decrypt methods of noise.Session use the convention that the first 8 bytes of a buffer are an explicit nonce for the actual Noise message in the remainder. There is a replayWindow type that keeps track of the highest correctly authenticated nonce yet seen from the peer, and a bitmap of already uses nonces within a recent window. A nonce is valid if it is higher than the highest nonce so far, or if it is within the recent window and not used yet. https://repo.or.cz/champa.git/blob/3e594d4520c601c991c9121481bd7941cc142b11:/noise/replay.go The tunnel client has a simple noiseDial function that does the handshake exchange. There is not yet any provision for retransmitting handshake messages. https://repo.or.cz/champa.git/blob/3e594d4520c601c991c9121481bd7941cc142b11:/champa-client/main.go#l36 The tunnel server is more complicated, since it must handle sessions from many clients at once. On receiving an Init message from a client for which no session yet exists, it sends back a Resp message, creates a session for that client, and processes future Transport messages in that session. The Noise session is removed after the termination of the underlying KCP/smux session, which is driven by an idle timeout. https://repo.or.cz/champa.git/blob/3e594d4520c601c991c9121481bd7941cc142b11:/champa-server/main.go#l313