Verifying Signatures
To ensure message integrity and authenticity, the system signs each webhook payload using its private Ed25519 key. You must verify this using the public_key you received during registration.
Verification Steps
- Retrieve the timestamp from the
X-DLT-Timestampheader. - Retrieve the signature from the
X-DLT-Signatureheader (Base64URL encoded). - Read the raw request body (exact bytes, do not parse as JSON yet).
- Construct the signed message by concatenating the timestamp, a dot (
.), and the raw body bytes:message = timestamp + "." + raw_body - Use an Ed25519 library to verify the signature against this message using the provided public key.
Code Examples
- Python
- Java
- TypeScript (Node.js)
- Go
Requires the pynacl library.
import nacl.signing
import nacl.exceptions
import base64
def verify_signature(public_key_b64u, signature_b64u, timestamp, raw_body):
"""
Verifies the Ed25519 signature.
:param public_key_b64u: Base64URL encoded 32-byte public key
:param signature_b64u: Base64URL encoded 64-byte signature
:param timestamp: Value of X-DLT-Timestamp header
:param raw_body: Raw HTTP request body as bytes
"""
try:
# 1. Decode Base64URL strings
# Adding padding if necessary (handled automatically by urlsafe_b64decode in modern Python)
decoded_public_key = base64.urlsafe_b64decode(public_key_b64u + '==')
decoded_signature = base64.urlsafe_b64decode(signature_b64u + '==')
# 2. Construct the signed message
msg = timestamp.encode() + b"." + raw_body
# 3. Verify
verify_key = nacl.signing.VerifyKey(decoded_public_key)
verify_key.verify(msg, decoded_signature)
return True
except (nacl.exceptions.BadSignatureError, Exception):
return False
Requires the BouncyCastle library.
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
import org.bouncycastle.crypto.signers.Ed25519Signer;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class WebhookVerifier {
public static boolean verify(String publicKeyB64Url, String signatureB64Url, String timestamp, byte[] rawBody) {
try {
byte[] publicKeyBytes = Base64.getUrlDecoder().decode(publicKeyB64Url);
byte[] signatureBytes = Base64.getUrlDecoder().decode(signatureB64Url);
// Construct message: timestamp + "." + rawBody
byte[] msgPrefix = (timestamp + ".").getBytes(StandardCharsets.UTF_8);
byte[] msg = new byte[msgPrefix.length + rawBody.length];
System.arraycopy(msgPrefix, 0, msg, 0, msgPrefix.length);
System.arraycopy(rawBody, 0, msg, msgPrefix.length, rawBody.length);
Ed25519PublicKeyParameters publicKey = new Ed25519PublicKeyParameters(publicKeyBytes, 0);
Ed25519Signer verifier = new Ed25519Signer();
verifier.init(false, publicKey);
verifier.update(msg, 0, msg.length);
return verifier.verifySignature(signatureBytes);
} catch (Exception e) {
return false;
}
}
}
Uses the built-in crypto module.
import * as crypto from 'crypto';
/**
* @param publicKeyB64u Base64URL encoded public key
* @param signatureB64u Base64URL encoded signature from X-DLT-Signature
* @param timestamp Value from X-DLT-Timestamp
* @param rawBody Raw request body as a Buffer
*/
export function verifySignature(
publicKeyB64u: string,
signatureB64u: string,
timestamp: string,
rawBody: Buffer
): boolean {
try {
const publicKey = Buffer.from(publicKeyB64u, 'base64url');
const signature = Buffer.from(signatureB64u, 'base64url');
const msg = Buffer.concat([
Buffer.from(timestamp),
Buffer.from('.'),
rawBody
]);
return crypto.verify(
null,
msg,
{
key: publicKey,
format: 'raw',
type: 'public',
algorithm: 'ed25519'
} as any, // 'ed25519' algorithm is supported in modern Node.js
signature
);
} catch (err) {
return false;
}
}
Uses the standard library.
import (
"crypto/ed25519"
"encoding/base64"
)
func VerifySignature(publicKeyB64u, signatureB64u, timestamp string, rawBody []byte) bool {
pubKey, err := base64.RawURLEncoding.DecodeString(publicKeyB64u)
if err != nil {
// Try with padding if RawURLEncoding fails
pubKey, err = base64.URLEncoding.DecodeString(publicKeyB64u)
if err != nil { return false }
}
sig, err := base64.RawURLEncoding.DecodeString(signatureB64u)
if err != nil {
sig, err = base64.URLEncoding.DecodeString(signatureB64u)
if err != nil { return false }
}
// msg = timestamp + "." + rawBody
msg := append([]byte(timestamp), '.')
msg = append(msg, rawBody...)
return ed25519.Verify(pubKey, msg, sig)
}