Skip to content

HMAC: Nedir, Nasıl Çalışır ve Ne Zaman Yanlış Araçtır

Webhook, imzalı URL ve servisler arası kimlik doğrulama için pratik bir HMAC-SHA-256 rehberi: üç dilde çalışan kod ve dijital imzaya geçmenin gerektiği sınır.

Webhook alıcıları, imzalı indirme bağlantıları ve servisler arası iç çağrıların hepsi aynı soruya cevap arar: bu istek gerçekten paylaşımlı sırra sahip taraftan mı geldi ve bozulmadan mı ulaştı? Sabit zamanlı karşılaştırma, ham gövde üzerinden hash alma ve zaman kayması toleransının aynı anda doğru olması gerekir; aksi halde koruma süs olarak kalır. Her iki tarafın da aynı sırrı tutabildiği simetrik güven durumunda bütünlük ve özgünlük için doğru varsayılan, standart kütüphanenin sabit zamanlı karşılaştırma fonksiyonuyla birlikte HMAC-SHA-256'dır; bu yazı primitifi tanımlıyor, üç dilde çalışan kod örnekleri veriyor ve HMAC'in artık doğru araç olmaktan çıktığı sınırı çiziyor.

HMAC'in tam tanımı

HMAC, "Hash-based Message Authentication Code" ifadesinin kısaltmasıdır. RFC 2104 yapıyı H(K_opad, H(K_ipad, mesaj)) olarak tanımlar. Burada K_ipad, anahtarın altta yatan hash'in blok boyutuna kadar tekrarlanan 0x36 baytı ile XOR'lanmış hali; K_opad ise aynı şekilde 0x5C baytı ile XOR'lanmış halidir. İki katmanlı hash yapısı, SHA-256 gibi Merkle-Damgard hash'leri üzerinde basit bir H(sır || mesaj) çözümünü kıran uzunluk genişletme saldırılarına karşı HMAC'i güvenli kılan unsurdur. FIPS 198-1 resmi standarttır ve bugün varsayılan örnekleme HMAC-SHA-256'dır.

Doğrulayıcı MAC'i yeniden hesaplar ve gelen etiketle karşılaştırır. Bu karşılaştırma sabit zamanlı olmak zorundadır; yani ilk farklı baytın nerede olduğuna bakmaksızın bütün baytları inceler. == veya strcmp ilk farkta erken döner ve bu erken dönüş, farkın konumunu zamanlama üzerinden sızdırır; saldırgan yeterince istekle etiketi bayt bayt geri çıkarabilir. Yaygın kullanılan her dilin standart kütüphanesi sabit zamanlı bir karşılaştırma sunar; o fonksiyonu kullanın.

Yapının kendisi ucuzdur. Kilobayt boyutundaki bir yük için tek haneli mikrosaniye ölçeğinde bir maliyet tipiktir; bu nedenle HMAC neredeyse hiç performans profilinde görünmez.

Üç dilde uygulama

Aynı şekil her dilde işler: göndericinin hash'lediği ham baytları hash'leyin, başlığı bayta çevirin, ardından sabit zamanlı karşılaştırın. Node'un timingSafeEqual fonksiyonu uzunluk uyuşmazlığında istisna fırlatır; bu nedenle Node örneği karşılaştırmadan önce açık bir uzunluk kontrolü yapar. Python'un compare_digest ve Go'nun hmac.Equal fonksiyonları zaten eşit olmayan uzunlukta false döndüğü için bu örneklerde açık bir uzunluk kontrolü gereksizdir ve sadece gürültü olur.

typescript
// Node.js: webhook imzasını doğrulamaimport { createHmac, timingSafeEqual } from "node:crypto";
export function verify(  rawBody: Buffer,  headerSig: string,  secret: string,): boolean {  if (!/^[0-9a-f]{64}$/i.test(headerSig)) return false;  const expected = createHmac("sha256", secret).update(rawBody).digest();  const received = Buffer.from(headerSig, "hex");  if (expected.length !== received.length) return false;  return timingSafeEqual(expected, received);}

