From b9b831fb90de3cf89c0a8514af550743976ee6ac Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Thu, 22 Oct 2026 23:08:25 +0200 Subject: [PATCH] =?UTF-8?q?D30:=20a=20wait=20stamp=20dated=20after=20now?= =?UTF-8?q?=20(a=20clock=20that=20jumped=20back)=20is=20dropped,=20never?= =?UTF-8?q?=20a=20block=20until=20that=20date=20=E2=80=94=20the=20commit's?= =?UTF-8?q?=20security=20review;=20red-proved;=20unreleased?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 +++- internal/wgtunnel/rekey.go | 8 ++++++++ internal/wgtunnel/rekey_test.go | 19 +++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa3b3f7..2b29f14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,9 @@ hard link, then the temp file is renamed OVER `private.key` and the directory fsynced (a power cut or a kill between two renames used to leave no key, or an empty one). `private.key.prev` keeps the FIRST old key (the escrow may hold it); later ones are `private.key.prev.`, the newest three kept. The once-an-hour bound is saved in `wg/last-rekey`, so - an agent restart does not reset it. Tests `TestD30_TwoRotationsKeepTheFirstOldKey`, + an agent restart does not reset it; a start or a last re-key dated AFTER now (a clock that jumped back) is dropped, + never a block until that date (the commit's own security review). Tests `TestD30_TwoRotationsKeepTheFirstOldKey`, + `TestD30_ALastReKeyFromTheFutureDoesNotBlock`, `TestD30_TheHourBoundSurvivesARestart`, red-proved. **Live (felhom.eu `audits/night-2026-10-23/P2-D30/`):** the D30 re-key ran on a test box (before these two changes): clock corrected → 15 min → new key registered → the box switched at its next desired-state fetch → a handshake with the new key. diff --git a/internal/wgtunnel/rekey.go b/internal/wgtunnel/rekey.go index 157eefb..e60abe7 100644 --- a/internal/wgtunnel/rekey.go +++ b/internal/wgtunnel/rekey.go @@ -65,12 +65,20 @@ func (m *Manager) clockAheadRekeyLocked(ctx context.Context, block *hub.WireWire "future_s", fut, "rekey_after", m.rekeyAfter.String()) return false } + // A clock that jumps backwards must not stretch either wait (commit security review 2026-10-23 night): a start or a + // last re-key stamped AFTER now is from a wrong clock and is dropped — this whole feature exists because clocks jump. + if now.Before(m.futureSince) { + m.futureSince = now + } if now.Sub(m.futureSince) < m.rekeyAfter { return false } if m.lastRekey.IsZero() { m.lastRekey = m.loadLastRekey() } + if now.Before(m.lastRekey) { + m.lastRekey = time.Time{} + } if !m.lastRekey.IsZero() && now.Sub(m.lastRekey) < rekeyMinGap { return false } diff --git a/internal/wgtunnel/rekey_test.go b/internal/wgtunnel/rekey_test.go index 239a8c3..dfe954d 100644 --- a/internal/wgtunnel/rekey_test.go +++ b/internal/wgtunnel/rekey_test.go @@ -206,3 +206,22 @@ func TestD30_TheHourBoundSurvivesARestart(t *testing.T) { } _ = active } + +// Commit security review 2026-10-23 night: a last re-key saved while the clock ran ahead (a later date than now) must +// not block the next re-key until that date. RED-PROOF: with the „lastRekey after now → dropped" guard removed, this +// fails — seen failing 2026-10-23 night. +func TestD30_ALastReKeyFromTheFutureDoesNotBlock(t *testing.T) { + fh := &fakeHub{} + m, _, active, _ := testManager(t, fh) + pub := establishWG(t, m, active) + m.saveLastRekey(time.Unix(hs+2*365*86400, 0)) // written while the clock was two years ahead + ctx := context.Background() + t0 := int64(hs - 3*3600) + at(m, t0) + m.Watchdog(ctx, true, testBlock(pub)) + at(m, t0+16*60) + m.Watchdog(ctx, true, testBlock(pub)) + if localPub(t, m) == pub { + t.Fatal("a last re-key dated in the future must not block the re-key") + } +}