Lewati ke konten utama

Verifikasi Tanda Tangan

Siapa pun yang tahu URL Anda bisa mengirim permintaan palsu. Karena itu selalu verifikasi tanda tangan sebelum memercayai sebuah event.

Format header​

X-Etalasse-Signature: t=1760151845,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  • t adalah waktu kirim dalam detik Unix.
  • v1 adalah tanda tangan heksadesimal. Saat rotasi secret sedang masa tumpang tindih, ada dua nilai v1 di header yang sama.
  • Setiap percobaan ulang ditandatangani ulang dengan t yang baru.

Cara menghitung​

  1. Ambil t dan semua v1 dari header.
  2. Susun teks t + . + body mentah (persis byte yang diterima).
  3. Hitung HMAC_SHA256(secret, teks) dalam heksadesimal. Secret adalah seluruh string whsec_... sebagai UTF-8.
  4. Bandingkan dengan setiap v1 memakai perbandingan waktu konstan. Cukup satu yang cocok.
  5. Tolak jika selisih |waktu sekarang - t| lebih dari 300 detik (5 menit), untuk mencegah pemutaran ulang (replay).
Verifikasi tanda tangan webhook: ambil t dan v1 dari header, hitung HMAC-SHA256 dari t, titik, dan body mentah dengan secret whsec, bandingkan dengan setiap v1 dan periksa selisih waktu 300 detikX-Etalasse-Signaturet=1760151845,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdTeks yang ditandatangani1760151845.{"id":"evt_…","type":…}t + titik + body mentah (byte persis)HMAC-SHA256whsec_…a1B2secret utuh, UTF-8Hasil (heksadesimal)5257a869…e108d8bdSama dengan salah satu v1?timingSafeEqualcocokSelisih waktu?|now - t| <= 300cocokTerimabalas 2xx, lalu prosestidaktidakTolak dengan 400invalid signature123
(1) Gabungkan t, titik, dan body mentah, (2) hitung HMAC-SHA256 dengan seluruh secret whsec_, (3) bandingkan dengan setiap v1 dan periksa batas 300 detik. Selain itu, tolak dengan 400.

(1) Gabungkan t, titik, dan body mentah, (2) hitung HMAC-SHA256 dengan seluruh secret whsec_, (3) bandingkan dengan setiap v1 dan periksa batas 300 detik. Selain itu, tolak dengan 400.

Contoh Node.js (Express)​

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

const app = express();
const SECRET = process.env.ETALASSE_WEBHOOK_SECRET; // whsec_...
const TOLERANCE_SECONDS = 300;

function verify(rawBody, header) {
if (!header) return false;
const parts = header.split(',').map((p) => p.trim());
const t = parts.find((p) => p.startsWith('t='))?.slice(2);
const signatures = parts.filter((p) => p.startsWith('v1=')).map((p) => p.slice(3));
if (!t || signatures.length === 0) return false;

if (Math.abs(Date.now() / 1000 - Number(t)) > TOLERANCE_SECONDS) return false;

const expected = crypto
.createHmac('sha256', SECRET)
.update(`${t}.${rawBody}`)
.digest('hex');
const expectedBuf = Buffer.from(expected, 'utf8');

return signatures.some((sig) => {
const sigBuf = Buffer.from(sig, 'utf8');
return sigBuf.length === expectedBuf.length && crypto.timingSafeEqual(sigBuf, expectedBuf);
});
}

// Important: read the RAW body, not parsed JSON.
app.post('/hooks/etalasse', express.raw({ type: 'application/json' }), (req, res) => {
const rawBody = req.body.toString('utf8');
if (!verify(rawBody, req.get('X-Etalasse-Signature'))) {
return res.status(400).send('invalid signature');
}
const event = JSON.parse(rawBody);
// TODO: skip if event.id was already processed, then handle the event.
res.status(200).send('ok');
});

app.listen(3000);

Contoh PHP​

<?php
$secret = getenv('ETALASSE_WEBHOOK_SECRET'); // whsec_...
$tolerance = 300;

$rawBody = file_get_contents('php://input'); // raw body
$header = $_SERVER['HTTP_X_ETALASSE_SIGNATURE'] ?? '';

$t = null;
$signatures = [];
foreach (explode(',', $header) as $part) {
$part = trim($part);
if (strpos($part, 't=') === 0) {
$t = substr($part, 2);
} elseif (strpos($part, 'v1=') === 0) {
$signatures[] = substr($part, 3);
}
}

$valid = false;
if ($t !== null && count($signatures) > 0 && abs(time() - (int) $t) <= $tolerance) {
$expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
foreach ($signatures as $sig) {
if (hash_equals($expected, $sig)) {
$valid = true;
break;
}
}
}

if (!$valid) {
http_response_code(400);
exit('invalid signature');
}

$event = json_decode($rawBody, true);
// TODO: skip if $event['id'] was already processed, then handle the event.
http_response_code(200);
echo 'ok';

Kesalahan yang sering terjadi​

  • Memakai JSON yang sudah di-parse lalu diserialisasi ulang. Spasi, urutan kunci, atau escape berubah sehingga tanda tangan tidak cocok. Pakai body mentah.
  • Middleware yang mengubah body (misalnya express.json() sebelum rute Anda). Pasang express.raw pada rute webhook.
  • Perbandingan biasa (==). Gunakan crypto.timingSafeEqual atau hash_equals.
  • Lupa memeriksa t. Tanpa batas 5 menit, permintaan lama bisa diputar ulang.
  • Hanya membaca satu v1. Saat rotasi, tanda tangan yang cocok bisa nilai kedua. Periksa semuanya.
  • Secret salah atau terpotong. Pakai seluruh string whsec_..., bukan hanya bagian sesudah awalan, dan hati-hati dengan spasi atau baris baru di variabel lingkungan.
  • Jam server melenceng. Sinkronkan dengan NTP; jika tidak, semua permintaan sah bisa ditolak.
  • Membalas lambat. Balas 2xx secepatnya (batas 8 detik) lalu kerjakan prosesnya di latar belakang.

Halaman terkait​