Przejdź do głównej zawartości

Podpis HMAC

Każdy webhook wysyłany przez DPay Web EID jest podpisany cyfrowo algorytmem HMAC-SHA256. Weryfikacja podpisu pozwala potwierdzić, że webhook rzeczywiście pochodzi od DPay i nie został zmodyfikowany w drodze.

Bezpieczeństwo

Zawsze weryfikuj podpis przed przetworzeniem webhooka. Bez weryfikacji każdy może wysłać fałszywy webhook na Twój endpoint i podszyć się pod DPay.

Algorytm

signature = HMAC-SHA256(secret, "{timestamp}.{raw_body}")

Gdzie:

  • secret - klucz HMAC (whsec_xxx) otrzymany przy rejestracji webhooka
  • timestamp - wartość nagłówka X-Timestamp (Unix timestamp)
  • raw_body - dokładnie surowe ciało zapytania (przed JSON parse)

Kroki weryfikacji

  1. Pobierz X-Timestamp i X-Signature z nagłówków HTTP
  2. Odczytaj surowe ciało zapytania (raw body, nie sparsowany JSON)
  3. Wylicz: HMAC-SHA256(secret, "{timestamp}.{raw_body}")
  4. Porównaj wynik z wartością X-Signature po usunięciu prefiksu sha256=
  5. Użyj funkcji odpornej na timing attacks (hash_equals, hmac.compare_digest)

Przykład PHP

<?php
function verifyWebhookSignature(
string $payload,
string $signature,
string $timestamp,
string $secret
): bool {
$expected = hash_hmac('sha256', "{$timestamp}.{$payload}", $secret);
$actual = str_replace('sha256=', '', $signature);

return hash_equals($expected, $actual);
}

// Użycie w kontrolerze:
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_TIMESTAMP'] ?? '';
$secret = getenv('DPAY_EID_WEBHOOK_SECRET'); // whsec_xxx

if (!verifyWebhookSignature($payload, $signature, $timestamp, $secret)) {
http_response_code(401);
exit('Invalid signature');
}

// Podpis OK - przetwarzaj zdarzenie
$event = json_decode($payload, true);
$eventType = $event['type'];
$sessionId = $event['data']['verification_session_id'];

switch ($eventType) {
case 'verification.completed':
$resultStatus = $event['data']['result_status'];
// Pobierz pełny wynik z /verifications/{id}/result
break;
case 'verification.failed':
// Obsługa odrzucenia
break;
}

http_response_code(200);
echo 'OK';

Przykład Python (Flask)

import hmac
import hashlib
import os
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = os.environ['DPAY_EID_WEBHOOK_SECRET']

def verify_signature(payload: bytes, signature: str, timestamp: str) -> bool:
"""Weryfikuje podpis HMAC-SHA256 webhooka."""
message = f"{timestamp}.{payload.decode('utf-8')}"
expected = hmac.new(
WEBHOOK_SECRET.encode(),
message.encode(),
hashlib.sha256
).hexdigest()
actual = signature.replace("sha256=", "")
return hmac.compare_digest(expected, actual)

@app.route("/webhooks/dpay", methods=["POST"])
def handle_webhook():
payload = request.get_data() # surowe body, NIE request.json
signature = request.headers.get("X-Signature", "")
timestamp = request.headers.get("X-Timestamp", "")

if not verify_signature(payload, signature, timestamp):
abort(401)

event = request.get_json()
event_type = event["type"]

if event_type == "verification.completed":
session_id = event["data"]["verification_session_id"]
result_status = event["data"].get("result_status")
# Przetwarzaj wynik...

return "", 200

Przykład Node.js (Express)

const crypto = require('crypto');
const express = require('express');
const app = express();

const WEBHOOK_SECRET = process.env.DPAY_EID_WEBHOOK_SECRET;

// Ważne: zapisz raw body, NIE używaj express.json() przed weryfikacją
app.use('/webhooks/dpay', express.raw({ type: 'application/json' }));

function verifySignature(payload, signature, timestamp) {
const message = `${timestamp}.${payload.toString('utf-8')}`;
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(message)
.digest('hex');
const actual = signature.replace('sha256=', '');

// Timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(actual, 'hex')
);
}

app.post('/webhooks/dpay', (req, res) => {
const signature = req.headers['x-signature'] || '';
const timestamp = req.headers['x-timestamp'] || '';

if (!verifySignature(req.body, signature, timestamp)) {
return res.status(401).send('Invalid signature');
}

const event = JSON.parse(req.body.toString('utf-8'));

switch (event.type) {
case 'verification.completed':
// Przetwarzaj wynik
break;
case 'verification.failed':
// Obsługa odrzucenia
break;
}

res.status(200).send('OK');
});

Najczęstsze błędy

BłądPrzyczynaRozwiązanie
Sygnatura zawsze niezgodnaUżywasz sparsowanego JSON zamiast raw bodyCzytaj surowe ciało zapytania (php://input, request.get_data(), req.body z express.raw())
Sygnatura niezgodna sporadycznieMiddleware modyfikuje body (np. dodaje whitespace)Zarejestruj raw body przed wszystkimi middleware'ami JSON
hash_equals rzuca błądRóżne długości stringówNajpierw sprawdź czy nagłówki są obecne, dopiero potem porównuj
401 na produkcji, OK na sandboxieInny secret per środowiskoKażde środowisko ma własny webhook + własny secret
Timing attackUżywanie === zamiast hash_equalsZawsze używaj timing-safe comparison

Replay attack protection

Dodatkowo możesz weryfikować że X-Timestamp jest świeży (np. nie starszy niż 5 minut):

$timestamp = (int) $_SERVER['HTTP_X_TIMESTAMP'];
$now = time();

if (abs($now - $timestamp) > 300) { // 5 minut
http_response_code(401);
exit('Webhook timestamp too old');
}

To zabezpiecza przed atakami typu replay - odtworzeniem starszego, nagranego wcześniej webhooka.