[noise] Noise-inspired protocol based on HandshakeIK (noh2's Handshake IS)

Jason A. Donenfeld Jason at zx2c4.com
Thu Aug 27 05:37:38 PDT 2015


Hi,

I was hoping you could have a look at this noise-inspired handshake. It's
extremely similar, though it avoids the use of aad and shortens some
things. Please let me know if parts seem insecure or silly.

Thanks,
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
   - AE0(key, plain text) = ChaCha20Poly1305-RFC7539(key, nonce, plain text)
    with nonce being composed of 96bits of zeros
   - AE(key, counter, plain text) = little_endian(counter) ||
   ChaCha20Poly1305-RFC7539(key, nonce, plain text) with nonce being
   composed of 32bits of zeros followed by the 64bit little-endian counter
   - AE0_LEN(plain len) = plain len + 16
   - AE_LEN(plain len) = plain len + 16 + 8
   - GETKEY(key, counter) = ChaCha20(32 bytes of zeros, key, nonce) with
   nonce being composed of 32bits of zeros followed by the 64bit
   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 encryped_ephemeral[AE0_LEN(32)]
    u8 encrypted_static[AE0_LEN(32)]
    u8 encrypted_timestamp[AE0_LEN(12)]
}

The fields are populated as follows:

initiator.ephemeral_private = DH_GENERATE()
msg.message_type = 1
msg.sender_index = little_endian(initiator.sender_index)
initiator.key = HASH("WireGuard-zx2c4-20150827" || 0x0 ||
msg.sender_index || 0x0 || HASH(responder.static_public))
msg.encrypted_ephemeral = AE0(initiator.key,
DH_PUBKEY(initiator.ephemeral_private))
initiator.key = KDF(GETKEY(initiator.key, 1),
DH_PUBKEY(initiator.ephemeral_private))
initiator.key = KDF(GETKEY(initiator.key, 0),
DH(initiator.ephemeral_private, responder.static_public))
msg.encrypted_static = AE0(initiator.key, initiator.static_public)
initiator.key = KDF(GETKEY(initiator.key, 1), initiator.static_public)
initiator.key = KDF(GETKEY(initiator.key, 0),
DH(initiator.static_private, responder.static_public))
msg.encrypted_timestamp = AE0(initiator.key, TAI64N())
initiator.key = KDF(GETKEY(initiator.key, 1), msg)

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 encrypted_ephemeral[AE0_LEN(32)]
}

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)
responder.key = KDF(GETKEY(responder.key, 0), msg.sender_index ||
msg.receiver_index)
msg.encrypted_ephemeral = AE0(responder.key,
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))
responder.key = KDF(GETKEY(responder.key, 0), msg)

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, 0)
initiator.sending_key_counter = 0
initiator.receiving_key = GETKEY(initiator.key, 1)
initiator.receiving_key_counter = 0

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

And then all previous keys and ephemeral keys 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
    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
msg.encrypted_encapsulated_packet = AE(initiator.sending_key,
++initiator.sending_key_counter, encapsulated_packet)

The responder uses his responder.receiving_key to read the message.
DoS Mitigation

We require authentication in the first handshake message sent because it
does not require allocating any state on the server for potentially
unauthentic messages. In fact, the server does not even *respond at all* to
an unauthorized client; it is silent and invisible. The handshake avoids a
denial of service vulnerability created by allowing any state to be created
in response to packets that have not yet been authenticated.

This, however, introduces the issue of having authentication in the first
packet: it is always open to a replay attack. An attacker could replay
initial handshake messages to trick the server into regenerating its
ephemeral key, thereby disconnecting the legitimate client connection
(though not affecting the security of any messages). For that reason, we
include a TAI64N <http://cr.yp.to/libtai/tai64.html> timestamp in the first
message. The server keeps track of the greatest timestamp received *per
client* and discards packets containing timestamps less than or equal to
it. If the server restarts and looses this state, that is not a problem: an
initial packet from earlier can be replayed, but it could not possibly
disrupt any ongoing sessions, since the server has just restarted. Once
clients reconnect to the server after its restart, they will be using
greater timestamps, invalidating the previous ones. This timestamp ensures
that an attacker can't disrupt a current session between client and server.
Nonce Reuse & Replay Attacks

Nonces are never reused. A 64bit counter is used, and cannot be wound
backward. UDP, however, sometimes delivers messages out of order. For that
reason we use a sliding window, in which we keep track of the
*greatest* counter
received, as well as the sizeof(unsigned long) * 8possible counter values
possibly preceding the greatest one. This avoids replay attacks while
ensuring nonces are never reused and that UDP can maintain out-of-order
delivery performance.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://moderncrypto.org/mail-archive/noise/attachments/20150827/cd09f5dc/attachment.html>


More information about the Noise mailing list