Request-Signatur

Jede Webhook-Anfrage von Emailit enthält einen X-Emailit-Signature-Header mit einer HMAC-SHA256-Signatur und einen X-Emailit-Timestamp-Header mit dem Unix-Zeitstempel der Signierung. Sie sollten diese Signatur verifizieren, um sicherzustellen, dass die Anfrage authentisch ist und nicht manipuliert wurde.

Funktionsweise

  1. Beim Erstellen eines Webhooks generiert Emailit ein Signatur-Geheimnis für diesen Endpunkt (z.B. whsec_xxxxx)
  2. Für jede Zustellung verknüpft Emailit den Zeitstempel und den rohen Request-Body als {'{'}timestamp{'}'}.{'{'}rawBody{'}'}
  3. Ein HMAC-SHA256-Hash wird über diesen String mit Ihrem Signatur-Geheimnis berechnet
  4. Die Signatur wird im X-Emailit-Signature-Header und der Zeitstempel im X-Emailit-Timestamp-Header übertragen
  5. Ihr Server berechnet die Signatur erneut und vergleicht sie mit dem Header-Wert

Signatur verifizieren

So verifizieren Sie eine Webhook-Signatur:

  1. Extrahieren Sie die Header X-Emailit-Signature und X-Emailit-Timestamp aus der Anfrage
  2. Verknüpfen Sie den Zeitstempel und den rohen Request-Body als {'{'}timestamp{'}'}.{'{'}rawBody{'}'}
  3. Berechnen Sie einen HMAC-SHA256-Hash über diesen String mit Ihrem Webhook-Signatur-Geheimnis
  4. Vergleichen Sie den berechneten Hash mit der Signatur mittels zeitkonstantem Vergleich
  5. Optional: Lehnen Sie Anfragen ab, deren Zeitstempel älter als einige Minuten ist, um Replay-Angriffe zu verhindern
import crypto from 'crypto';

function verifyWebhookSignature(rawBody, signature, timestamp, secret) {
const signedPayload = `${timestamp}.${rawBody}`;
const computed = crypto
.createHmac('sha256', secret)
.update(signedPayload, 'utf8')
.digest('hex');

return crypto.timingSafeEqual(
Buffer.from(computed, 'hex'),
Buffer.from(signature, 'hex')
);
}

// In Ihrem Webhook-Handler:
const rawBody = req.body; // roher Request-Body als String
const signature = req.headers['x-emailit-signature'];
const timestamp = req.headers['x-emailit-timestamp'];
const secret = process.env.WEBHOOK_SIGNING_SECRET;

// Schutz vor Replay-Angriffen (5 Minuten Toleranz)
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (age > 300) {
return res.status(401).send('Anfrage zu alt');
}

if (!verifyWebhookSignature(rawBody, signature, timestamp, secret)) {
return res.status(401).send('Ungültige Signatur');
}
import hmac
import hashlib
import time

def verify_webhook_signature(raw_body, signature, timestamp, secret):
signed_payload = f"{timestamp}.{raw_body}"
computed = hmac.new(
    secret.encode('utf-8'),
    signed_payload.encode('utf-8'),
    hashlib.sha256
).hexdigest()

return hmac.compare_digest(computed, signature)

# Schutz vor Replay-Angriffen (5 Minuten Toleranz)
age = int(time.time()) - int(timestamp)
if age > 300:
raise ValueError("Anfrage zu alt")
function verifyWebhookSignature(
string $rawBody,
string $signature,
string $timestamp,
string $secret
): bool {
$signedPayload = "{$timestamp}.{$rawBody}";
$computed = hash_hmac('sha256', $signedPayload, $secret);
return hash_equals($computed, $signature);
}

// Schutz vor Replay-Angriffen (5 Minuten Toleranz)
$age = time() - intval($timestamp);
if ($age > 300) {
http_response_code(401);
exit('Anfrage zu alt');
}
require 'openssl'

def verify_webhook_signature(raw_body, signature, timestamp, secret)
signed_payload = "#{timestamp}.#{raw_body}"
computed = OpenSSL::HMAC.hexdigest('SHA256', secret, signed_payload)
Rack::Utils.secure_compare(computed, signature)
end

# Schutz vor Replay-Angriffen (5 Minuten Toleranz)
age = Time.now.to_i - timestamp.to_i
raise 'Anfrage zu alt' if age > 300
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
"strconv"
)

func verifyWebhookSignature(rawBody, signature, timestamp, secret string) bool {
signedPayload := fmt.Sprintf("%s.%s", timestamp, rawBody)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(signedPayload))
computed := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(computed), []byte(signature))
}

// Schutz vor Replay-Angriffen (5 Minuten Toleranz)
ts, _ := strconv.ParseInt(timestamp, 10, 64)
if time.Now().Unix()-ts > 300 {
// Anfrage ablehnen
}

Wichtige Hinweise

  • Die Signatur wird über {'{'}timestamp{'}'}.{'{'}rawBody{'}'} berechnet — verwenden Sie immer den rohen Request-Body-String, nicht eine neu serialisierte Version (z.B. JSON.stringify(req.body)). Unterschiede in Leerzeichen oder Schlüssel-Reihenfolge führen zu einer ungültigen Signatur.
  • Verwenden Sie immer eine zeitkonstante Vergleichsfunktion (z.B. crypto.timingSafeEqual, hmac.compare_digest, hash_equals), um Timing-Angriffe zu verhindern.
  • Erwägen Sie, Anfragen abzulehnen, deren X-Emailit-Timestamp älter als einige Minuten ist, um Replay-Angriffe zu verhindern.
  • Halten Sie Ihr Signatur-Geheimnis sicher — speichern Sie es in Umgebungsvariablen, nicht im Quellcode.
  • Das Signatur-Geheimnis hat das Präfix whsec_ (z.B. whsec_xxxxx). Verwenden Sie den vollständigen Wert einschließlich des Präfixes bei der HMAC-Berechnung.
  • Ihr Webhook-Signatur-Geheimnis finden Sie im Emailit-Dashboard unter den Webhook-Einstellungen oder über die Webhooks-API.
Lokalisiert durch KI