w7corp/easywechat · error · InvalidSignatureException
Invalid Signature
Error message
Invalid Signature
What it means
The final cryptographic check in Validator::validate() (src/Pay/Validator.php:64-71): openssl_verify() with OPENSSL_ALGO_SHA256 over the exact string "{$timestamp}\n{$nonce}\n{$body}\n" (trailing newline included), using the platform cert public key registered for the Wechatpay-Serial header, against the base64-decoded Wechatpay-Signature. Any result other than 1 throws InvalidSignatureException — the message failed authentication: wrong key, altered body, or corrupted signature.
Source
Thrown at src/Pay/Validator.php:68
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 the untouched original PSR-7 message; if the stream was already read, call $request->getBody()->rewind() first — never re-encode JSON before validating.
- Check that the cert mapped to the incoming Wechatpay-Serial is a WeChat Pay platform certificate (not your apiclient merchant cert) and is current; refresh via GET /v3/certificates.
- Reproduce the signed string exactly — timestamp, nonce, raw body and the trailing newline — when debugging or building tests.
- Ensure no proxy/CDN rewrites the body or the Wechatpay-Signature header, so the base64 value survives intact.
- If it persists, log a hash of the exact bytes being verified plus timestamp, nonce and serial, and compare against a freshly downloaded cert.
Example fix
// before: body re-encoded before validation -> bytes no longer match the signature $parsed = json_decode((string) $request->getBody(), true); $rebuilt = $request->withBody(Utils::streamFor(json_encode($parsed))); $app->getValidator()->validate($rebuilt); // Invalid Signature // after: validate the untouched raw message, rewind if already read $request->getBody()->rewind(); $app->getValidator()->validate($request);
Defensive patterns
Strategy: try-catch
Try / catch
use EasyWeChat\Pay\Exceptions\InvalidSignatureException;
try {
$app->getValidator()->validate($request);
} catch (InvalidSignatureException $e) {
// Authentication failed: discard the payload, alert, and reject so
// WeChat Pay retries or gives up. Never process or partially trust it.
Log::warning('wechat-pay signature rejected', [
'serial' => $request->getHeaderLine('Wechatpay-Serial'),
'body_sha256' => hash('sha256', (string) $request->getBody()),
]);
return $response->withStatus(401, 'Verify Fail');
}
// Only start business processing after successful validation Prevention
- Forward the raw request to the validator untouched; rewind PSR-7 streams instead of rebuilding bodies
- Map Wechatpay-Serial strictly to WeChat Pay platform certs, never the merchant apiclient cert
- Refresh platform certs on rotation — a stale key makes every verification fail
- Disable body-rewriting middleware (pretty-print, charset conversion) on notify routes
- For local tests, stub or skip validation instead of faking signatures with the wrong key
When it happens
Trigger: Request body changed between receipt and validation (JSON re-serialization, whitespace/charset/BOM changes, framework normalizing the payload); the serial maps to the wrong cert — the merchant apiclient cert instead of the WeChat Pay platform cert, or a stale pre-rotation cert; signature header mangled by a proxy (base64 corruption); PSR-7 body stream consumed and not rewound so (string) $body returns empty; hand-built test messages signed with the wrong key.
Common situations: Middleware that decodes and re-encodes the raw body on the notify route; stale `platform_certs` after WeChat rotated platform certs; confusing the merchant certificate with the platform certificate; proxies rewriting body or headers; local test harnesses signing with the merchant private key.
Related errors
- -40001
- Encrypt AES ECB failed.
- Decrypt failed
- Read the $certificate failed, please check it whether or nor
- Encrypted message is required, plaintext message rejected.
AI-assisted analysis of w7corp/easywechat@f0cf0a8b83 (2026-08-21).
Data as JSON: /api/errors/bf44b164a576504c.
Report an issue: GitHub.