Webhook’s signature verification
To ensure the authenticity and integrity of webhook requests, Cakewalk signs each payload with a cryptographic signature. When your application receives a webhook, it must verify this signature using the public key provided by Cakewalk. The signature is included in the X-SIGNATURE
header, and it is generated using the SHA hash of the raw request body, signed with Cakewalk’s private RSA key.
Your server retrieves the corresponding public key from the https://open-api.getcakewalk.io/api/Keys
endpoint, using your API credentials for authorization. By verifying the signature against the raw payload using this public key, your application can confirm that the request was not tampered with and was genuinely sent by Cakewalk.
📋 Prepare configuration options
1. Define a configuration class:
public class CakewalkApiOptions
{
public const string SectionName = "CakewalkApi";
public string ApiKey { get; set; } = string.Empty;
public string ApiSecret { get; set; } = string.Empty;
public string PublicKeyEndpoint { get; set; } = "<https://open-api.getcakewalk.io/api/Keys>";
}
2. Register configuration in Program.cs
or Startup.cs
:
Program.cs
or Startup.cs
:builder.Services.Configure<CakewalkApiOptions>(
builder.Configuration.GetSection(CakewalkApiOptions.SectionName));
✅ Environment variables like CakewalkApi__ApiKey will automatically be bound if you use double underscores (__) in the name.
🔐 Signature Verification with Remote Public Key
SignatureService
Implementation:
SignatureService
Implementation:public class SignatureService : ISignatureService
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly CakewalkApiOptions _options;
private RSA? _publicKey;
public SignatureService(
IHttpClientFactory httpClientFactory,
IOptions<CakewalkApiOptions> optionsAccessor)
{
_httpClientFactory = httpClientFactory;
_options = optionsAccessor.Value;
}
public async Task<bool> VerifyAsync(string payload, string signature)
{
if (_publicKey == null)
{
await LoadPublicKeyAsync();
}
if (_publicKey == null)
{
throw new InvalidOperationException("Public key could not be loaded.");
}
var data = Encoding.UTF8.GetBytes(payload);
var signatureBytes = Convert.FromBase64String(signature);
return _publicKey.VerifyData(data, signatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
private async Task LoadPublicKeyAsync()
{
var client = _httpClientFactory.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, _options.PublicKeyEndpoint);
request.Headers.Add("X-API-KEY", _options.ApiKey);
request.Headers.Add("X-API-SECRET", _options.ApiSecret);
using var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
var pemContent = await response.Content.ReadAsStringAsync();
_publicKey = RSA.Create();
_publicKey.ImportFromPem(pemContent.ToCharArray());
}
}
🧪 Interface and Registration
Interface:
public interface ISignatureService
{
Task<bool> VerifyAsync(string payload, string signature);
}
Dependency Injection in Program.cs
:
Program.cs
:builder.Services.AddHttpClient(); // Register HttpClientFactory
builder.Services.AddSingleton<ISignatureService, SignatureService>();
🛡️ Implement the webhook endpoint with the validation logic
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
[ApiController]
[Route("api/webhook")]
public class WebhookController : ControllerBase
{
private readonly ISignatureService _signatureService;
private readonly ILogger<WebhookController> _logger;
public WebhookController(ISignatureService signatureService, ILogger<WebhookController> logger)
{
_signatureService = signatureService;
_logger = logger;
}
[HttpPost]
public async Task<IActionResult> ReceiveWebhook()
{
// Validate signature
var (isValid, body) = await ValidateSignatureAsync(Request);
if (!isValid)
{
_logger.LogWarning("Invalid webhook signature.");
return Unauthorized("Invalid signature.");
}
_logger.LogInformation("Valid webhook received: {Payload}", body);
// Deserialize and handle payload as needed
// Example
try
{
var model = JsonSerializer.Deserialize<WebhookPayload>(body);
if (model == null)
{
return BadRequest("Invalid payload format.");
}
// Process the payload
// ...
return Ok();
}
catch (JsonException ex)
{
_logger.LogError(ex, "Failed to deserialize webhook payload.");
return BadRequest("Invalid JSON format.");
}
}
private async Task<(bool isValid, string body)> ValidateSignatureAsync(HttpRequest request)
{
if (!request.Headers.TryGetValue("X-SIGNATURE", out var signatureHeader))
{
throw new Exception("Missing X-SIGNATURE header.");
}
request.EnableBuffering(); // Allow reading body multiple times
using var reader = new StreamReader(request.Body, Encoding.UTF8, leaveOpen: true);
var rawBody = await reader.ReadToEndAsync();
request.Body.Position = 0;
var isValid = await _signatureService.VerifyAsync(rawBody, signatureHeader!);
return (isValid, rawBody);
}
}
✅ Sample appsettings.json
or environment variables
appsettings.json
or environment variables{
"CakewalkApi": {
"ApiKey": "your-api-key-here",
"ApiSecret": "your-api-secret-here",
"PublicKeyEndpoint": "<https://open-api.getcakewalk.io/api/Keys>"
}
}
Or via environment variables:
CakewalkApi__ApiKey=your-api-key
CakewalkApi__ApiSecret=your-api-secret
Last updated
Was this helpful?