Python ile HMAC İmzalama: API'leri Güvende Tutmak

Selamlar, bu yazımda servisten servise iletişimde işime sıkça yarayan bir konuyu, yani HMAC imzalamayı konuşalım. Sadece API key kullanmanın neden yetmediğini, Python'ın hmac ve hashlib modülleriyle imzayı nasıl ürettiğimizi ve sahada en sık tökezlediğimiz yerleri sırayla göstereceğim. Lafı uzatmadan başlayalım.

API key tek başına 'kim olduğunu' söyler, ama 'isteğin yolda değiştirilip değiştirilmediğini' söylemez. Birisi token'ı bir şekilde kapsa - log'dan, proxy'den, eski bir cihazdan - aynı isteği saatler sonra tekrar oynatabilir. HMAC işte bu boşluğu kapatır: paylaşılan bir secret ile, isteğin tamamı üzerinden bir parmak izi üretiriz. Sunucu da aynı izi bağımsız hesaplar, ikisi tutuyorsa istek geçer.

HMAC ne zaman JWT veya OAuth'tan daha mantıklı?

Şahsi kanaatim, HMAC parlak bir tercih oluyor şu durumlarda: iki backend servis birbirine konuşuyor (kullanıcı yok ortada), webhook gönderiyorsunuz (Stripe, GitHub bunu böyle yapıyor), ya da partner entegrasyonu var ve kimlik akışını basit tutmak istiyorsunuz. JWT bir kullanıcı oturumunun taşıyıcısıdır ve genellikle expiry'sine kadar geçerlidir; OAuth ise yetki delegasyonu için tasarlandı. Servis-servis trafikte ikisi de aşırı kalır. HMAC ise her isteği tek tek imzalar, yani çalınan bir token sonsuza kadar açık kapı bırakmaz.

Temel Python implementasyonu

Hadi en sade haliyle bakalım. hmac standart kütüphanede zaten var, ekstra bir bağımlılığa ihtiyacımız yok:

import hmac
import hashlib
import base64
from datetime import datetime, timezone

def sign_request(secret: str, method: str, path: str, body: str = '') -> tuple[str, str]:
    timestamp = datetime.now(timezone.utc).isoformat()
    canonical = f'{method}\n{path}\n{timestamp}\n{body}'
    digest = hmac.new(
        secret.encode('utf-8'),
        canonical.encode('utf-8'),
        hashlib.sha256,
    ).digest()
    signature = base64.b64encode(digest).decode('utf-8')
    return signature, timestamp

Burada kritik nokta canonical string. Hem istemci hem sunucu aynı sırayı, aynı ayırıcıyı kullanmalı. Bir taraf path'in sonuna / koyup öbürü koymazsa, imza tutmaz, siz de saatlerce 'ama secret doğru ya' diye bakarsınız. Tecrübeyle sabittir.

Sunucu tarafında doğrulama

Sunucu tarafında klasik bir hata: imzayı == ile karşılaştırmak. Bunu yapmayın. Karakter karakter karşılaştıran string eşitliği timing attack'a açıktır - saldırgan cevap sürelerinden ilk birkaç karakteri çıkarabilir. Doğru yol hmac.compare_digest:

def verify(secret: str, method: str, path: str, timestamp: str,
           body: str, provided_signature: str) -> bool:
    expected = hmac.new(
        secret.encode('utf-8'),
        f'{method}\n{path}\n{timestamp}\n{body}'.encode('utf-8'),
        hashlib.sha256,
    ).digest()
    expected_b64 = base64.b64encode(expected).decode('utf-8')
    return hmac.compare_digest(expected_b64, provided_signature)

Timestamp'i de mutlaka kontrol edin: bizde tipik tolerans 5 dakika. Bu pencerenin dışındaki istekleri reddedin, böylece eski bir isteği yeniden oynatan birinin elinde sadece kısa bir aralık kalır.

Sık karşılaşılan hatalar

  • Secret'i istemcide saklamak: Mobil uygulamada veya tarayıcıda secret'i tutarsanız orası açık. HMAC, backend'i olan istemciler için. Public bir uygulama yazıyorsanız OAuth düşünün.
  • Secret'in log'a düşmesi: İstek header'larını dump ederken X-Signature ve API key'i mask'leyin. Bir kez Sentry'ye veya CloudWatch'a düşerse işiniz var.
  • Timestamp olmadan imza: Sadece body'yi imzalarsanız, isteği aynen tekrar gönderen biri (replay) sorunsuz geçer. Timestamp + nonce kombinasyonu şart.
  • Yanlış kanonik form: Query parametrelerini sıralamamak, body'nin trailing newline'ını dahil edip etmemek - iki tarafın farklı düşünmesi imzayı bozar. Spec'i yazıya dökün, hem istemci hem sunucu aynı yazılı kurala baksın.
  • == ile karşılaştırma: Yukarıda dedim ama önemli olduğu için tekrar: hmac.compare_digest kullanın.

Kapanış

Bu yazımızda HMAC imzalamanın neden API key'in üstüne bir kat eklediğini, Python'da nasıl ürettiğimizi ve sahadaki tipik tuzakları gördük. Bana sorarsanız servis-servis trafikte HMAC, JWT'ye göre çoğu zaman daha küçük yüzey alanıyla aynı işi görür - özellikle webhook tarafında neredeyse standart bence. Umarım faydalı olur, bir sonraki yazıda görüşmek üzere.