w7corp/easywechat · error · InvalidSignatureException
Clock Offset Exceeded
Error message
Clock Offset Exceeded
What it means
Replay protection in Validator::validate() (src/Pay/Validator.php:49-51): every signed message carries Wechatpay-Timestamp, and the validator rejects anything where time() - intval($timestamp) exceeds MAX_ALLOWED_CLOCK_OFFSET = 300 seconds (Validator.php:14). It is raised as InvalidSignatureException even when the signature itself is valid, because a stale timestamp can indicate a replayed notification. The check is one-directional — only past-dated timestamps are rejected; future timestamps pass.
Source
Thrown at src/Pay/Validator.php:50
public function validate(MessageInterface $message): void
{
foreach ([self::HEADER_SIGNATURE, self::HEADER_TIMESTAMP, self::HEADER_SERIAL, self::HEADER_NONCE] as $header) {
if (! $message->hasHeader($header)) {
throw new InvalidSignatureException("Missing Header: {$header}");
}
}
[$timestamp] = $message->getHeader(self::HEADER_TIMESTAMP);
[$nonce] = $message->getHeader(self::HEADER_NONCE);
[$serial] = $message->getHeader(self::HEADER_SERIAL);
[$signature] = $message->getHeader(self::HEADER_SIGNATURE);
$body = (string) $message->getBody();
$message = "{$timestamp}\n{$nonce}\n{$body}\n";
if (\time() - \intval($timestamp) > self::MAX_ALLOWED_CLOCK_OFFSET) {
throw new InvalidSignatureException('Clock Offset Exceeded');
}
$publicKey = $this->merchant->getPlatformCert($serial);
if (! $publicKey) {
throw new InvalidConfigException(
"No platform certs found for serial: {$serial},
please download from wechat pay and set it in merchant config with key `certs`."
);
}
if (\openssl_verify(
$message,
base64_decode($signature),
strval($publicKey),
OPENSSL_ALGO_SHA256
) !== 1) {
throw new InvalidSignatureException('Invalid Signature');View on GitHub (pinned to f0cf0a8b83)
Solutions
- Validate at ingress: run the Validator/Server flow inside the HTTP handler immediately on receipt, then enqueue the already-verified message.
- Sync the clock on every host and container: enable chrony, ntpd or systemd-timesyncd and verify with `date -u` against world time.
- If validation must happen late, treat the stale notification as untrusted and confirm the real order state via the WeChat Pay query API instead of trusting the old body.
- Monitor webhook queue lag and alert well before it approaches the 300-second window.
Example fix
// before: validate long after receipt (queue worker runs 10 min later)
$storedRequest = $cache->get('wechat_notify_' . $id);
$app->getValidator()->validate($storedRequest); // Clock Offset Exceeded
// after: validate immediately in the HTTP handler, queue only the verified message
$app->getValidator()->validate($request); // ingress, within seconds of delivery
$queue->push(ProcessWechatNotify::class, ['body' => (string) $request->getBody()]); Defensive patterns
Strategy: validation
Validate before calling
$timestamp = (int) $request->getHeaderLine('Wechatpay-Timestamp');
if ($timestamp <= 0 || (time() - $timestamp) > 300) {
// Stale/replayed or clock-drifted: reject before full validation
return new \GuzzleHttp\Psr7\Response(400);
}
$app->getValidator()->validate($request); Try / catch
use EasyWeChat\Pay\Exceptions\InvalidSignatureException;
try {
$app->getValidator()->validate($request);
} catch (InvalidSignatureException $e) {
if (str_contains($e->getMessage(), 'Clock Offset')) {
// Late processing or clock drift: don't trust the stored message —
// query the order via the WeChat Pay API for truth, then act on that.
return confirmOrderStateViaApi($request);
}
return $response->withStatus(401);
} Prevention
- Run chrony/ntpd or systemd-timesyncd on every app host and container
- Validate webhooks at ingress; enqueue only after validation succeeds
- Monitor webhook queue lag and alert well before it approaches 300 seconds
- Never replay captured notifications against the live validator — Clock Offset Exceeded is the by-design result
When it happens
Trigger: Local server clock behind real time so genuine notifications look older than 5 minutes; validating a webhook long after receipt (queue worker picks it up 10 minutes later, retry backlog, crashed workers resumed); replaying captured requests through the validator during debugging; WeChat re-delivering an old notification after a long endpoint outage.
Common situations: Containers or VMs without NTP (chrony/ntpd/systemd-timesyncd blocked in the network policy); store-now-validate-later webhook architectures; queue lag exceeding 300 seconds; reprocessing archived notifications in a test or migration environment.
Related errors
- Missing Header: {$header}
- No platform certs found for serial: {$serial},
- Invalid Signature
- Encrypt AES ECB failed.
- Decrypt failed
AI-assisted analysis of w7corp/easywechat@f0cf0a8b83 (2026-08-21).
Data as JSON: /api/errors/df9728d106811450.
Report an issue: GitHub.