更多信息可以在技术白皮书中找到。如果只是快速了解一下概况,请继续阅读。
基元
使用以下协议和原语:
- ChaCha20用于对称加密,使用Poly1305进行身份验证,使用RFC7539 的 AEAD 构造
- ECDH 的Curve25519
- BLAKE2s用于散列和密钥散列,在RFC7693中描述
- SipHash24用于哈希表键
- 用于密钥派生的HKDF,如RFC5869中所述
无连接协议
任何安全协议都需要保留某些状态,因此有一个初始的非常简单的握手,用于建立用于数据传输的对称密钥。这种握手每隔几分钟发生一次,以便提供旋转密钥以实现完美的前向保密性。它基于时间完成,而不是基于先前数据包的内容,因为它旨在优雅地处理数据包丢失。有一个巧妙的脉冲机制,通过自动检测握手何时过期来确保最新的密钥和握手是最新的,并在需要时重新协商。它对每个主机使用单独的数据包队列,以便它可以最大限度地减少握手期间的数据包丢失,同时为所有客户端提供稳定的性能。
换句话说,你打开设备,其他一切都会自动处理。你无需担心要求设备重新连接、断开连接或重新初始化,或任何此类操作。
以下计时器正在发挥作用:
REKEY_TIMEOUT + jitter
如果尚未收到响应,则在 ms后重试握手启动,其中jitter
是 0 到 333 ms 之间的某个随机值。- 如果从给定对等点接收到了数据包,但是我们尚未在
KEEPALIVE
毫秒内将数据包发送回给定对等点,则我们将发送一个空数据包。 - 如果我们已经向给定对等方发送了一个数据包,但是在 ms 内没有从该对等方收到数据包
KEEPALIVE + REKEY_TIMEOUT
,我们将启动新的握手。 REJECT_AFTER_TIME * 3
如果没有交换新密钥,则所有临时私钥和对称会话密钥在 ms 之后都会被清零。- 发送数据包后,如果使用该密钥发送的数据包数量超过
REKEY_AFTER_MESSAGES
,我们将启动新的握手。 - 发送数据包后,如果发送者是握手的原始发起者,并且当前会话密钥为
REKEY_AFTER_TIME
ms old,我们将发起新的握手。如果发送者是握手的原始响应者,我们不会像原始发起者那样在 ms 后重新启动新的握手REKEY_AFTER_TIME
。 - 收到数据包后,如果接收方是握手的原始发起者,并且当前会话密钥是
REKEY_AFTER_TIME - KEEPALIVE_TIMEOUT - REKEY_TIMEOUT
ms old,则我们将发起新的握手。 - 握手每毫秒仅发起一次
REKEY_TIMEOUT
,并强制执行严格的速率限制。 - 如果会话计数器大于
REJECT_AFTER_MESSAGES
或其密钥早于REJECT_AFTER_TIME
ms,则数据包将被丢弃。 - 在尝试发起新的握手 ms后
REKEY_ATTEMPT_TIME
,重试放弃并停止,并清除所有排队等待发送的现有数据包。如果明确有数据包排队等待发送,则重置此计时器。
未来的工作涉及调整REKEY_TIMEOUT
使用指数退避。
握手完成后,发起者向响应者发送消息,然后响应者向发起者发送消息,发起者可以发送加密会话数据包,但响应者不能。响应者必须等到收到发起者发送的一个加密会话数据包后才能使用新会话,以便提供密钥确认。因此,在响应者使用新建立的会话收到第一个数据包之前,它必须将数据包排队以便稍后发送,或者使用上一个会话(如果存在且有效)。因此,在发起者收到响应者的响应后,如果它没有立即排队等待发送的数据包,则应发送一个空数据包,以提供此确认。
密钥交换和数据包
WireGuard 使用NoiseNoise_IK
的握手,以CurveCP、NaCL、KEA+、SIGMA、FHMQV和HOMQV的工作为基础。所有数据包均通过 UDP 发送。
密钥交换具有以下优良特性:
- 避免密钥泄露冒充
- 避免重放攻击
- 完美前向保密
- 实现“AKE安全”
- 身份隐藏
如果需要额外的对称密钥加密层(例如,用于后量子抗性),WireGuard 还支持可选的预共享密钥,该密钥混合到公钥加密中。当未使用预共享密钥模式时,下面使用的预共享密钥值被假定为 32 字节的全零字符串。
对于以下数据包的描述,请参考以下函数:
DH(private key, public key)
:Curve25519 点乘以private key
和public key
,返回 32 个字节的输出DH_GENERATE()
:生成随机的 Curve25519 私钥,返回 32 个字节的输出RAND(len)
:返回len
输出的随机字节DH_PUBKEY(private key)
:从计算 Curve25519 公钥private key
,返回 32 个字节的输出AEAD(key, counter, plain text, auth text)
:ChaCha20Poly1305 AEAD,如 RFC7539 中所述,其nonce
由 32 位零组成,后跟 64 位小端值counter
XAEAD(key, nonce, plain text, auth text)
:XChaCha20Poly1305 AEAD,带有随机的 24 字节随机数AEAD_LEN(plain len)
:plain len + 16
HMAC(key, input)
:HMAC-Blake2s(key, input, 32)
, returning 32 bytes of outputMAC(key, input)
:Keyed-Blake2s(key, input, 16)
, returning 16 bytes of outputHASH(input)
:Blake2s(input, 32)
, returning 32 bytes of outputTAI64N()
: TAI64N timestamp of current time which is 12 bytesCONSTRUCTION
: the UTF-8 valueNoise_IKpsk2_25519_ChaChaPoly_BLAKE2s
, 37 bytesIDENTIFIER
: the UTF-8 valueWireGuard v1 zx2c4 [email protected]
, 34 bytesLABEL_MAC1
: the UTF-8 valuemac1----
, 8 bytesLABEL_COOKIE
: the UTF-8 valuecookie--
, 8 bytes
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
.
Cookie Reply Packet
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.