{"record":{"id":"acddd6555f5b1f08","repo":"zeroclaw-labs/zeroclaw","slug":"request-timestamp-too-old-or-too-far-in-future","errorCode":null,"errorMessage":"Request timestamp too old or too far in future","messagePattern":"Request timestamp too old or too far in future","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/nodes/transport.rs","lineNumber":45,"sourceCode":"    })?;\n    mac.update(&timestamp.to_le_bytes());\n    mac.update(nonce.as_bytes());\n    mac.update(payload);\n    Ok(hex::encode(mac.finalize().into_bytes()))\n}\n\n/// Verify a signed request, rejecting stale timestamps for replay protection.\npub fn verify_request(\n    shared_secret: &str,\n    payload: &[u8],\n    timestamp: i64,\n    nonce: &str,\n    signature: &str,\n    max_age_secs: i64,\n) -> Result<bool> {\n    let now = Utc::now().timestamp();\n    if (now - timestamp).abs() > max_age_secs {\n        bail!(\"Request timestamp too old or too far in future\");\n    }\n\n    let expected = sign_request(shared_secret, payload, timestamp, nonce)?;\n    Ok(constant_time_eq(expected.as_bytes(), signature.as_bytes()))\n}\n\n/// Constant-time comparison to prevent timing attacks.\nfn constant_time_eq(a: &[u8], b: &[u8]) -> bool {\n    if a.len() != b.len() {\n        return false;\n    }\n    a.iter()\n        .zip(b.iter())\n        .fold(0u8, |acc, (x, y)| acc | (x ^ y))\n        == 0\n}\n\n// ── Node transport client ───────────────────────────────────────","sourceCodeStart":27,"sourceCodeEnd":63,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/nodes/transport.rs#L27-L63","documentation":"verify_request is the replay-protection step of ZeroClaw's node transport: before comparing HMAC signatures it checks that the request timestamp is within max_age_secs (300 seconds — a 5-minute window — set in NodeTransport::new) of the receiving node's clock, in either direction. Requests older or further in the future than the window are rejected outright, so stale or pre-signed captured requests cannot be replayed.","triggerScenarios":"Client node clock skewed more than 5 minutes from the receiver (VM resumed from snapshot, broken NTP, dual-boot clock offset); replaying a captured request after the window; long queueing between signing and verification so the timestamp ages out.","commonSituations":"Laptops/VMs with dead RTC batteries or suspended then resumed; containers with drifted clocks; CI environments sending signed node requests after delays; timezone confusion is NOT the cause — these are Unix epoch timestamps.","solutions":["Sync clocks on both nodes: enable NTP (timedatectl set-ntp true, or run chrony/ntpd) and confirm with timedatectl / date on each side","Have the sender regenerate and re-sign the request with a fresh timestamp","If genuine propagation delay exceeds 5 minutes (rare), rebuild the transport with a larger max_request_age_secs — the default is hard-coded at 300 in NodeTransport::new","If it happens on every request between two specific nodes, compare `date +%s` on both to measure skew directly"],"exampleFix":"# diagnose on both nodes\ndate +%s   # values must differ by < 300\n\n# fix\nsudo timedatectl set-ntp true","handlingStrategy":"validation","validationCode":"let now = Utc::now().timestamp();\nlet signed_at = timestamp_used_for_signature; // client side\nif (now - signed_at).abs() > max_age_secs {\n    // do not send: clocks drifted; resync NTP first\n}","typeGuard":null,"tryCatchPattern":"match verify_request(secret, payload, ts, nonce, sig, max_age) {\n    Err(e) if e.to_string().contains(\"too old or too far in future\") => {\n        // reject as replay/skew: log both clocks, do NOT widen the window per request\n    }\n    other => other,\n}","preventionTips":["Run NTP/chrony on every node and alert on drift beyond ~60s","Sign immediately before sending (the transport already does) and never cache signed requests","Monitor this rejection rate — a spike indicates clock or replay problems","Keep the replay window tight; widen it only with a documented reason, never in response to a single failure"],"tags":["rust","zeroclaw","security","hmac","replay-protection","clock-skew","nodes"],"backgroundTag":"request-timestamp-expired","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}