# Webhook Signature Verification This page explains how to verify the authenticity of webhook events sent by the Xenia External API. All webhook events include cryptographic signatures that allow you to verify that the events were sent by Xenia and haven't been tampered with. ## Overview When Xenia sends a webhook event to your endpoint, it includes two important headers: - `X-Signature`: The RSA-SHA256 signature of the webhook payload - `X-Timestamp`: The timestamp when the webhook was sent (used in signature calculation) To verify the webhook authenticity: 1. Retrieve the public key from the `/external-api/v1/webhook-verification-key` endpoint 2. Concatenate the raw request body with the timestamp 3. Verify the signature using RSA-SHA256 ## Getting the Public Key The public key used for verification can be retreived from the following endpoint: ```bash curl -X GET https://your-xenia-api.com/external-api/v1/webhook-verification-key \ -H "X-Api-Key: YOUR_API_KEY" ``` Example Response: ```json { "data": { "publicKey": "MIIBIjANBgk...", "algorithm": "RSA-SHA256 + PKCS#1 padding", "keyFormat": "base64" } } ``` ## Verification Examples ```js index.js import crypto from 'crypto'; import axios from 'axios'; // Cache for the public key let keyCache = { key: null, expiresAt: null }; /** * Verify webhook signature with automatic key fetching and caching * @param {string} requestBody - Raw webhook request body * @param {string} xSignature - X-Signature header value * @param {string} xTimestamp - X-Timestamp header value * @param {string} apiUrl - Your Xenia API base URL * @param {string} apiKey - API key for X-Api-Key header * @returns {Promise} True if signature is valid */ async function verifyWebhookSignature(requestBody, xSignature, xTimestamp, apiUrl, apiKey) { try { // Check cache and fetch key if needed let publicKey = keyCache.key; if (!publicKey || !keyCache.expiresAt || new Date() >= keyCache.expiresAt) { const response = await axios.get(`${apiUrl}/external-api/v1/webhook-verification-key`, { headers: { 'X-Api-Key': apiKey } }); publicKey = response.data.data.publicKey; keyCache = { key: publicKey, expiresAt: new Date(Date.now() + 60 * 60 * 1000) // 1 hour expiry }; } // Convert base64 public key to PEM format const pemKey = `-----BEGIN PUBLIC KEY-----\n${publicKey.match(/.{1,64}/g).join('\n')}\n-----END PUBLIC KEY-----`; // Verify signature const verifier = crypto.createVerify('RSA-SHA256'); verifier.update(requestBody + (xTimestamp || '')); return verifier.verify(pemKey, Buffer.from(xSignature, 'base64')); } catch (error) { console.error('Signature verification failed:', error.message); return false; } } // Usage in Express webhook endpoint app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => { const isValid = await verifyWebhookSignature( req.body.toString('utf-8'), req.headers['x-signature'], req.headers['x-timestamp'], process.env.XENIA_API_URL, process.env.XENIA_API_KEY ); if (!isValid) { return res.status(401).json({ error: 'Invalid signature' }); } // Process verified webhook const webhookData = JSON.parse(req.body); // ... handle webhook ... res.status(204).send(); }); ``` ```csharp Main.cs using System; using System.Net.Http; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; public class WebhookVerifier { private readonly HttpClient _httpClient; private readonly IMemoryCache _cache; public WebhookVerifier(HttpClient httpClient, IMemoryCache cache) { _httpClient = httpClient; _cache = cache; } /// /// Verify webhook signature with automatic key fetching and caching /// /// Raw webhook request body /// X-Signature header value /// X-Timestamp header value /// Your Xenia API base URL /// Your API key /// True if signature is valid public async Task VerifyWebhookSignature( string requestBody, string xSignature, string xTimestamp, string apiUrl, string apiKey) { try { // Check cache and fetch key if needed const string cacheKey = "xenia_webhook_key"; if (!_cache.TryGetValue(cacheKey, out var publicKey)) { var request = new HttpRequestMessage(HttpMethod.Get, $"{apiUrl}/external-api/v1/webhook-verification-key"); request.Headers.Add("X-Api-Key", apiKey); var response = await _httpClient.SendAsync(request); response.EnsureSuccessStatusCode(); var json = await response.Content.ReadAsStringAsync(); var keyData = JsonDocument.Parse(json); publicKey = keyData.RootElement .GetProperty("data") .GetProperty("publicKey") .GetString(); // Cache for 1 hour _cache.Set(cacheKey, publicKey, TimeSpan.FromHours(1)); } using var rsa = RSA.Create(); // Decode base64 public key (DER format) var publicKeyBytes = Convert.FromBase64String(publicKey); rsa.ImportSubjectPublicKeyInfo(publicKeyBytes, out _); var dataToVerify = Encoding.UTF8.GetBytes(requestBody + (xTimestamp ?? "")); var signature = Convert.FromBase64String(xSignature); return rsa.VerifyData(dataToVerify, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); } catch (Exception ex) { Console.WriteLine($"Signature verification failed: {ex.Message}"); return false; } } } // Usage in ASP.NET Core controller [ApiController] public class WebhookController : ControllerBase { private readonly WebhookVerifier _verifier; public WebhookController(WebhookVerifier verifier) { _verifier = verifier; } [HttpPost("webhook")] public async Task ReceiveWebhook() { using var reader = new StreamReader(Request.Body); var requestBody = await reader.ReadToEndAsync(); var isValid = await _verifier.VerifyWebhookSignature( requestBody, Request.Headers["X-Signature"], Request.Headers["X-Timestamp"], Configuration["Xenia:ApiUrl"], Configuration["Xenia:ApiKey"] ); if (!isValid) { return Unauthorized("Invalid signature"); } // Process verified webhook var webhookData = JsonSerializer.Deserialize(requestBody); // ... handle webhook ... return NoContent(); } } // Register in Startup.cs public void ConfigureServices(IServiceCollection services) { services.AddHttpClient(); services.AddMemoryCache(); services.AddScoped(); } ``` ## Security Best Practices 1. **Always verify signatures**: Never process webhook events without verifying their signatures first. 2. **Refresh keys periodically**: Fetch the public key from the verification endpoint periodically (e.g., daily) to ensure you have the latest key in case of rotation. 3. **Handle errors gracefully**: If signature verification fails, log the event for investigation but don't process the webhook data. 4. **Validate timestamps**: Consider checking that the timestamp is recent (e.g., within the last 5 minutes) to prevent replay attacks. ## Troubleshooting ### Common Issues 1. **Signature verification always fails** - Ensure you're using the raw request body, not a parsed/modified version - Verify the public key format is correct (PEM format with headers) - Check that you're concatenating the payload with timestamp correctly 2. **Missing headers** - Both `X-Signature` and `X-Timestamp` headers must be present - Header names are case-insensitive in HTTP but may be case-sensitive in your code ## Support If you continue to experience issues with webhook signature verification, please contact Xenia support with: - The webhook event ID - The timestamp of the webhook - Any error messages from your verification code - A sample of your verification implementation (with sensitive data removed)