larksuite/cli · error

timestamp drift %.0fs exceeds limit %ds

Error message

timestamp drift %.0fs exceeds limit %ds

What it means

Verify() in the sidecar package checks that a signed request's Timestamp is within MaxTimestampDrift (60s) of the server's current time before validating the HMAC. This error means the absolute difference exceeded 60 seconds, so the request is rejected as a replay/stale request. The timestamp check is an anti-replay window; it runs before signature comparison.

Source

Thrown at sidecar/hmac.go:76

// Sign computes the HMAC-SHA256 signature over the canonical request string.
func Sign(key []byte, req CanonicalRequest) string {
	mac := hmac.New(sha256.New, key)
	mac.Write([]byte(req.canonicalString()))
	return hex.EncodeToString(mac.Sum(nil))
}

// Verify checks that signature matches the HMAC-SHA256 of the canonical
// request and that the timestamp is within MaxTimestampDrift seconds of now.
// Returns nil on success.
func Verify(key []byte, req CanonicalRequest, signature string) error {
	ts, err := strconv.ParseInt(req.Timestamp, 10, 64)
	if err != nil {
		return fmt.Errorf("invalid timestamp %q: %w", req.Timestamp, err)
	}
	drift := math.Abs(float64(time.Now().Unix() - ts))
	if drift > MaxTimestampDrift {
		return fmt.Errorf("timestamp drift %.0fs exceeds limit %ds", drift, MaxTimestampDrift)
	}
	expected := Sign(key, req)
	if !hmac.Equal([]byte(expected), []byte(signature)) {
		return fmt.Errorf("HMAC signature mismatch")
	}
	return nil
}

// Timestamp returns the current Unix epoch seconds as a string.
func Timestamp() string {
	return strconv.FormatInt(time.Now().Unix(), 10)
}

View on GitHub (pinned to 7fd6ef3c07)

Solutions

  1. Call sidecar.Timestamp() (or time.Now().Unix()) immediately before signing each request, and re-sign on any retry.
  2. Fix the machine's clock: run NTP/chrony (e.g. `sudo sntp -sS pool.ntp.org` or enable time sync in the VM/container runtime).
  3. Ensure the timestamp is Unix epoch SECONDS, not milliseconds — divide a ms value by 1000.
  4. Check drift by comparing `time.Now().Unix()` on both sides; if skew is structural, sync the sandbox host clock, since the 60s limit is a fixed constant (MaxTimestampDrift).

Example fix

// before
req.Timestamp = cachedTimestamp // signed once at startup
// after
req.Timestamp = sidecar.Timestamp() // fresh epoch seconds per request/retry
Defensive patterns

Strategy: validation

Validate before calling

ts, err := strconv.ParseInt(req.Timestamp, 10, 64)
if err != nil {
	return fmt.Errorf("bad timestamp: %w", err)
}
drift := math.Abs(float64(time.Now().Unix() - ts))
if drift > sidecar.MaxTimestampDrift {
	return fmt.Errorf("client clock off by %.0fs; sync NTP before calling", drift)
}

Type guard

func timestampFresh(req sidecar.CanonicalRequest) bool {
	ts, err := strconv.ParseInt(req.Timestamp, 10, 64)
	if err != nil {
		return false
	}
	d := math.Abs(float64(time.Now().Unix() - ts))
	return d <= sidecar.MaxTimestampDrift
}

Try / catch

if err := sidecar.Verify(key, req, sig); err != nil {
	if strings.Contains(err.Error(), "timestamp drift") {
		// clock skew: resync clock, refresh timestamp, re-sign and retry once
		req.Timestamp = sidecar.Timestamp()
		sig := sidecar.Sign(key, req)
		err = sidecar.Verify(key, req, sig)
	}
	return err
}

Prevention

When it happens

Trigger: Calling sidecar.Verify (directly or via ServeHTTP / verifyWithClientKeys) with a CanonicalRequest whose Timestamp string parses to a Unix time more than 60 seconds away from time.Now() on the verifying side — including timestamps in the future.

Common situations: Client clock skew (VM/container clock drift, suspended laptop, VM resuming from snapshot); signing a request and retrying it more than a minute later; manually constructed requests reusing an old cached timestamp; timezone-naive code building the timestamp in non-epoch units (e.g. milliseconds, which makes drift astronomically large).

Related errors


AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04). Data as JSON: /api/errors/8c706ea392dea88c. Report an issue: GitHub.