[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