Signature verification (Ed25519)
本頁內容尚未翻譯。
Status
Stable· Last verified 2026-07-12 · APIv10· Sources Interactions overview — Security & Authorization, discord-interactions-js
If you receive interactions over an HTTP endpoint, signature verification is
mandatory — not a best practice you can defer. This page is the precise flow;
the interaction-endpoint
example implements it with zero dependencies and a passing local test.
Receiving interactions over the gateway instead? You don’t do this — the gateway connection is already authenticated by your bot token.
Why it is non-negotiable
Section titled “Why it is non-negotiable”[!CAUTION] Verified — Discord enforces this Discord runs “automated, routine security checks” that include purposefully sending you invalid signatures. If your endpoint accepts one, Discord removes your interactions URL and alerts you via email and System DM. Source: Interactions overview.
The flow
Section titled “The flow”Discord → your endpoint (POST) with headers: X-Signature-Ed25519 : <hex signature> X-Signature-Timestamp : <timestamp>and a raw JSON body.
Verify: message = X-Signature-Timestamp + <raw request body> valid = Ed25519_verify(publicKey, signature, message)
if not valid → respond HTTP 401 and stop. if valid → parse JSON; if type == 1 (PING) respond {"type": 1} (PONG).Verified specifics from the docs:
- Headers.
X-Signature-Ed25519(the signature, hex) andX-Signature-Timestamp(the timestamp, unix seconds). - What is signed. The timestamp concatenated with the request body, verified with Ed25519 against your app’s Public Key (Developer Portal → General Information).
- Return 401 when verification fails.
- Answer the PING (
type: 1) with aPONG(type: 1).
Atlas hardening (recommended; implemented in the example):
- Timestamp skew / replay window. Even a correctly signed request should be
rejected if
|now − timestamp|is too large (the example uses 5 minutes). Signature alone does not stop an attacker from replaying a captured request hours later. - Body size cap. Refuse oversized POSTs early (the example uses 256 KiB) so a bad client cannot force unbounded memory use before verification.
The one pitfall that causes most failures: the raw body
Section titled “The one pitfall that causes most failures: the raw body”[!CAUTION] Verify the exact bytes Discord sent You must verify against the raw, unparsed body string — the bytes exactly as received. If a framework parses the JSON first and you re-serialize it (changing whitespace or key order), the signature will not match and every valid request fails. The official docs verify against the raw body string (
request.data.decode("utf-8")in Python,req.rawBodyin JS).
Practical consequences:
- In Express, capture the raw body (e.g.
express.json({ verify: (req, _res, buf) => { req.rawBody = buf } })) or use the officialverifyKeyMiddleware. Don’t verifyJSON.stringify(req.body). - On serverless, read the raw request text before any automatic JSON parsing.
Reference implementation (zero-dependency, Node ≥ 20)
Section titled “Reference implementation (zero-dependency, Node ≥ 20)”Node’s built-in WebCrypto verifies Ed25519 with a raw public key — no tweetnacl
needed. This is the core of the atlas example:
import { webcrypto } from "node:crypto";const { subtle } = webcrypto;
const MAX_SKEW_SEC = 5 * 60; // reject stale timestamps (replay window)
const hexToBytes = (hex) => { if (typeof hex !== "string" || hex.length % 2 || !/^[0-9a-fA-F]*$/.test(hex)) { throw new Error("invalid hex"); } return Uint8Array.from(hex.match(/.{1,2}/g).map((b) => parseInt(b, 16)));};
export async function verify(rawBody, signature, timestamp, publicKeyHex) { if (!signature || !timestamp) return false; const ts = Number(timestamp); if (!Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > MAX_SKEW_SEC) { return false; // missing, non-numeric, or outside replay window } try { const key = await subtle.importKey( "raw", hexToBytes(publicKeyHex), { name: "Ed25519" }, false, ["verify"], ); const message = new TextEncoder().encode(timestamp + rawBody); return await subtle.verify( { name: "Ed25519" }, key, hexToBytes(signature), message, ); } catch { return false; // bad hex / bad key material — never fail open }}Alternatively use Discord’s official discord-interactions
(verifyKey / verifyKeyMiddleware), which many hosting guides reference.
Test it without Discord
Section titled “Test it without Discord”The example generates its own Ed25519 keypair, signs a PING, and asserts the
endpoint returns 200/PONG — then tampers the body and asserts 401:
cd examples/interaction-endpoint && node --test# ✓ valid PING → 200 PONG# ✓ tampered body → 401# ✓ missing signature headers → 401# ✓ valid /ping command → 200 message reply# ✓ stale timestamp (outside replay window) → 401# (+ unit tests for hex rejection and maxSkewSec)Checklist
Section titled “Checklist”- Verify before parsing/altering the body.
- Compare against
timestamp + rawBody, Ed25519, your Public Key. - Return 401 on failure; never “fail open.”
- Reject stale timestamps (replay window) and non-hex signatures.
- Cap request body size before buffering unbounded input.
- Answer
PING(1) withPONG(1). - Keep the Public Key configured per environment (dev/prod apps differ).
- Interaction lifecycle — where verification sits in the flow.
- Security pitfalls — beyond signatures.