300) return false; // 2. Nonce replay protection try { $redis = \App\Core\Redis::getInstance(); $nonceKey = 'hmac_nonce:' . $nonce; if ($redis->exists($nonceKey)) return false; // Replay attack $redis->setex($nonceKey, 600, '1'); // TTL 10 minutes } catch (\Throwable $e) { // Redis unavailable — log but don't fail (degrade gracefully) error_log('[HMAC] Redis unavailable for nonce check: ' . $e->getMessage()); } // 3. Build & compare signature $bodyHash = hash('sha256', $body); $stringToSign = strtoupper($method) . "\n" . $path . "\n" . $timestamp . "\n" . $nonce . "\n" . $bodyHash; $calculated = hash_hmac('sha256', $stringToSign, $secret); return hash_equals($calculated, $signature); } public function sign(string $secret, string $method, string $path, string $timestamp, string $nonce, string $body): string { $bodyHash = hash('sha256', $body); $stringToSign = strtoupper($method) . "\n" . $path . "\n" . $timestamp . "\n" . $nonce . "\n" . $bodyHash; return hash_hmac('sha256', $stringToSign, $secret); } }