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") + } +}