Skip to Content
Webhook'larİmza doğrulama

İmza doğrulama

Her webhook teslimi birlikte kullanılan iki başlık içerir:

X-Signature: sha256=9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b X-Timestamp: 1729536000

sha256=’den sonraki hex string, HMAC-SHA256(secret, timestamp + "." + raw_body)’dir. Nokta gerçek bir bayttır; timestamp ASCII olarak unix-saniyedir.

Neden doğrulamalısınız

Webhook URL’leri sızar. Proxy loglarında, ekran görüntülerinde, tarayıcı geçmişinde, partner destek taleplerinde görünürler. Bir imza kontrolü olmadan, URL’nizi öğrenen herkes sahte bir payment.settled event’i POST edebilir ve sizi ödenmemiş siparişleri karşılamaya kandırabilir. Doğrulama, isteğin InfraIO’dan geldiğini kriptografik olarak kanıtlar.

İmzalanmış payload’ın içine timestamp’i dahil etmek size ayrıca replay koruması sağlar: bir teslim yakalayan saldırgan, imza algılanabilir şekilde eskimeden onu daha sonra yeniden gönderemez.

Algoritma

signed_payload = timestamp + "." + raw_body expected_header = "sha256=" + lowercase_hex( HMAC_SHA256(secret, signed_payload) ) constant_time_compare(expected_header, x_signature_header)

Ardından timestamp’in yakın olduğunu kontrol edin (tipik tolerans: ±5 dakika).

Her zaman ham istek gövdesi baytlarını iletin. Framework’ler genellikle handler’ınız çalışmadan önce JSON’ı ayrıştırır; yeniden stringleştirilmiş sürüm gönderdiğimizden farklı olabilir (anahtar sırası, boşluk, sayı biçimlendirmesi) ve HMAC eşleşmez. Next.js App Router’da JSON.parse’tan önce await req.text() kullanın. Express’te yalnızca webhook rotasında express.raw({ type: 'application/json' }) mount edin.

Uygulamalar

