value (any case), e.g. getallheaders(). Read the raw body with * file_get_contents('php://input') and verify BEFORE json_decode. */ public static function verify(string $secretKey, string $merchantCode, array $headers, string $method, string $path, string $rawBody, ?int $nowMs = null): string { $h = array_change_key_case($headers, CASE_LOWER); $code = (string) ($h['x-merchant-code'] ?? ''); $rid = (string) ($h['x-request-id'] ?? ''); $ts = (string) ($h['x-timestamp'] ?? ''); $sig = strtolower((string) ($h['x-signature'] ?? '')); if ($code === '' || $rid === '' || $ts === '' || $sig === '' || $code !== $merchantCode) { return self::INVALID_SIGNATURE; } if (!ctype_digit($ts)) { return self::TIMESTAMP_EXPIRED; } $now = $nowMs ?? self::nowMs(); if (abs($now - (int) $ts) > self::TIMESTAMP_WINDOW_MS) { return self::TIMESTAMP_EXPIRED; } $want = self::sign($secretKey, $method, $path, $code, $ts, $rid, $rawBody); return hash_equals($want, $sig) ? self::OK : self::INVALID_SIGNATURE; } /** A response body: {code, message, data}. Business results are always HTTP 200. */ public static function envelope(string $code, string $message = '', ?array $data = null): string { $out = ['code' => $code, 'message' => $message !== '' ? $message : $code]; if ($data !== null) { $out['data'] = $data; } return json_encode($out, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } } if (PHP_SAPI === 'cli' && realpath($_SERVER['SCRIPT_FILENAME'] ?? '') === __FILE__) { $key = 'test-secret-key-0001'; $path = '/wallet/debit'; $code = 'M0001'; $ts = '1792000000000'; $rid = 'req-0001'; $body = '{"txnId":"T7K2M9QXH4ZP8WNA","amount":"10.00"}'; $check = function (bool $ok, string $what) { if (!$ok) { fwrite(STDERR, "FAIL: $what\n"); exit(1); } }; $check(MerchantSign::bodySha256($body) === 'b8d0525520debc2d5e2c62e83d2303a4cd29ed0c18c4139e98196c76c14fc2b8', 'body hash'); $sig = MerchantSign::sign($key, 'POST', $path, $code, $ts, $rid, $body); $check($sig === 'e5273ee9fb4c05408af2f8b32b26b4b98bc891fb77e5ffe5b46f9c112ebe6589', "signature $sig"); $h = ['X-Merchant-Code' => $code, 'X-Request-Id' => $rid, 'X-Timestamp' => $ts, 'X-Signature' => $sig]; $now = 1792000000000; $check(MerchantSign::verify($key, $code, $h, 'POST', $path, $body, $now) === MerchantSign::OK, 'verify'); $check(MerchantSign::verify($key, $code, $h, 'POST', $path, str_replace('10.00', '10.01', $body), $now) === MerchantSign::INVALID_SIGNATURE, 'tampered'); $check(MerchantSign::verify($key, 'M0002', $h, 'POST', $path, $body, $now) === MerchantSign::INVALID_SIGNATURE, 'other merchant'); $check(MerchantSign::verify($key, $code, $h, 'POST', $path, $body, $now + MerchantSign::TIMESTAMP_WINDOW_MS + 1) === MerchantSign::TIMESTAMP_EXPIRED, 'stale'); echo "MerchantSign.php: self-test passed $sig\n"; }