Buffer.from(str, "hex") hatalı girdide istisna fırlatmaz; hex olmayan karakterleri ve tek uzunluklu girdilerin son nibble'ını sessizce atar, çağıranın beklediğinden daha kısa bir tampon döndürür. HMAC-SHA-256 her zaman 32 baytlık bir etiket ürettiği için regex tam 64 hex karakteri zorunlu kılar; bu, kod çözmeden önce sessiz kırpma boşluğunu kapatır. Kod çözme sonrası uzunluk kontrolü SHA-256 için artık gereksizdir ancak savunma derinliği olarak korunur.

python
# Python: aynı şekil, stdlib hmacimport hmacimport hashlib
def verify(raw_body: bytes, header_sig: str, secret: bytes) -> bool:    expected = hmac.new(secret, raw_body, hashlib.sha256).digest()    try:        received = bytes.fromhex(header_sig)    except ValueError:        return False    return hmac.compare_digest(expected, received)
go
// Go: crypto/hmac.Equal sabit zamanlıdırpackage webhook
import (    "crypto/hmac"    "crypto/sha256"    "encoding/hex")
func Verify(rawBody []byte, headerSig string, secret []byte) bool {    mac := hmac.New(sha256.New, secret)    mac.Write(rawBody)    expected := mac.Sum(nil)    received, err := hex.DecodeString(headerSig)    if err != nil {        return false    }    return hmac.Equal(expected, received)}

Üç ayrıntı güvenliği taşır. Birincisi, girdi ayrıştırılıp yeniden dize haline getirilmiş bir JSON değil, gövdenin ham bayt akışıdır. İkincisi, karşılaştırma dilin eşitlik operatörüyle değil, standart kütüphane primitifiyle (timingSafeEqual, compare_digest, hmac.Equal) yapılır. Üçüncüsü, sır opak bayt dizisi olarak ele alınır; aynı karşılaştırmaya bir tarafa hex kodlu dize, diğer tarafa ikili sindirim verirseniz uzunluk uyuşmazlığı hatasıyla karşılaşırsınız.

HMAC'in doğru araç olduğu durumlar

İki tarafın da meşru biçimde bir sırrı paylaştığı ve hedefin bütünlük ile özgünlük olduğu her durumda bu yapı oturur.

Webhook alıcıları. Stripe, Stripe-Signature: t=<unix-ts>,v1=<hex> biçiminde başlık gönderir; v1 değeri t.ham_govde dizisi üzerinden hesaplanan HMAC-SHA-256'dır. Zaman damgası imzalanan içeriğin parçası olduğundan alıcı, beş dakikalık bir pencerenin dışındaki istekleri reddederek basit tekrar saldırılarını engelleyebilir. GitHub aynı yaklaşımı X-Hub-Signature-256 üzerinden uygular. Shopify ham gövdeyi imzalar ve etiketi X-Shopify-Hmac-Sha256 başlığıyla yollar; ancak değer Stripe ve GitHub'daki gibi hex değil, base64'tür. Yukarıdaki Node doğrulayıcıyı Shopify uç noktasında kullanacaksanız Buffer.from(headerSig, "hex") çağrısını Buffer.from(headerSig, "base64") ile değiştirin ve hex regex'ini kaldırın.

İmzalı URL'ler. Kısa ömürlü bir indirme veya yükleme bağlantısı, parametreleri (son kullanma, kaynak, metot) ve bunların üzerinde hesaplanan bir HMAC etiketini taşır. Sunucu her istekte etiketi yeniden hesaplar. Okuyucunun açık anahtar altyapısına ihtiyacı yoktur; ihraç eden ile doğrulayan aynı taraftır.

Güvenilen eşler arasında iç servis kimlik doğrulaması. Aynı operatörün altında çalışan ve sır deposundan dönen ortak bir anahtarı paylaşan iki servis, istek zarflarını HMAC ile imzalayabilir. Bu, mTLS veya servis mesh'inden önce gelen prototip katmanı iç kimlik doğrulama yöntemidir; kalıcı çözüm değildir ama dürüst ve küçüktür.

