[noise] Boiled down noise implementation spec

Jason A. Donenfeld Jason at zx2c4.com
Mon Aug 31 09:38:29 PDT 2015


Hi,

I'm using noise for a very specific purpose, which involves the
HandshakeIS. I've hence boiled down the noise spec to only the parts that
are relevant to me, while still trying to remain as close as possible to
the spec. Below is what I have. Note that a few days I ago I flirted with
being considerably divergent from noise-proper, and sent that spec. Today's
spec is much closer, being about 99% in line with noise (I hope).

Here's where it differs:

   - Little endian everywhere (except for timestamps, where djb's TAI64N's
   big endianness has the nice quality of being memcmp()able).
   - My prologue is a 1 byte message type, and then two 2byte
   sender/receiver indices. I think the noise spec allows for this kind of
   extension though.
   - Nonces for transport messages begin at 1, not at 0! This is because I
   take advantage of x86 primitives for "increment and return" as an atomic
   operation, so I don't need locks for preventing nonce-reuse. I realize that
   I could probably always just subtract one from all "increment and return"
   atomic operations, and at the higher end, the wrap around of unsigned
   integers would have the correct behavior, but right now I'm not that
   courageous. If somebody wants to convince me that such a subtraction
   <<really is okay to do>> I'm all ears to the encouragement.
   - I use blake2b instead of HMAC-SHA2-256.
   - My payload messages don't have a length param, because the Poly MAC
   prevents against tampering with the encrypted contents, and encrypted is an
   IP packet, which usually knows its own semantics, and also encapsulating
   all of this is a UDP packet anyway.
   - I don't have a length field in handshake messages, because they're
   fixed size.


Let me know what you think, and if there are places where this actually
does diverge from the noise spec that I did not anticipate.

Jason

For the following packet descriptions, refer to these functions:

   - DH(private key, public key) = Curve25519-point-multiplication(private
   key, public key) returning 32 bytes of output
   - DH_GENERATE() = generate Curve25519 private key returning 32 bytes of
   output
   - DH_PUBKEY(private key) = calculate Curve25519 public key from private
   key returning 32 bytes of output
   - AEAD(key, counter, plain text, auth text) =
   ChaCha20Poly1305-RFC7539(key, nonce, plain text, auth text) with noncebeing
   composed of 32 bits of zeros followed by the 64-bit little-endian value of
   counter
   - AEAD_LEN(plain len) = plain len + 16
   - GETKEY(key, counter) = ChaCha20(32 bytes of zeros, key, nonce) with
   nonce being composed of 32 bits of zeros followed by the 64-bit
   little-endian counter
   - KDF(key, input) = Blake2b(key, input) returning 32 bytes of output
   - HASH(input) = Blake2b(input) returning 32 bytes of output
   - TAI64N() = TAI64N timestamp of current time which is 12 bytes

First Message: Initiator to Responder

The initiator sends this message:

msg = handshake_initiation {
    u8 message_type
    u16 sender_index
    u8 unencryped_ephemeral[32]
    u8 encrypted_static[AEAD_LEN(32)]
    u8 encrypted_timestamp[AEAD_LEN(12)]
}

The fields are populated as follows:

initiator.key = HASH("WireGuard-zx2c4-20150827") (or maybe some other
fixed string; it doesn't matter)
initiator.hash = HASH(responder.static_public)
initiator.ephemeral_private = DH_GENERATE()
msg.message_type = 1
msg.sender_index = little_endian(initiator.sender_index)
msg.unencrypted_ephemeral = DH_PUBKEY(initiator.ephemeral_private)
initiator.key = KDF(GETKEY(initiator.key, 0),
DH(initiator.ephemeral_private, responder.static_public))
msg.encrypted_static = AEAD(initiator.key, 0, initiator.static_public,
initiator.hash)
initiator.hash = HASH(initiator.hash || initiator.static_public)
initiator.key = KDF(GETKEY(initiator.key, 0),
DH(initiator.static_private, responder.static_public))
stamp = TAI64N()
msg.encrypted_timestamp = AEAD(initiator.key, 0, stamp, initiator.hash)
initiator.hash = HASH(initiator.hash || stamp)

When the responder receives this message, he decrypts and does all the
above operations in reverse, so that the state is identical.
Second Message: Responder to Initiator

The responder sends this message, after processing the first message above
and applying the same operations to arrive at an identical state:

msg = handshake_initiation {
    u8 message_type
    u16 sender_index
    u16 receiver_index
    u8 unencrypted_ephemeral[32]
    u8 encrypted_nothing[AEAD_LEN(0)]
}

The fields are populated as follows:

responder.ephemeral_private = DH_GENERATE()
msg.message_type = 2
msg.sender_index = little_endian(responder.sender_index)
msg.receiver_index = little_endian(initiator.sender_index)
msg.unencrypted_ephemeral = DH_PUBKEY(responder.ephemeral_private)
responder.key = KDF(GETKEY(responder.key, 1),
DH(responder.ephemeral_private, initiator.ephemeral_public))
responder.key = KDF(GETKEY(responder.key, 0),
DH(responder.ephemeral_private, initiator.static_public))
msg.encrypted_nothing = AEAD(responder.key, 0, [empty], responder.hash)

When the initiator receives this message, he decrypts and does all the
above operations in reverse, so that the state is identical.
Data Keys Derivation

After the above two messages have been exchanged, keys are calculated by
the initiator for sending and receiving data:

initiator.sending_key = GETKEY(initiator.key, 1)
initiator.sending_key_counter = 0
initiator.receiving_key = GETKEY(initiator.key, 2)
initiator.receiving_key_counter = 0

responder.receiving_key = GETKEY(responder.key, 1)
responder.receiving_key_counter = 0
responder.sending_key = GETKEY(responder.key, 2)
responder.sending_key_counter = 0

And then all previous keys, ephemeral keys, and hashes are zeroed out.
Subsequent Messages: Exchange of Data Packets

The initiator and the responder exchange this packet for sharing
encapsulated packet data:

msg = packet_data {
    u8 message_type
    u16 receiver_index
    u64 counter
    u8 encrypted_encapsulated_packet[]
}

The fields are populated as follows:

msg.message_type = 3
msg.receiver_index = little_endian(responder.sender_index)
encapsulated_packet = encapsulated_packet || random padding in order
to make the length a multiple of 16
counter = ++initiator.sending_key_counter
msg.counter = little_endian(counter)
msg.encrypted_encapsulated_packet = AEAD(initiator.sending_key,
counter, encapsulated_packet, [empty])

The responder uses his responder.receiving_key to read the message.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://moderncrypto.org/mail-archive/noise/attachments/20150831/ba8acbc2/attachment.html>


More information about the Noise mailing list