更多信息可以在技术白皮书中找到。如果只是快速了解一下概况,请继续阅读。

基元

使用以下协议和原语:

无连接协议

任何安全协议都需要保留某些状态,因此有一个初始的非常简单的握手,用于建立用于数据传输的对称密钥。这种握手每隔几分钟发生一次,以便提供旋转密钥以实现完美的前向保密性。它基于时间完成,而不是基于先前数据包的内容,因为它旨在优雅地处理数据包丢失。有一个巧妙的脉冲机制,通过自动检测握手何时过期来确保最新的密钥和握手是最新的,并在需要时重新协商。它对每个主机使用单独的数据包队列,以便它可以最大限度地减少握手期间的数据包丢失,同时为所有客户端提供稳定的性能。

换句话说,你打开设备,其他一切都会自动处理。你无需担心要求设备重新连接、断开连接或重新初始化,或任何此类操作。

以下计时器正在发挥作用:

未来的工作涉及调整REKEY_TIMEOUT使用指数退避。

握手完成后,发起者向响应者发送消息,然后响应者向发起者发送消息,发起者可以发送加密会话数据包,但响应者不能。响应者必须等到收到发起者发送的一个加密会话数据包后才能使用新会话,以便提供密钥确认。因此,在响应者使用新建立的会话收到第一个数据包之前,它必须将数据包排队以便稍后发送,或者使用上一个会话(如果存在且有效)。因此,在发起者收到响应者的响应后,如果它没有立即排队等待发送的数据包,则应发送一个空数据包,以提供此确认。

密钥交换和数据包

WireGuard 使用NoiseNoise_IK的握手,以CurveCPNaCLKEA+SIGMAFHMQVHOMQV的工作为基础。所有数据包均通过 UDP 发送。

密钥交换具有以下优良特性:

如果需要额外的对称密钥加密层(例如,用于后量子抗性),WireGuard 还支持可选的预共享密钥,该密钥混合到公钥加密中。当未使用预共享密钥模式时,下面使用的预共享密钥值被假定为 32 字节的全零字符串。

对于以下数据包的描述,请参考以下函数:

First Message: Initiator to Responder

The initiator sends this message:

msg = handshake_initiation {
    u8 message_type
    u8 reserved_zero[3]
    u32 sender_index
    u8 unencrypted_ephemeral[32]
    u8 encrypted_static[AEAD_LEN(32)]
    u8 encrypted_timestamp[AEAD_LEN(12)]
    u8 mac1[16]
    u8 mac2[16]
}

The fields are populated as follows:

initiator.chaining_key = HASH(CONSTRUCTION)
initiator.hash = HASH(HASH(initiator.chaining_key || IDENTIFIER) || responder.static_public)
initiator.ephemeral_private = DH_GENERATE()
msg.message_type = 1
msg.reserved_zero = { 0, 0, 0 }
msg.sender_index = little_endian(initiator.sender_index)

msg.unencrypted_ephemeral = DH_PUBKEY(initiator.ephemeral_private)
initiator.hash = HASH(initiator.hash || msg.unencrypted_ephemeral)

temp = HMAC(initiator.chaining_key, msg.unencrypted_ephemeral)
initiator.chaining_key = HMAC(temp, 0x1)

temp = HMAC(initiator.chaining_key, DH(initiator.ephemeral_private, responder.static_public))
initiator.chaining_key = HMAC(temp, 0x1)
key = HMAC(temp, initiator.chaining_key || 0x2)

msg.encrypted_static = AEAD(key, 0, initiator.static_public, initiator.hash)
initiator.hash = HASH(initiator.hash || msg.encrypted_static)

temp = HMAC(initiator.chaining_key, DH(initiator.static_private, responder.static_public))
initiator.chaining_key = HMAC(temp, 0x1)
key = HMAC(temp, initiator.chaining_key || 0x2)

msg.encrypted_timestamp = AEAD(key, 0, TAI64N(), initiator.hash)
initiator.hash = HASH(initiator.hash || msg.encrypted_timestamp)

msg.mac1 = MAC(HASH(LABEL_MAC1 || responder.static_public), msg[0:offsetof(msg.mac1)])
if (initiator.last_received_cookie is empty or expired)
    msg.mac2 = [zeros]
else
    msg.mac2 = MAC(initiator.last_received_cookie, msg[0:offsetof(msg.mac2)])

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_response {
    u8 message_type
    u8 reserved_zero[3]
    u32 sender_index
    u32 receiver_index
    u8 unencrypted_ephemeral[32]
    u8 encrypted_nothing[AEAD_LEN(0)]
    u8 mac1[16]
    u8 mac2[16]
}

The fields are populated as follows:

responder.ephemeral_private = DH_GENERATE()
msg.message_type = 2
msg.reserved_zero = { 0, 0, 0 }
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.hash = HASH(responder.hash || msg.unencrypted_ephemeral)

