Firma della Richiesta
Ogni richiesta webhook da Emailit include un header X-Emailit-Signature contenente una firma HMAC-SHA256 e un header X-Emailit-Timestamp con il timestamp Unix di quando la richiesta è stata firmata. È necessario verificare questa firma per assicurarsi che la richiesta sia autentica e non sia stata manomessa.
Come funziona
- Quando crei un webhook, Emailit genera un segreto di firma per quell'endpoint
- Per ogni consegna, Emailit concatena il timestamp e il corpo grezzo della richiesta come
{timestamp}.{rawBody} - Viene calcolato un hash HMAC-SHA256 su quella stringa utilizzando il tuo segreto di firma
- La firma viene inviata nell'header
X-Emailit-Signaturee il timestamp inX-Emailit-Timestamp - Il tuo server ricalcola la firma e la confronta con il valore dell'header
Verifica della firma
Per verificare una firma webhook:
- Estrai gli header
X-Emailit-SignatureeX-Emailit-Timestampdalla richiesta - Concatena il timestamp e il corpo grezzo della richiesta come
{timestamp}.{rawBody} - Calcola un hash HMAC-SHA256 di quella stringa utilizzando il tuo segreto di firma webhook
- Confronta l'hash calcolato con la firma utilizzando un confronto sicuro temporalmente
- Opzionalmente, rifiuta le richieste dove il timestamp è più vecchio di alcuni minuti per proteggerti dagli attacchi replay
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')
);
}
// Nel tuo gestore webhook:
const rawBody = req.body; // corpo grezzo della richiesta come stringa
const signature = req.headers['x-emailit-signature'];
const timestamp = req.headers['x-emailit-timestamp'];
const secret = process.env.WEBHOOK_SIGNING_SECRET;
// Protezione contro attacchi replay (tolleranza di 5 minuti)
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (age > 300) {
return res.status(401).send('Richiesta troppo vecchia');
}
if (!verifyWebhookSignature(rawBody, signature, timestamp, secret)) {
return res.status(401).send('Firma non valida');
}
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)
# Protezione contro attacchi replay (tolleranza di 5 minuti)
age = int(time.time()) - int(timestamp)
if age > 300:
raise ValueError("Richiesta troppo vecchia")
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);
}
// Protezione contro attacchi replay (tolleranza di 5 minuti)
$age = time() - intval($timestamp);
if ($age > 300) {
http_response_code(401);
exit('Richiesta troppo vecchia');
}
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
# Protezione contro attacchi replay (tolleranza di 5 minuti)
age = Time.now.to_i - timestamp.to_i
raise 'Richiesta troppo vecchia' 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))
}
// Protezione contro attacchi replay (tolleranza di 5 minuti)
ts, _ := strconv.ParseInt(timestamp, 10, 64)
if time.Now().Unix()-ts > 300 {
// rifiuta richiesta
}
Verifica firma
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')
);
}
// Nel tuo gestore webhook:
const rawBody = req.body; // corpo grezzo della richiesta come stringa
const signature = req.headers['x-emailit-signature'];
const timestamp = req.headers['x-emailit-timestamp'];
const secret = process.env.WEBHOOK_SIGNING_SECRET;
// Protezione contro attacchi replay (tolleranza di 5 minuti)
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (age > 300) {
return res.status(401).send('Richiesta troppo vecchia');
}
if (!verifyWebhookSignature(rawBody, signature, timestamp, secret)) {
return res.status(401).send('Firma non valida');
}
Note importanti
- La firma viene calcolata su
{timestamp}.{rawBody}— utilizza sempre la stringa del corpo grezzo della richiesta, non una versione ri-serializzata (ad es.JSON.stringify(req.body)). Differenze negli spazi bianchi o nell'ordinamento delle chiavi comprometteranno la firma. - Utilizza sempre una funzione di confronto sicuro temporalmente (ad es.
crypto.timingSafeEqual,hmac.compare_digest,hash_equals) per prevenire attacchi temporali. - Considera di rifiutare le richieste dove l'
X-Emailit-Timestampè più vecchio di alcuni minuti per proteggerti dagli attacchi replay. - Mantieni sicuro il tuo segreto di firma — conservalo nelle variabili d'ambiente, non nel codice sorgente.
- Puoi trovare il tuo segreto di firma webhook nella dashboard di Emailit sotto le impostazioni webhook, o tramite l'API Webhooks.