w7corp/easywechat · error · RuntimeException

-40001

-40001

Error message

Invalid Signature.

What it means

Encryptor::decrypt() recomputes the SHA1 signature over the sorted [token, timestamp, nonce, ciphertext] values and compares it with hash_equals against the msg_signature you supplied. Any mismatch throws RuntimeException('Invalid Signature.', -40001 ERROR_INVALID_SIGNATURE) before decryption is attempted — this is the WeChat callback authenticity check.

Source

Thrown at src/Kernel/Encryptor.php:189

        $attributes = array_map(
            static fn (string|int $attribute): string => (string) $attribute,
            $attributes
        );

        sort($attributes, SORT_STRING);

        return sha1(implode('', $attributes));
    }

    /**
     * @throws RuntimeException
     */
    public function decrypt(string $ciphertext, string $msgSignature, string $nonce, int|string $timestamp): string
    {
        $signature = $this->createSignature($this->token, $timestamp, $nonce, $ciphertext);

        if (! hash_equals($signature, $msgSignature)) {
            throw new RuntimeException('Invalid Signature.', self::ERROR_INVALID_SIGNATURE);
        }

        $plaintext = Pkcs7::unpadding(
            openssl_decrypt(
                base64_decode($ciphertext, true) ?: '',
                'aes-256-cbc',
                $this->aesKey,
                OPENSSL_NO_PADDING,
                iv: substr($this->aesKey, 0, self::BLOCK_SIZE)
            ) ?: '',
            blockSize: strlen($this->aesKey)
        );
        $plaintext = substr($plaintext, self::BLOCK_SIZE);
        $contentLength = (unpack('N', substr($plaintext, 0, 4)) ?: [])[1];

        if ($this->receiveId && trim(substr($plaintext, $contentLength + 4)) !== $this->receiveId) {
            throw new RuntimeException('Invalid appId.', self::ERROR_INVALID_APP_ID);
        }

View on GitHub (pinned to f0cf0a8b83)

Solutions

  1. Compare the token in your config with the Token field of the callback server config in the WeChat admin console — they must match exactly
  2. Make sure timestamp, nonce and msg_signature all come from the same request's query string, and the ciphertext comes from that request's body, without re-encoding
  3. Prefer $server->handle($request) / Server::handle() which extracts the four inputs from the PSR-7 request correctly instead of assembling them manually

Example fix

// before: manual assembly, easy to get wrong
$plaintext = $encryptor->decrypt(
    (string) $request->getContent(),
    $request->query('msg_signature'),
    $request->query('nonce'),
    $request->query('timestamp')
);

// after: let the Server validate and decrypt
$server = $app->getServer();
$server->handle(function (array $message, \asyWeChat\ernel\essage $msg) { /* ... */ });
// inside a dedicated handler, token/nonce/timestamp are taken from the same request
Defensive patterns

Strategy: try-catch

Try / catch

try {
    $plaintext = $encryptor->decrypt($ciphertext, $msgSignature, $nonce, $timestamp);
} catch (\EasyWeChat\Kernel\Exceptions\RuntimeException $e) {
    if ((int) $e->getCode() === \EasyWeChat\Kernel\Encryptor::ERROR_INVALID_SIGNATURE) {
        // do NOT process the message; respond 403 and log for investigation
        abort(403, 'Invalid callback signature');
    }
    throw $e;
}

Prevention

When it happens

Trigger: Handling a WeChat callback with an Encryptor whose token differs from the Token configured in the MP/WeCom admin console; passing a timestamp/nonce taken from the wrong source (body vs query string); double-decoding or re-encoding the msg_signature/ciphertext from the query; or replaying a captured request through a different token.

Common situations: Token mismatch after rotating server config, multiple apps (test/prod) sharing one callback URL, framework middleware urldecoding query params twice, using the encrypted string from the body with the signature of a different field.

Related errors


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