temp = HMAC(responder.chaining_key, msg.unencrypted_ephemeral)
responder.chaining_key = HMAC(temp, 0x1)

temp = HMAC(responder.chaining_key, DH(responder.ephemeral_private, initiator.ephemeral_public))
responder.chaining_key = HMAC(temp, 0x1)

temp = HMAC(responder.chaining_key, DH(responder.ephemeral_private, initiator.static_public))
responder.chaining_key = HMAC(temp, 0x1)

temp = HMAC(responder.chaining_key, preshared_key)
responder.chaining_key = HMAC(temp, 0x1)
temp2 = HMAC(temp, responder.chaining_key || 0x2)
key = HMAC(temp, temp2 || 0x3)
responder.hash = HASH(responder.hash || temp2)

msg.encrypted_nothing = AEAD(key, 0, [empty], responder.hash)
responder.hash = HASH(responder.hash || msg.encrypted_nothing)

msg.mac1 = MAC(HASH(LABEL_MAC1 || initiator.static_public), msg[0:offsetof(msg.mac1)])
if (responder.last_received_cookie is empty or expired)
    msg.mac2 = [zeros]
else
    msg.mac2 = MAC(responder.last_received_cookie, msg[0:offsetof(msg.mac2)])

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 and responder for sending and receiving data:

temp1 = HMAC(initiator.chaining_key, [empty])
temp2 = HMAC(temp1, 0x1)
temp3 = HMAC(temp1, temp2 || 0x2)
initiator.sending_key = temp2
initiator.receiving_key = temp3
initiator.sending_key_counter = 0
initiator.receiving_key_counter = 0

temp1 = HMAC(responder.chaining_key, [empty])
temp2 = HMAC(temp1, 0x1)
temp3 = HMAC(temp1, temp2 || 0x2)
responder.receiving_key = temp2
responder.sending_key = temp3
responder.receiving_key_counter = 0
responder.sending_key_counter = 0

And then all previous chaining 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
    u8 reserved_zero[3]
    u32 receiver_index
    u64 counter
    u8 encrypted_encapsulated_packet[]
}

The fields are populated as follows:

msg.message_type = 4
msg.reserved_zero = { 0, 0, 0 }
msg.receiver_index = little_endian(responder.sender_index)
encapsulated_packet = encapsulated_packet || zero 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.

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 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 loses 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.

Furthermore, computing the DH() function is CPU intensive. In order to fend off a CPU-exhaustion attack, if the server is under load, it may choose to not process handshake messages, but instead respond with a cookie reply packet. In order for the server to remain silent unless it receives a valid packet, while under load, all messages are required to have a MAC that combines the receiver's public key and optionally the PSK as the MAC key. When the server is under load, it will only accept packets that additionally have a second MAC of the prior bytes of the message that utilize the cookie as the MAC key. We therefore compute msg.mac1 and msg.mac2 as seen in the handshake messages above. Cookies expire after two minutes and are a MAC of the sender's IP address using a changing (every two minutes) server secret as the MAC key. This allows for proof of IP ownership, which can then be rate limited properly. The server, after computing these MACs as well and comparing them to the ones received in the message, must reject messages with an invalid msg.mac1 and when under load must reject messages with an invalid msg.mac2.

As mentioned above, when a message with a valid msg.mac1 is received, but msg.mac2 is all zeros or invalid and the server is under load, the server may send a cookie reply packet as follows:

msg = packet_cookie_reply {
    u8 message_type
    u8 reserved_zero[3]
    u32 receiver_index
    u8 nonce[24]
    u8 encrypted_cookie[AEAD_LEN(16)]
}

msg.message_type = 3
msg.reserved_zero = { 0, 0, 0 }
msg.receiver_index = little_endian(initiator.sender_index)
msg.nonce = RAND(24)
cookie = MAC(responder.changing_secret_every_two_minutes, initiator.ip_address)
msg.encrypted_cookie = XAEAD(HASH(LABEL_COOKIE || responder.static_public), msg.nonce, cookie, last_received_msg.mac1)

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 and a window of roughly 2000 prior values, checked after verifying the authentication tag. This avoids replay attacks while ensuring nonces are never reused and that UDP can maintain out-of-order delivery performance.

DiffServ Considerations

The "DiffServ" bits in an IP packet are generally split into two portions: one describing the quality of service, via the DSCP value, and the other containing bits used for Explicit Congestion Notification (ECN). All handshake packets have a DSCP value of 0x88 (AF41), so that these packets are the least likely to be dropped, as they're essential for the control functionality of the tunnel, and the ECN is set to 00. All transport data packets have a DSCP value of 0, because the DSCP value of the inner packet is never copied to the outer packet, so that we don't leak information about the data inside the encrypted inner packet. However, we do copy the ECN bits to and from the inner packets, in accordance with the logic described in RFC6040.