Browse WireGuard specs (24 of 38)

Proofs / WireGuard / Earlier, longer properties

Replayed initiations

Over any sequence of fresh and replayed initiations, with the clocks moving (the real-time clock also backwards), an initiation is accepted exactly when its timestamp is newer than every accepted one and the last acceptance was at least 20 ms earlier; a replay is always rejected and changes nothing.

spec
x_replay
length
128 lines of C
own proof
3,912 lines of Rocq
status
Proved
24 of 38
On this page

The spec

The property is stated by the C program harness/x_replay.c, 128 lines. Here is its main cut down to the inputs it draws, the calls into the code under test and the checks (lines 31–128; ... 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);
	use_psk = nondet_u8() & 1;
	nondet_bytes(psk, 32);
	...
	now = draw_boottime();
	...
	steps = draw_steps();
	for (i = 0; i < steps; i++) {
		kind = nondet_u8() % 3;
			...
			draw_real_clock();
			if (!wg_noise_handshake_create_initiation(m, &pb.handshake))
			...
			wg_cookie_add_mac_to_packet(m, sizeof(*m), &pb);
			...
			u64 d = draw_u64() >> (nondet_u8() % 64);
		...
		ret = wg_noise_handshake_consume_initiation(m, &wb);
			...
			CHECK(!acc);   // the model never accepts a replay of an accepted initiation
			...
			CHECK(ret == NULL && hs_same(&snap, &pa.handshake) && pa.refcount.refcount == 1);
			...
			CHECK(ret == &pa && pa.refcount.refcount == 2);
			...
			CHECK(pa.refcount.refcount == 1);
			CHECK(pa.handshake.state == HANDSHAKE_CONSUMED_INITIATION);
			CHECK(bytes_eq(pa.handshake.latest_timestamp, ci.ts, 12));
			CHECK(pa.handshake.last_initiation_consumption == m_last_consumption(last, now));
			CHECK(bytes_eq(pa.handshake.hash, ci.st.h, 32) && bytes_eq(pa.handshake.chaining_key, ci.st.ck, 32));
...
}

The full spec at commit 3c1fc13 ↗

The property as the spec's first comment states it
Law `replay`: replayed and non-newer initiations are rejected, over ANY sequence of initiations.

Devices A and B with ANY static keys and one preshared key (or none), each with a peer for the other.
A sequence of ANY length of events, each one of:
  - A creates a new initiation (any real-time clock, which may go backwards; any ephemeral randomness), adds
    its macs, and B consumes it;
  - B consumes again the last initiation A created (a replay, possibly of a rejected one);
  - B consumes again the last initiation it accepted (a replay of an accepted one);
with B's boot clock moving forward by ANY amount (possibly 0) before each consumption.
Claims, against a shadow model of B's view of A (latest accepted TAI64N timestamp, last consumption time):
 - B accepts exactly when the message decrypts (model) AND its timestamp is greater than every timestamp
   accepted so far AND the last acceptance was at least 20 ms ago; the accepted initiation updates the model;
 - a replay of an accepted initiation is ALWAYS rejected;
 - a rejected initiation changes nothing in B's handshake (byte for byte) nor its peer's references;
 - peer references are balanced after every step.

The code it is about

wg_noise_handshake_consume_initiation in drivers/net/wireguard/noise.c, lines 583–668 (... marks lines left out):

C
struct wg_peer *
wg_noise_handshake_consume_initiation(struct message_handshake_initiation *src,
				      struct wg_device *wg)
{
	...
	down_read(&handshake->lock);
	replay_attack = memcmp(t, handshake->latest_timestamp,
			       NOISE_TIMESTAMP_LEN) <= 0;
	flood_attack = (s64)handshake->last_initiation_consumption +
			       NSEC_PER_SEC / INITIATIONS_PER_SECOND >
		       (s64)ktime_get_coarse_boottime_ns();
	up_read(&handshake->lock);
	if (replay_attack || flood_attack)
		break;

...
}

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_replay.v:

Rocq
Theorem x_replay : 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_replay : harness_safe WgNoise.contracts.x_replay.whole_prog.
Proof. exact (proj1 (proj2 (proj2 (proj2 (proj2 (proj2 (proj2 (proj2 (proj2 (proj2 (proj2 (proj2 (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.

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

How it was proved

src/proof/harness/replay/ holds 3,912 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 noise.c: see the list on the left, or the summary.