zeroclaw-labs/zeroclaw · error · anyhow::Error

Request timestamp too old or too far in future

Error message

Request timestamp too old or too far in future

What it means

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.

Source

Thrown at crates/zeroclaw-runtime/src/nodes/transport.rs:45

    })?;
    mac.update(&timestamp.to_le_bytes());
    mac.update(nonce.as_bytes());
    mac.update(payload);
    Ok(hex::encode(mac.finalize().into_bytes()))
}

/// Verify a signed request, rejecting stale timestamps for replay protection.
pub fn verify_request(
    shared_secret: &str,
    payload: &[u8],
    timestamp: i64,
    nonce: &str,
    signature: &str,
    max_age_secs: i64,
) -> Result<bool> {
    let now = Utc::now().timestamp();
    if (now - timestamp).abs() > max_age_secs {
        bail!("Request timestamp too old or too far in future");
    }

    let expected = sign_request(shared_secret, payload, timestamp, nonce)?;
    Ok(constant_time_eq(expected.as_bytes(), signature.as_bytes()))
}

/// Constant-time comparison to prevent timing attacks.
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
    if a.len() != b.len() {
        return false;
    }
    a.iter()
        .zip(b.iter())
        .fold(0u8, |acc, (x, y)| acc | (x ^ y))
        == 0
}

// ── Node transport client ───────────────────────────────────────

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Sync clocks on both nodes: enable NTP (timedatectl set-ntp true, or run chrony/ntpd) and confirm with timedatectl / date on each side
  2. Have the sender regenerate and re-sign the request with a fresh timestamp
  3. 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
  4. If it happens on every request between two specific nodes, compare `date +%s` on both to measure skew directly

Example fix

# diagnose on both nodes
date +%s   # values must differ by < 300

# fix
sudo timedatectl set-ntp true
Defensive patterns

Strategy: validation

Validate before calling

let now = Utc::now().timestamp();
let signed_at = timestamp_used_for_signature; // client side
if (now - signed_at).abs() > max_age_secs {
    // do not send: clocks drifted; resync NTP first
}

Try / catch

match verify_request(secret, payload, ts, nonce, sig, max_age) {
    Err(e) if e.to_string().contains("too old or too far in future") => {
        // reject as replay/skew: log both clocks, do NOT widen the window per request
    }
    other => other,
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/acddd6555f5b1f08. Report an issue: GitHub.