[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