Verifying signatures
Every delivery carries a NEO-Signature header. Verify it before you trust the body.
Header format
NEO-Signature: t=1793906533,v1=5f1d3c9a0e7b4a2d8c6f1e0b9a8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d
t is the Unix timestamp at which the signature was computed and v1 is the hex HMAC-SHA256 of "{t}.{raw body}" using your endpoint secret. Reject requests whose timestamp is more than five minutes old to prevent replays, and compare signatures in constant time.
Node
import crypto from "node:crypto";
export function verifyNeoSignature(body, header, secret) {
const parts = Object.fromEntries(
header.split(",").map((p) => p.split("="))
);
const signed = `${parts.t}.${body}`;
const expected = crypto
.createHmac("sha256", secret)
.update(signed)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
return (
fresh &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
);
}
Python
import hmac, hashlib, time
def verify_neo_signature(body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
signed = f"{parts['t']}.".encode() + body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
fresh = abs(time.time() - int(parts["t"])) < 300
return fresh and hmac.compare_digest(expected, parts["v1"])
PHP
function verifyNeoSignature(string $body, string $header, string $secret): bool
{
$parts = [];
foreach (explode(',', $header) as $p) { [$k, $v] = explode('=', $p, 2); $parts[$k] = $v; }
$expected = hash_hmac('sha256', $parts['t'].'.'.$body, $secret);
$fresh = abs(time() - (int) $parts['t']) < 300;
return $fresh && hash_equals($expected, $parts['v1']);
}
Use the raw request body exactly as received. Re-serialising the JSON will change whitespace and break the signature.