Plugin Security
Signature verification and security best practices.
Signature Model
All communication is signed using HMAC-SHA256. The signature ensures:
- Authenticity — Request came from the expected sender
- Integrity — Payload wasn't modified
- Freshness — Not a replay attack
Verification — Node.js
Verify inbound requests using HMAC-SHA256:
Signature Verification — Node.js
TypeScript
const crypto = require('crypto');
function verifySignature(req, secret) {
const signature = req.headers['x-plugin-signature'];
const timestamp = req.headers['x-plugin-timestamp'];
// Check timestamp freshness (5 min window)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) {
throw new Error('Request timestamp too old');
}
const rawBody = req.rawBody || JSON.stringify(req.body);
const expected = crypto
.createHmac('sha256', secret)
.update(timestamp + '.' + rawBody)
.digest('hex');
if (!crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from('sha256=' + expected)
)) {
throw new Error('Invalid signature');
}
return true;
}Verification — PHP
PHP/Laravel verification:
Signature Verification — PHP
PHP
<?php
function verifySignature(Request $request, string $secret): bool
{
$signature = $request->header('X-Plugin-Signature');
$timestamp = $request->header('X-Plugin-Timestamp');
if (abs(time() - (int) $timestamp) > 300) {
abort(401, 'Timestamp too old');
}
$rawBody = $request->getContent();
$expected = 'sha256=' . hash_hmac(
'sha256', $timestamp . '.' . $rawBody, $secret
);
if (!hash_equals($expected, $signature)) {
abort(401, 'Invalid signature');
}
return true;
}Why Raw Body Signing?
ThreeU signs the raw request body rather than json_encode(payload):
- Deterministic — Exact bytes transmitted are signed
- Language-agnostic — No JSON encoding differences
- Tamper-proof — Any modification invalidates the signature
- Standard — Matches Stripe, GitHub, etc.