lib/verify-infraio.ts
import { createHmac, timingSafeEqual } from "node:crypto"; const TOLERANCE_SECONDS = 5 * 60; export function verifyInfraIo({ body, signature, timestamp, secret, }: { body: string; // ham metin — ayrıştırılmış JSON DEĞİL signature: string; // X-Signature başlığının değeri timestamp: string; // X-Timestamp başlığının değeri (unix saniye) secret: string; // whsec_… }): boolean { const ts = Number.parseInt(timestamp, 10); if (!Number.isFinite(ts)) return false; if (Math.abs(Math.floor(Date.now() / 1000) - ts) > TOLERANCE_SECONDS) { return false; // çok eski veya gelecekte çok ileri } const expected = "sha256=" + createHmac("sha256", secret) .update(`${timestamp}.${body}`) .digest("hex"); const a = Buffer.from(signature); const b = Buffer.from(expected); return a.length === b.length && timingSafeEqual(a, b); }

Replay koruması

İmzalanmış payload’ın içindeki timestamp ilk savunma hattıdır — bir teslim yakalayan saldırgan, tolerans pencereniz dolduktan sonra onu yeniden gönderemez.

Kemer-ve-pantolon askısı (payment.settled gibi yüksek değerli event’ler için önerilir):

  1. Benzersiz bir kısıtlama ile bir tabloda X-Delivery üzerinde dedup yapın. Tolerans penceresi içindeki replay’ler no-op’a dönüşür — handler’ınız işi iki kez yapmadan 200 döndürür. Bu, meşru yeniden denemeler için istediğiniz aynı idempotency’dir. (X-Delivery bir teslimin her yeniden denemesinde stabildir; payload bir event_id alanı taşımaz.)
  2. Saat sapmanızın izin verdiği en küçük toleransı kullanın. ±5 dakika önerilen varsayılandır ve çoğu NTP-senkronize filonun sürdürebileceği şeyle eşleşir. Daha sıkı olması iyidir; ±30 saniyenin altında, yavaş yukarı yönlü NTP’li ağlarda meşru teslimleri reddetmeye başlarsınız.

Bir secret’ı rotate etme

  1. Panel → Geliştiriciler → Webhook’lar → [uç nokta] → ⋮ → Rotate Secret.
  2. Yeni bir secret oluşturulur ve tam olarak bir kez gösterilir. Diyaloğu kapatmadan önce kopyalayın.
  3. Env var’ınızı güncelleyin ve doğrulayıcınızı 24 saat içinde yeniden dağıtın.

Kayma penceresi (çift imzalama)

Bir rotation’dan sonraki 24 saat boyunca, her teslim iki imza taşır:

X-Signature: sha256=<hmac(new_secret, ts + "." + body)> X-Signature-Prev: sha256=<hmac(prev_secret, ts + "." + body)> X-Timestamp: 1729536000

Önceki secret’ı çalıştıran bir doğrulayıcı X-Signature-Prev ile eşleşir; yeni secret’ı çalıştıran bir doğrulayıcı X-Signature ile eşleşir. Her iki başlığın geçmesi yeterlidir — handler’ınız dağıtımı tutmadan geçiş sırasında teslimi kabul edebilir.

Kayma penceresi kapandıktan sonra yalnızca X-Signature gönderilir. Önceki secret kabul edilmeyi durdurur ve hâlâ onunla yapılandırılmış herhangi bir doğrulayıcı teslimleri reddetmeye başlar — bu yüzden roll-out’unuzu 24 saatlik bütçe içinde bitirin.

Önerilen alıcı deseni

// Bir rotation kayma penceresinde her iki imzayı da kabul edin. const sig = req.headers["x-signature"] ?? ""; const sigPrev = req.headers["x-signature-prev"] ?? ""; const ok = verify(body, sig, ts, CURRENT_SECRET) || (PREV_SECRET && verify(body, sigPrev, ts, PREV_SECRET));

Uç noktanızdaki kayma penceresi süresi dolduğunda ve PREV_SECRET’ı env’inizden kaldırdığınızda, X-Signature-Prev dalını düşürebilirsiniz.

Acil durum iptali

Bir secret public olarak sızdıysa ve önceki secret’ı hemen geçersiz kılmanız gerekiyorsa — yani 24 saatlik çakışmanın bilinen-hatalı bir anahtarı canlı tutmasını istemiyorsanız — iki kez rotate edin. İlk rotation, sızdırılmış secret’ı prev yuvasına taşır; ikinci rotation, onu prev yuvasından çıkarır (hâlâ yeni anahtarla değiştirir), böylece sızdırılmış değer artık kabul edilmez.

Kablolamanızı test etme

Panelde, Geliştiriciler → Webhook’lar’ı açın ve doğrulamak istediğiniz uç noktada Send Test’e tıklayın. URL’ye senkron olarak sentetik bir zarf imzalar ve POST ederiz, ardından HTTP durumunu, gecikmeyi ve yanıtınızın 512 baytlık bir parçacığını gösteririz. Payload şekli:

{ "event_id": "<uuid>", "event_type": "webhook.test.ping", "created_at": "2026-05-17T12:00:00Z", "test": true, "data": { "merchant_id": "<your-merchant-id>", "webhook_id": "<endpoint-id>", "message": "Test ping from the merchant dashboard..." } }

Test ping, production teslimleri ile aynı imzalama şemasını kullanır, bu yüzden bu butondan yeşil bir onay, doğrulayıcınızın gerçek event’leri de kabul ettiğini doğrular. Test ping’leri RMQ yeniden deneme pipeline’ını atlar — yeniden denemeleri çalıştırmak istiyorsanız, ilgili API akışı üzerinden gerçek bir event tetikleyin.

Yaygın arızalar

BelirtiOlası neden
Dev’de her zaman false döndürürGövde HMAC öncesinde JSON-ayrıştırıldı. Önce ham baytları okuyun.
Dün çalışıyordu, bugün başarısızSecret’ı rotate ettiniz ama bu sunucudaki env var hâlâ eskisini içeriyor. Yeni secret ile yeniden dağıtın.
Eski event’lerde başarısız, yenilerde çalışıyorRotation öncesinde bir teslim kuyruğa alınmıştı; imza eski secret’ı kullanıyor ve doğrulayıcınız artık kabul etmiyor. Yeniden denemenin onu düşürmesini bekleyin veya panel üzerinden replay yapın.
Timestamp karşılaştırmasında bir kaymaUnix-saniyeyi unix-saniyeye karşılaştırdığınızdan emin olun. JS’deki Date.now() milisaniyedir — 1000’e bölün.
Yerelde çalışıyor, prod’da başarısızBir proxy (Cloudflare, nginx) açıyor, yeniden kodluyor veya sonundaki bir newline’ı çıkarıyor. Handler’ınızın gördüğü baytları inceleyin.
Test ping 401 / imza uyumsuzluğu diyorDoğrulayıcınız yalnızca body’yi imzalıyor (2026 öncesi şema). timestamp + "." + body imzalamak için güncelleyin.
Başlık tamamen eksikUç nokta farklı bir ortam için kayıtlı. Test modu uç noktaları yalnızca environment=test event’leri alır.