İstek imzalama protokolleri. AWS Signature Version 4, tarih, bölge ve servis üzerinden HMAC zincirleyerek isteğe özel bir imzalama anahtarı türetir: kDate = HMAC("AWS4"+kSecret, YYYYMMDD), ardından kRegion, kService ve son olarak kSigning = HMAC(kService, "aws4_request"). İmza HMAC(kSigning, stringToSign) olur. Zincir, herhangi bir türetilmiş anahtarın etki alanını daraltır. Bu desen, dahili imzalama altyapıları için ödünç almaya değer bir yaklaşımdır.

HMAC'in yanlış araç olduğu durumlar

HMAC'in yapamayacaklarını adlandırınca sınır netleşir.

Parola saklama. HMAC kasıtlı olarak hızlıdır. Parola hash'leme ise kasıtlı olarak yavaş olmalı ve maliyet parametreleri donanımla birlikte ölçeklenebilmelidir. Argon2id veya bcrypt kullanın. OWASP Cryptographic Storage Cheat Sheet bu sınırı açıkça koyar.

Asimetrik güven ve inkar edilemezlik. HMAC'i doğrulayabilen herkes imza üretebilir, çünkü doğrulama ve imzalama aynı anahtarı kullanır. Birden çok tarafın doğrulaması ama yalnızca bir tarafın imzalayabilmesi gerekiyorsa veya üçüncü bir tarafın sonradan hangi tarafın mesajı ürettiğini ispatlaması gerekiyorsa doğru araç dijital imzadır. Modern varsayılan Ed25519'dur; Ed25519 desteklenmediğinde ECDSA P-256 ve RSA-PSS kabul edilebilir alternatiflerdir. İki bilinen servis arasında HS256 ile JWT sorun değildir; ancak tam olarak güvenmediğiniz bir doğrulayıcı havuzuna dağıtılan HS256 jetonları, kılık değiştirmiş asimetrik güven demektir; doğru hamle RS256 veya EdDSA'ya geçmektir.

Oturum veya parola sıfırlama jetonları. Bir oturum kimliği veriden türetilmez; sunucunun sakladığı opak rastgele bir değerdir. CSPRNG (crypto.randomBytes, secrets.token_urlsafe, crypto/rand.Read) kullanın ve değeri gerektiğinde tuzlanıp hash'lenmiş halde oturum deposunda tutun. HMAC bu noktada hiçbir şey katmaz.

Gizlilik. HMAC şifrelemez. Bir saldırganın mesajı okuyamaması gerekiyorsa hem gizlilik hem bütünlük sağlayan AES-GCM veya ChaCha20-Poly1305 gibi kimlik doğrulamalı şifreleme modlarını kullanın.

Operasyonel notlar

Anahtar rotasyonu. Doğrulayıcıyı aynı anda birkaç aktif anahtarı kabul edecek şekilde tasarlayın. Rotasyonda yeni anahtarı en yüksek öncelikle ekleyin ve eskiyi tam bir dağıtım penceresi boyunca canlı tutun. Üreticiler kendi takvimlerine göre yeni anahtara geçer.

typescript
// Çok anahtarlı doğrulayıcı şekliconst ACTIVE_KEYS: ReadonlyArray<{ id: string; secret: string }> = [  { id: "2026-05", secret: process.env.HMAC_KEY_CURRENT! },  { id: "2026-04", secret: process.env.HMAC_KEY_PREVIOUS! },];
export function verifyAny(rawBody: Buffer, headerSig: string): boolean {  for (const { secret } of ACTIVE_KEYS) {    if (verify(rawBody, headerSig, secret)) return true;  }  return false;}

Reddetme yolu bütün anahtarları tarar; geçerli bir imzaya sahip olmayan bir saldırgan, hangi anahtarların aktif olduğuna dair bir zamanlama sinyali alamaz. Kabul yolu ilk eşleşmede döner; elindeki geçerli imzayla bir zamanlama gözlemcisi hangi anahtarın imzaladığını çıkarabilir, fakat bu ancak halihazırda geçerli bir imzaya sahip biri için mümkündür. Pratikte sızıntı düşük etkilidir ve daha basit yapı tercih edilir.

