Discord Slash Command handlers must respond to Discord's pings with validated pongs, which means we need to validate the signature against the request. Discord gives us two examples, one in JavaScript:
const nacl = require('tweetnacl');// Your public key can be found on your application in the Developer Portalconst PUBLIC_KEY = 'APPLICATION_PUBLIC_KEY';const signature = req.get('X-Signature-Ed25519');const timestamp = req.get('X-Signature-Timestamp');const body = req.rawBody; // rawBody is expected to be a string, not raw bytesconst isVerified = nacl.sign.detached.verify(Buffer.from(timestamp + body),Buffer.from(signature, 'hex'),Buffer.from(PUBLIC_KEY, 'hex'));if (!isVerified) {return res.status(401).end('invalid request signature');}
and one example in python:
from nacl.signing import VerifyKeyfrom nacl.exceptions import BadSignatureError# Your public key can be found on your application in the Developer PortalPUBLIC_KEY = 'APPLICATION_PUBLIC_KEY'verify_key = VerifyKey(bytes.fromhex(PUBLIC_KEY))signature = request.headers["X-Signature-Ed25519"]timestamp = request.headers["X-Signature-Timestamp"]body = request.datatry:verify_key.verify(f'{timestamp}{body}'.encode(), bytes.fromhex(signature))except BadSignatureError:abort(401, 'invalid request signature')
These examples show that we need to find an ed25519 library to use, and pass in two headers from the discord request (X-Signature-Ed25519
and X-Signature-Timestamp
) as well as the public key and some hex conversion.
You can find your application's public key at https://discord.com/developers/applications/<application_id>/information
. We'll use ed25519_dalek and hex to decode the environment variable.
We'll write a function called validate_discord_signature
that takes http::HeaderMap
, a aws_lambda_events::encodings::Body
(I'm working in a lambda), and the PublicKey
.
use aws_lambda_events::encodings::Body;use ed25519_dalek::{PublicKey, Signature, Verifier};use http::HeaderMap;lazy_static! {static ref PUB_KEY: PublicKey = PublicKey::from_bytes(&hex::decode(env::var("DISCORD_PUBLIC_KEY").expect("Expected DISCORD_PUBLIC_KEY to be set in the environment")).expect("Couldn't hex::decode the DISCORD_PUBLIC_KEY")).expect("Couldn't create a PublicKey from DISCORD_PUBLIC_KEY bytes");}pub fn validate_discord_signature(headers: &HeaderMap,body: &Body,pub_key: &PublicKey,) -> anyhow::Result<()> {let sig_ed25519 = {let header_signature = headers.get("X-Signature-Ed25519").ok_or(anyhow!("missing X-Signature-Ed25519 header"))?;let decoded_header = hex::decode(header_signature)?;let mut sig_arr: [u8; 64] = [0; 64];for (i, byte) indecoded_header.into_iter().enumerate(){sig_arr[i] = byte;}Signature::new(sig_arr)};let sig_timestamp =headers.get("X-Signature-Timestamp").ok_or(anyhow!("missing X-Signature-Timestamp header"),)?;if let Body::Text(body) = body {let content = sig_timestamp.as_bytes().iter().chain(body.as_bytes().iter()).cloned().collect::<Vec<u8>>();pub_key.verify(&content.as_slice(), &sig_ed25519).map_err(anyhow::Error::msg)} else {Err(anyhow!("Invalid body type"))}}