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

  1. Validate the untouched original PSR-7 message; if the stream was already read, call $request->getBody()->rewind() first — never re-encode JSON before validating.
  2. 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.
  3. Reproduce the signed string exactly — timestamp, nonce, raw body and the trailing newline — when debugging or building tests.
  4. Ensure no proxy/CDN rewrites the body or the Wechatpay-Signature header, so the base64 value survives intact.
  5. 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

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


AI-assisted analysis of w7corp/easywechat@f0cf0a8b83 (2026-08-21). Data as JSON: /api/errors/bf44b164a576504c. Report an issue: GitHub.