{"record":{"id":"bf44b164a576504c","repo":"w7corp/easywechat","slug":"invalid-signature","errorCode":null,"errorMessage":"Invalid Signature","messagePattern":"Invalid Signature","errorType":"exception","errorClass":"InvalidSignatureException","httpStatus":null,"severity":"error","filePath":"src/Pay/Validator.php","lineNumber":68,"sourceCode":"            throw new InvalidSignatureException('Clock Offset Exceeded');\n        }\n\n        $publicKey = $this->merchant->getPlatformCert($serial);\n\n        if (! $publicKey) {\n            throw new InvalidConfigException(\n                \"No platform certs found for serial: {$serial}, \n                please download from wechat pay and set it in merchant config with key `certs`.\"\n            );\n        }\n\n        if (\\openssl_verify(\n            $message,\n            base64_decode($signature),\n            strval($publicKey),\n            OPENSSL_ALGO_SHA256\n        ) !== 1) {\n            throw new InvalidSignatureException('Invalid Signature');\n        }\n    }\n}\n","sourceCodeStart":50,"sourceCodeEnd":72,"githubUrl":"https://github.com/w7corp/easywechat/blob/f0cf0a8b8361417ed683b8246d0ecbaf0aafcaa8/src/Pay/Validator.php#L50-L72","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: body re-encoded before validation -> bytes no longer match the signature\n$parsed = json_decode((string) $request->getBody(), true);\n$rebuilt = $request->withBody(Utils::streamFor(json_encode($parsed)));\n$app->getValidator()->validate($rebuilt); // Invalid Signature\n\n// after: validate the untouched raw message, rewind if already read\n$request->getBody()->rewind();\n$app->getValidator()->validate($request);","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"use EasyWeChat\\Pay\\Exceptions\\InvalidSignatureException;\n\ntry {\n    $app->getValidator()->validate($request);\n} catch (InvalidSignatureException $e) {\n    // Authentication failed: discard the payload, alert, and reject so\n    // WeChat Pay retries or gives up. Never process or partially trust it.\n    Log::warning('wechat-pay signature rejected', [\n        'serial' => $request->getHeaderLine('Wechatpay-Serial'),\n        'body_sha256' => hash('sha256', (string) $request->getBody()),\n    ]);\n\n    return $response->withStatus(401, 'Verify Fail');\n}\n\n// Only start business processing after successful validation","preventionTips":["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"],"tags":["wechat-pay","signature","openssl","webhook","security","php"],"backgroundTag":"signature-verification-failed","analyzedSha":"f0cf0a8b8361417ed683b8246d0ecbaf0aafcaa8","analyzedAt":"2026-08-21T05:29:19.565Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}