Bu hepsini-dene yapısı, zarfı sizin denetlemediğiniz üçüncü taraf gelen webhook'ları için uygundur. İmzalı URL veya servisler arası RPC'de imzayı atan taraf sizseniz, mesaja anahtar kimliği ve imzalanmış bir zaman damgası yerleştirin; doğrulayıcı anahtarı kimlik üzerinden bulup validFrom <= t <= validUntil aralığını uygulasın. JWT'deki kid ve exp alanları bu desenin kanonik karşılığıdır.

Anahtar uzunluğu. HMAC-SHA-256 için CSPRNG ile üretilmiş 32 baytlık bir sır seçin (SHA-256'nın blok boyutu 64 bayt olsa da güvenlik seviyesi sindirim boyutuyla sınırlıdır). 32 baytlık bir sır, RFC 2104 çevresindeki tarihsel anahtar uzunluğu normalleştirme tartışmasını tamamen ortadan kaldırır.

Kanonikleştirme. Ayrıştırıp yeniden serileştirdiğiniz bir JSON nesnesi üzerinden imza üretmeyin. Boşluklar, anahtar sırası ve Unicode kaçışları farklı serileştiriciler arasında farklılaşır ve etiket doğrulanmaz. Stripe ve GitHub bu nedenle ham istek baytlarını imzalar. Express'te webhook rotasına yalnızca express.raw({ type: "application/json" }) bağlayın ve JSON'u alt katmanda ayrıştırın. Koa'da gövde ayrıştırıcıdan önce bir middleware ile buffer'ı yakalayın. Go ve Python framework'lerinde gövdeyi bir kere bayta okuyun ve aynı buffer'ı hem doğrulayıcıya hem uygulama işleyicisine geçirin.

Zaman damgası ve tekrar. Geçerli bir imza tazeliği ispatlamaz. Zaman damgasını mesajın içinde imzalayın (Stripe'ın t.body şekli), dar bir pencerenin (tipik olarak 300 saniye) dışındaki istekleri reddedin ve tam-bir-kez işleme için (provider, event_id) çiftini bir idempotency tablosunda tutun. İmza sahteyi durdurur; idempotency hem kazara hem kasıtlı tekrarı durdurur.

Başlık enjeksiyonu riskleri. Başlık girdilerini güvenilmeyen bayt olarak ele alın. Beklenmedik kodlama, uzunluk veya format taşıyan imzaları çözmeden önce reddedin. Doğrulayıcı herhangi bir ayrıştırma hatasında istisna fırlatmak yerine reddederek dönmeli; hatalı biçimli bir başlık sabit zamanlı karşılaştırmayı kısa devre yaptırmamalı.

Algoritma hijyeni. HMAC-MD5 ve HMAC-SHA-1, MAC olarak teknik anlamda hâlâ kırılmamıştır; ancak kullanımları denetim bulgularına ve kötü sinyallere yol açar. Eski bir karşı taraf zorlamadıkça SHA-256'yı seçin; eski neden gerektiğinde çağrı noktasının yanına bir yorumla belgeleyin.

Kapanış

HMAC'in doğru yerine oturduğu nokta, simetrik güven altında bütünlük ve özgünlük; bunun için ham gövde hash'i ve sabit zamanlı karşılaştırma yeterlidir. Sınır da nettir: parola hash'leme, inkar edilemezlik, asimetrik doğrulayıcı havuzları, oturum jetonları ve gizlilik için farklı primitifler gerekir. Önünüzde bir webhook alıcısı, imzalı URL üreticisi veya iç RPC katmanı varsa atılacak adım karşılaştırmayı denetlemektir. Bir MAC'e karşı kullanılan ==, === veya strcmp çağrılarını timingSafeEqual, hmac.compare_digest veya hmac.Equal ile değiştirin; ardından doğrulayıcının yeniden serileştirilmiş JSON yerine ham baytları okuduğundan emin olun.

Kaynaklar

İlgili Yazılar