[NODE JS] Crypto ile HMAC İmza Oluşturma
Selamlar, bu konuda Node.js'in built-in crypto modülü ile HMAC imza oluşturmayı anlatacağım. HMAC yani Hash-based Message Authentication Code, bir mesajın hem bütünlüğünü hem de kaynağını doğrulamak için kullanılır. Webhook'lardan API güvenliğine kadar birçok yerde karşınıza çıkar.
Önce şunu anlamak lazım: normal hash ile HMAC arasında ciddi bir fark var. Normal hash'te sadece veriyi hashlersiniz, herkes aynı veriyi hashleyip aynı sonucu alabilir. Ama HMAC'ta bir secret key kullanılır. O key'e sahip olmayan biri aynı hash'i üretemez. Bu sayede mesajın gerçekten beklenen kaynaktan geldiğini doğrulayabilirsiniz.
Ben bununla en çok webhook doğrulamasında karşılaştım. Stripe, GitHub, Shopify gibi servisler webhook gönderirken bir HMAC imzası eklerler. Siz de gelen isteğin gerçekten o servisten geldiğini bu imzayı doğrulayarak anlarsınız. Sahte istekleri böyle eleyebilirsiniz, yoksa herkes sizin webhook endpoint'inize sahte veri gönderebilir.
Peki pratikte nasıl yapılır? Node.js'te crypto modülünün createHmac metodu ile tek satırda HMAC üretebilirsiniz. Ama dikkat etmeniz gereken ince noktalar var, özellikle timing attack konusu çok önemli.
Basit HMAC Oluşturma
JavaScript:
const crypto = require('crypto');
// HMAC oluşturma
function hmacOlustur(veri, secretKey) {
return crypto
.createHmac('sha256', secretKey)
.update(veri)
.digest('hex');
}
const secretKey = 'benim-gizli-anahtarim-2024';
const mesaj = 'Bu mesajin butunlugunu dogrulayacagiz';
const imza = hmacOlustur(mesaj, secretKey);
console.log('HMAC imza:', imza);
// Cikti: 64 karakterlik hex string
// Farkli algoritmalar
const sha512Hmac = crypto.createHmac('sha512', secretKey).update(mesaj).digest('hex');
const sha1Hmac = crypto.createHmac('sha1', secretKey).update(mesaj).digest('hex');
console.log('SHA-256:', imza.length, 'karakter');
console.log('SHA-512:', sha512Hmac.length, 'karakter');
console.log('SHA-1:', sha1Hmac.length, 'karakter');
// Base64 formatinda
const base64Hmac = crypto.createHmac('sha256', secretKey).update(mesaj).digest('base64');
console.log('Base64 HMAC:', base64Hmac);
createHmac metoduna ilk parametre olarak algoritma adı, ikinci parametre olarak secret key veriyorsunuz. Sonra update ile veriyi ekliyorsunuz ve digest ile çıktı formatını belirliyorsunuz. Hex en yaygın kullanılan format ama base64 de tercih edilebilir, özellikle HTTP header'larında.
Webhook İmza Doğrulama
Gerçek dünyada en çok bu senaryoyla karşılaşırsınız. Bir servis size webhook gönderdiğinde imzayı doğrulamanız gerekir:
JavaScript:
const crypto = require('crypto');
const express = require('express');
const app = express();
// Raw body lazim, JSON parse etmeden once
app.use('/webhook', express.raw({ type: 'application/json' }));
// Guvenli karsilastirma - timing attack'a karsi
function guvenliKarsilastir(a, b) {
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
}
// Stripe webhook dogrulama ornegi
app.post('/webhook/stripe', (req, res) => {
const WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET;
const imzaHeader = req.headers['stripe-signature'];
if (!imzaHeader) {
return res.status(401).json({ hata: 'Imza eksik' });
}
// Stripe imza formatini parse et
const parcalar = imzaHeader.split(',');
const timestamp = parcalar.find(p => p.startsWith('t=')).split('=')[1];
const gelenImza = parcalar.find(p => p.startsWith('v1=')).split('=')[1];
// Beklenen imzayi hesapla
const payload = timestamp + '.' + req.body.toString();
const beklenenImza = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(payload)
.digest('hex');
// Timing-safe karsilastirma
if (!guvenliKarsilastir(gelenImza, beklenenImza)) {
console.log('Webhook imza dogrulanamadi!');
return res.status(401).json({ hata: 'Gecersiz imza' });
}
// Replay attack koruması - timestamp kontrolu
const simdi = Math.floor(Date.now() / 1000);
if (simdi - parseInt(timestamp) > 300) {
return res.status(401).json({ hata: 'Timestamp cok eski' });
}
const event = JSON.parse(req.body.toString());
console.log('Dogrulanmis webhook:', event.type);
res.json({ alindi: true });
});
// Kendi API'niz icin imza olusturma
function apiImzasiOlustur(method, path, body, timestamp, secretKey) {
const imzalanacak = [method, path, timestamp, JSON.stringify(body)].join('\n');
return crypto.createHmac('sha256', secretKey).update(imzalanacak).digest('hex');
}
app.listen(3000);
Dikkat Edilmesi Gerekenler
Birkaç kritik nokta var. Birincisi, imza karşılaştırmasında kesinlikle === operatörü yerine crypto.timingSafeEqual kullanın. Normal karşılaştırma timing attack'a açıktır, yani saldırgan her karakterin eşleşip eşleşmediğine bakarak response süresinden imzayı tahmin edebilir. timingSafeEqual her durumda aynı sürede çalışır.
İkincisi, webhook doğrulamada body'yi raw olarak almanız gerekir. JSON.parse yapıldıktan sonra stringify ederseniz key sıralaması değişebilir ve imza tutmaz. O yüzden express.raw middleware'ini kullanın.
Üçüncüsü, replay attack'a karşı timestamp kontrolü yapın. Gelen webhook 5 dakikadan eskiyse reddedin. Yoksa birisi yakaladığı geçerli bir webhook'u tekrar tekrar gönderebilir.
Son olarak, secret key'lerinizi kesinlikle koda yazmayın. Environment variable kullanın. Benden söylemesi, bir keresinde key'i commit edip GitHub'a pushlamıştım, 10 dakikada botu fark ettim ama o 10 dakikada bile sıkıntı yaşayabilirsiniz.
Kolay gelsin!