[noise] Variable-length handshake payloads?
David Fifield
david at bamsoftware.com
Wed Dec 12 13:28:40 PST 2018
On Wed, Dec 12, 2018 at 08:39:34PM +0000, Jonas Acres wrote:
> Greetings, I hope this is the right place to ask Noise implementation
> questions.
>
> I am working on a project that uses Noise. We want to:
> 1. Exchange payloads during handshaking (i.e. use the payload fields in
> WriteMessage/ReadMessage),
> 2. Ensure that all transmitted bytes are indistinguishable from random noise,
> and
> 3. Have variable-length payloads.
>
> And #3 is where the problem comes in, because it seems to me that the spec
> indicates that everything in the 'payload' field will be passed to a single
> call of the EncryptAndHash/DecryptAndHash functions. This appears to require
> the caller to know the length of the payload in advance to know when they've
> got the whole ciphertext.
I don't think this is really a problem--the spec doesn't require a
specific on-wire implementation, unless you are talking about something
layered on top, like Noise Pipes. Framing and breaking up into messages
is outside the spec, one of the application's reponsibilities:
https://noiseprotocol.org/noise.html#application-responsibilities
Length fields: Applications must handle any framing or
additional length fields for Noise messages, considering that a
Noise message may be up to 65535 bytes in length. If an explicit
length field is needed, applications are recommended to add a
16-bit big-endian length field prior to each message.
As for encoding that are indistinguishable from random and allow
variable-length fields, there is some prior art. ScrambleSuit is the
first one I'm aware of. It works by sending random padding with a MAC
(also indistinguishable from random) as a termination marker.
https://censorbib.nymity.ch/#Winter2013b
Locating the MAC: The MAC is computationally indistinguishable
from the pseudo-random padding. To facilitate localisation of
the MAC, we place a cryptographic mark M right in front of it.
The mark is defined as M = MAC_k_t(T_t || P).
The obfs4 protocol uses the same basic idea as ScrambleSuit. Here, the
end of the variable-length handshake is M_C.
https://github.com/Yawning/obfs4/blob/master/doc/obfs4-spec.txt
X' = Elligator 2 representative of X (32 bytes)
P_C = Random padding [ClientMinPadLength, ClientMaxPadLength] bytes
M_C = HMAC-SHA256-128(B | NODEID, X')
E = String representation of the number of hours since the UNIX
epoch
MAC_C = HMAC-SHA256-128(B | NODEID, X' | P_C | M_C | E)
clientRequest = X' | P_C | M_C | MAC_C
Apart from one little oversight during the handshake, obfs4 allows
either side to send a packet of any size at any time.
https://lists.torproject.org/pipermail/tor-dev/2017-June/012310.html
https://people.torproject.org/~dcf/obfs4-timing/
ShadowSocks in AEAD mode uses a structure like this:
https://shadowsocks.org/en/spec/AEAD-Ciphers.html
[encrypted payload length][length tag][encrypted payload][payload tag]
The first AEAD encrypt/decrypt operation uses a counting nonce
starting from 0. After each encrypt/decrypt operation, the nonce
is incremented by one... Note that each TCP chunk involves two
AEAD encrypt/decrypt operation: one for the payload length, and
one for the payload. Therefore each chunk increases the nonce
twice.
> Below is a possible workaround that is a deviation from the spec. Here is what
> it looks on the write side:
>
> ---begin pseudocode---
> Let:
> payload be a variable-length byte sequence
> zerolen be a zero-length byte sequence
>
> // encrypt a blank, sort of like in REKEY()
> lengthObfuscator = EncryptAndHash(zerolen)
This seems to be a bug. I think you want to be encrypting a fixed-length
string of zeros, not a zero-length string. EncryptAndHash("") is defined
to return "".
https://noiseprotocol.org/noise.html#the-cipherstate-object
EncryptWithAd(ad, plaintext): If k is non-empty returns
ENCRYPT(k, n++, ad, plaintext). Otherwise returns plaintext.
https://noiseprotocol.org/noise.html#the-symmetricstate-object
EncryptAndHash(plaintext): Sets ciphertext = EncryptWithAd(h,
plaintext), calls MixHash(ciphertext), and returns ciphertext.
Note that if k is empty, the EncryptWithAd() call will set
ciphertext equal to plaintext.
> // calculate what the ciphertext length will be for our plaintext payload
> ctLength = calculateCiphertextLength( len(payload) )
>
> // xor the first two bytes of the ciphertext with the length
> obfuscatedLength = ctLength ^ (lengthObfuscator[0] << 8 | lengthObfuscator[1])
>
> // mix the obfuscated length into the hash, since it will be sent on the wire
> MixHash(obfuscatedLength)
>
> // cat the two together to get a modified payload
> modifiedPayload = obfuscatedLength || EncryptAndHash(payload)
>
> // the modified payload is what is actually sent on the wire at the end of a
> message pattern
> transmit(modifiedPayload)
> ---end pseudocode---
More information about the Noise
mailing list