|null $body * @return array */ public function signRequest(string $method, string $path, ?array $body): array { $timestamp = (string) time(); $nonce = (string) Str::uuid(); $bodyString = $body !== null ? $this->stableStringify($body) : ''; $bodyHash = hash('sha256', $bodyString); $canonical = implode("\n", [ strtoupper($method), $path, $bodyHash, $timestamp, $nonce, ]); $signature = hash_hmac('sha256', $canonical, $this->clientSecret); return [ 'Content-Type' => 'application/json', 'Accept' => 'application/json', 'X-Client-Id' => $this->clientId, 'X-Timestamp' => $timestamp, 'X-Nonce' => $nonce, 'X-Signature' => $signature, ]; } /** * @param array $query */ public function verifyReturnSignature(array $query): bool { $signature = (string) ($query['signature'] ?? ''); if ($signature === '' || !ctype_xdigit($signature)) { return false; } unset($query['signature']); // فقط اسکالرها — همه string طبق مستند پارتنر $payload = []; foreach ($query as $key => $value) { if (!is_string($key) || is_array($value) || $value === null) { continue; } $payload[$key] = (string) $value; } $expected = hash_hmac('sha256', $this->stableStringify($payload), $this->clientSecret); return $this->hashEqualsHex($signature, $expected); } /** * @param array $body */ public function verifyWebhookSignature(array $body, ?string $signatureHeader): bool { if ($signatureHeader === null || $signatureHeader === '') { return false; } $expected = hash_hmac('sha256', $this->stableStringify($body), $this->clientSecret); return $this->hashEqualsHex($signatureHeader, $expected); } public function isTimestampFresh(int|string|null $timestamp, int $skewSeconds = 300): bool { if ($timestamp === null || $timestamp === '') { return false; } return abs(time() - (int) $timestamp) <= $skewSeconds; } /** * @param mixed $value */ public function stableStringify($value): string { if ($value === null || !is_array($value)) { return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } if (array_is_list($value)) { $parts = array_map(fn ($item) => $this->stableStringify($item), $value); return '[' . implode(',', $parts) . ']'; } ksort($value); $parts = []; foreach ($value as $key => $item) { $parts[] = '"' . $key . '":' . $this->stableStringify($item); } return '{' . implode(',', $parts) . '}'; } private function hashEqualsHex(string $a, string $b): bool { if (!ctype_xdigit($a) || !ctype_xdigit($b) || strlen($a) !== strlen($b)) { return false; } return hash_equals(strtolower($a), strtolower($b)); } }