Browse WireGuard specs (28 of 38)

Proofs / WireGuard / Earlier, longer properties

mac1 and mac2

On any message of 32 to UINT_MAX bytes and any cookie state, wg_cookie_add_mac_to_packet and wg_cookie_validate_packet compute exactly the model's mac1, mac2 and verdict, the cookie secret is renewed once it is at least 120 seconds old, and nothing else changes.

  • At exactly 120 seconds the secret is already new, as cookie.c decides it, so a mac2 made with the old secret fails. The spec draws the clock at that boundary.
spec
x_cookie_mac
length
271 lines of C
own proof
8,631 lines of Rocq
status
Proved
28 of 38
On this page

The spec

The property is stated by the C program harness/x_cookie_mac.c, 271 lines. Here is its main cut down to the inputs it draws, the calls into the code under test and the checks (lines 62–271; ... marks lines left out). The checks call helpers and the model in the spec and in harness/common.h.

C
int main(void)
{
	...
	wg_shim_boottime_ns = draw_boottime();
	wg_noise_init();
	nondet_bytes(raw_a, 32);
	nondet_bytes(raw_b, 32);
	...
	CHECK(bytes_eq(wb.cookie_checker.secret, wg_shim_random_log, 32) &&
		...
		CHECK(bytes_eq(wb.cookie_checker.message_mac1_key, k, 32));
		...
		CHECK(bytes_zero(wb.cookie_checker.message_mac1_key, 32) &&
	...
	CHECK(wb.cookie_checker.device == &wb);
	...
	nondet_bytes(wb.cookie_checker.secret, 32);
	wb.cookie_checker.secret_birthdate = draw_boottime();
	...
	honest_peer = nondet_u8() % 4 != 0;
	...
	CHECK(bytes_eq(p.latest_cookie.cookie_decryption_key, k, 32));
	...
	wg_cookie_add_mac_to_packet(msg, len, &p);
	...
	CHECK(bytes_eq(msg, copy, len));
	...
	CHECK(p.latest_cookie.birthdate == cs.birthdate && p.latest_cookie.is_valid == cs.is_valid &&
	...
	CHECK(hs_same(&hs0, &p.handshake) && pr_same_but_cookie(&ps0, &p) && p.refcount.refcount == refs0 &&
	...
	r = wg_cookie_validate_packet(&wb.cookie_checker, &skb, check_cookie);
	...
	CHECK(wb.cookie_checker.secret_birthdate == secret_birth && bytes_eq(wb.cookie_checker.secret, secret, 32));
	CHECK(bytes_eq(msg, copy, len) && skb.data == msg && skb.len == len);
		...
		CHECK(r != INVALID_MAC);
			...
			CHECK(r == (wg_shim_ratelimit_allow ? VALID_MAC_WITH_COOKIE : VALID_MAC_WITH_COOKIE_BUT_RATELIMITED));
...
}

The full spec at commit 3c1fc13 ↗

The property as the spec's first comment states it
Law `cookie_mac`: mac1/mac2 creation (wg_cookie_add_mac_to_packet) and validation (wg_cookie_validate_packet)
against the model, on ANY message and ANY cookie state.

Responder device B with ANY static key (its cookie checker keys precomputed; its cookie secret and the secret's
birthdate ANY); a peer P of another device whose public key is B's (honest) or ANY; P's cookie state ANY
(valid or not, any birthdate, any cookie, any last mac1) or, in one mode, the cookie B would make for the
packet's source; a message of ANY length >= 32 (its last 32 bytes are mac1 || mac2; exact-size buffer) with ANY
content; ANY boot clock; the packet's protocol IPv4, IPv6 or other, ANY source address and port; the rate
limiter's answer ANY; check_cookie ANY.
Claims:
 - the checker's keys are HASH("mac1----" || S_pub) and HASH("cookie--" || S_pub) (zero without identity);
   the peer's are the same for its public key; wg_cookie_checker_init gives the checker a fresh
   random secret (the shim's draw) born now;
 - add_mac writes mac1 = MAC(mac1 key, m[0..len-32)), and mac2 = MAC(cookie, m[0..len-16)) if the cookie is
   valid and younger than 115 s (COOKIE_SECRET_MAX_AGE - COOKIE_SECRET_LATENCY: birthdate + 115 s > now,
   signed), else 16 zero bytes; nothing else of the message; the peer remembers mac1 (last_mac1_sent,
   have_sent_mac1) and nothing else of its cookie state changes;
 - validate returns exactly the model's verdict: INVALID_MAC if mac1 is not MAC(checker mac1 key, ...);
   VALID_MAC_BUT_NO_COOKIE if !check_cookie or mac2 does not match the cookie of the packet's source;
   VALID_MAC_WITH_COOKIE_BUT_RATELIMITED if the rate limiter refuses; else VALID_MAC_WITH_COOKIE.  The cookie
   is MAC32(secret, source address (4 bytes for IPv4, 16 for IPv6, none otherwise) || source port), and the
   secret is replaced by fresh random bytes (birthdate now) when it is 120 s old, exactly as cookie.c decides it
   (wg_birthdate_has_expired: (s64)(birthdate + 120 s) <= (s64)now): at EXACTLY 120 s the secret is new, so a
   mac2 made with the old one fails (wireguard-go rotates only after 120 s); the harness draws the clock at
   120 s +- 2 ns and 115 s +- 2 ns on purpose;
 - validate modifies neither the message nor the packet; the checker only by that rotation;
 - (whole-state frame; P has ANY handshake state, dead flag, last sent handshake, reference count
   below INT_MAX, its handshake entry in A's index table or not, and ANY keypairs - whole objects of any content -
   in that table and possibly in its slots) add_mac changes nothing of the peer but those two cookie fields
   (handshake, reference count, keypair slots and every byte of the keypairs, ... unchanged), neither device and
   not A's index table; validate changes nothing of either device but that rotation (identity, both checker keys,
   the checker's device, tables) nor the peer (its keypairs and A's index table included), and leaves every field
   of the packet as it was (data, length, protocol, source address and port); neither allocates nor makes a
   reference count warn;
 - round trips: a message with macs added by a peer that has B's public key has a valid mac1 at B; if the peer
   also holds B's current cookie for the packet's source (fresh), mac2 is valid too.

The code it is about

wg_cookie_validate_packet in drivers/net/wireguard/cookie.c, lines 119–156:

C
enum cookie_mac_state wg_cookie_validate_packet(struct cookie_checker *checker,
						struct sk_buff *skb,
						bool check_cookie)
{
	struct message_macs *macs = (struct message_macs *)
		(skb->data + skb->len - sizeof(*macs));
	enum cookie_mac_state ret;
	u8 computed_mac[COOKIE_LEN];
	u8 cookie[COOKIE_LEN];

	/* QUICKPROOF P08: `goto out` is `break` out of this block (VST has no goto); same statements in the same order */
	do {
	ret = INVALID_MAC;
	compute_mac1(computed_mac, skb->data, skb->len,
		     checker->message_mac1_key);
	if (crypto_memneq(computed_mac, macs->mac1, COOKIE_LEN))
		break;

	ret = VALID_MAC_BUT_NO_COOKIE;

	if (!check_cookie)
		break;

	make_cookie(cookie, skb, checker);

	compute_mac2(computed_mac, skb->data, skb->len, cookie);
	if (crypto_memneq(computed_mac, macs->mac2, COOKIE_LEN))
		break;

	ret = VALID_MAC_WITH_COOKIE_BUT_RATELIMITED;
	if (!wg_ratelimiter_allow(skb, dev_net(checker->device->dev)))
		break;

	ret = VALID_MAC_WITH_COOKIE;

	} while (0); /* QUICKPROOF P08: was the label `out:` */
	return ret;
}

This is the code under test as the proof sees it: Linux at 551c722f with the two lib/crypto patches, and with the idiom rewrites that VST needs, each marked QUICKPROOF in the code and listed in DEVIATIONS.md.

The theorem

QuickProof generated this statement from the spec in src/contracts/x_cookie_mac.v:

Rocq
Theorem x_cookie_mac : harness_safe whole_prog.

whole_prog is the spec linked with the code under test. The 19 x_ specs are proved together, as the theorem suite_x, from one specification of every function of the code. src/contracts/laws.v states each spec on its own program and takes it from suite_x:

Rocq
Theorem x_cookie_mac : harness_safe WgNoise.contracts.x_cookie_mac.whole_prog.
Proof. exact (proj1 (proj2 (proj2 (proj2 WgNoise.contracts.suite.suite_x)))). Qed.

qp verify checks that this statement is the one generated from the spec at the contract commit, and that its proof uses no axiom beyond the 13 that every spec here uses: standard logical axioms and VST's abstract malloc library.

The C files (units) its program links
  • drivers/net/wireguard/noise.c
  • drivers/net/wireguard/cookie.c
  • patch/counter_validate.c
  • lib/crypto/blake2s.c
  • lib/crypto/chacha.c
  • lib/crypto/chacha-block-generic.c
  • lib/crypto/poly1305.c
  • lib/crypto/poly1305-donna32.c
  • lib/crypto/chacha20poly1305.c
  • lib/crypto/curve25519.c
  • lib/crypto/curve25519-fiat32.c
  • patch/wg_shim.c
  • quickproof/libc/libc.c
  • quickproof/libc/malloc_unit.c

What it leaves out

  • Like every spec of this proof: runs of more than one thread (locks and RCU are those of a single thread), termination (a run that never returns is not a counterexample), and binaries built by a compiler (the theorem is about the code as CompCert defines C for 64-bit x86).
  • The kernel around the code is a shim of plain C for one thread, and the code carries rewrites of idioms that VST cannot handle. Both are tested against the kernel's code, not proved.

Recorded for this property in DEVIATIONS.md (section L, what the properties exclude or assume):

  • L06. wg_cookie_validate_packet, wg_cookie_add_mac_to_packet and wg_cookie_message_create are called on messages of at least 32 bytes (the two MACs) Why: their callers pass only handshake messages of fixed sizes (148 and 92 bytes)

The summary page lists what the proof as a whole does not cover.

How it was proved

src/proof/harness/cookie_mac/ holds 8,631 lines of Rocq (formerly Coq) that are specific to this spec. It builds on the specifications and proofs of the functions of noise.c, cookie.c and lib/crypto, the protocol model, and the Diffie-Hellman agreement of X25519. The whole proof of the 38 specs is about 300,000 lines. AI agents wrote it, Rocq checks all of it, and nobody has to read it. What people have to read is the spec above, with harness/common.h, and the kernel shim that DEVIATIONS.md describes.

Other specs about cookie.c: see the list on the left, or the summary.