70 %
Chris Biscardi

Validating Discord Slash Command ED25519 Signatures in Rust

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:

tweetnacl.js
javascript
const nacl = require('tweetnacl');
// Your public key can be found on your application in the Developer Portal
const 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 bytes
const 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:

nacl.py
python
from nacl.signing import VerifyKey
from nacl.exceptions import BadSignatureError
# Your public key can be found on your application in the Developer Portal
PUBLIC_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.data
try:
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.

The Rust Implementation

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.

validate_discord_signature.rs
rust
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) in
decoded_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"))
}
}