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📦 Project Structure
cakewalk_webhook/
├── config.py
├── signature_service.py
├── models.py
├── main.py
├── requirements.txtconfig.py — 🔧 Configuration
config.py — 🔧 Configurationfrom pydantic import Field
from pydantic_settings import BaseSettings
class CakewalkSettings(BaseSettings):
api_key: str = Field(..., alias="CAKEWALK_API_KEY")
api_secret: str = Field(..., alias="CAKEWALK_API_SECRET")
public_key_endpoint: str = "<https://open-api.getcakewalk.io/api/Keys>"
class Config:
env_file = ".env"
settings = CakewalkSettings()signature_service.py — 🔐 Signature Verification Logic
signature_service.py — 🔐 Signature Verification Logicimport base64
import requests
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.primitives import serialization
from cryptography.exceptions import InvalidSignature
from config import settings
class SignatureService:
def __init__(self):
self._public_key = None
def _load_public_key(self):
response = requests.get(
settings.public_key_endpoint,
headers={
"X-API-KEY": settings.api_key,
"X-API-SECRET": settings.api_secret,
},
timeout=10,
)
response.raise_for_status()
pem_data = response.text.encode("utf-8")
self._public_key = serialization.load_pem_public_key(pem_data)
def verify(self, payload: bytes, signature_b64: str) -> bool:
if self._public_key is None:
self._load_public_key()
signature = base64.b64decode(signature_b64)
try:
self._public_key.verify(
signature,
payload,
padding.PKCS1v15(),
hashes.SHA256(),
)
return True
except InvalidSignature:
return Falsemodels.py — 📄 Webhook Payload (Example)
models.py — 📄 Webhook Payload (Example)from pydantic import BaseModel, Field
class WebhookPayload(BaseModel):
event_type: str = Field(..., alias="eventType")
data: dictmain.py — 🚀 FastAPI App with Validation
main.py — 🚀 FastAPI App with Validationimport json
from typing import List
from fastapi import FastAPI, Request, Header, HTTPException, status
from pydantic import parse_obj_as
from signature_service import SignatureService
from models import WebhookPayload
app = FastAPI()
signature_service = SignatureService()
@app.post("/webhook")
async def receive_webhook(request: Request, x_signature: str = Header(None)):
if not x_signature:
raise HTTPException(status_code=400, detail="Missing X-SIGNATURE header")
raw_body = await request.body()
if not signature_service.verify(payload=raw_body, signature_b64=x_signature):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid signature")
try:
data = json.loads(raw_body)
payload = parse_obj_as(List[WebhookPayload], data)
except Exception:
raise HTTPException(status_code=400, detail="Invalid payload")
# 🎯 Your webhook processing logic here
print("✅ Received verified payload:", [event.dict() for event in payload])
return {"status": "success"}requirements.txt
requirements.txtfastapi
uvicorn
pydantic
pydantic-settings
requests
cryptography✅ .env File or Environment Variables
.env File or Environment VariablesCAKEWALK_API_KEY=your-api-key
CAKEWALK_API_SECRET=your-api-secret▶️ Run Your Webhook Server
uvicorn main:app --reload🧱 Project Structure:
com.example.cakewalk
├── CakewalkWebhookApplication.java # 🚀 Spring Boot main class
├── config
│ └── CakewalkApiProperties.java # ⚙️ Loads API credentials and endpoint
├── controller
│ └── WebhookController.java # 🛡️ Handles incoming webhooks
├── model
│ └── WebhookPayload.java # 📦 Matches the expected webhook JSON structure
├── service
│ └── SignatureService.java # 🔐 Fetches & uses public key to verify signature
CakewalkApiProperties - 📋 Configuration Class
CakewalkApiProperties - 📋 Configuration Classpackage com.example.cakewalk.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "cakewalk.api")
public class CakewalkApiProperties {
private String apiKey;
private String apiSecret;
private String publicKeyEndpoint = "<https://open-api.getcakewalk.io/api/Keys>";
public String getApiKey() { return apiKey; }
public void setApiKey(String apiKey) { this.apiKey = apiKey; }
public String getApiSecret() { return apiSecret; }
public void setApiSecret(String apiSecret) { this.apiSecret = apiSecret; }
public String getPublicKeyEndpoint() { return publicKeyEndpoint; }
public void setPublicKeyEndpoint(String publicKeyEndpoint) { this.publicKeyEndpoint = publicKeyEndpoint; }
}
SignatureService - 🔐 Signature Verification with Remote Public Key
SignatureService - 🔐 Signature Verification with Remote Public Keypackage com.example.cakewalk.service;
import com.example.cakewalk.config.CakewalkApiProperties;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import jakarta.annotation.PostConstruct;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
@Service
public class SignatureService {
@Qualifier("cakewalkApiProperties")
@Autowired
private CakewalkApiProperties config;
private PublicKey publicKey;
@PostConstruct
public void init() {
loadPublicKey();
}
public boolean verify(String payload, String signatureBase64) {
try {
if (publicKey == null) {
loadPublicKey();
}
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(publicKey);
signature.update(payload.getBytes(StandardCharsets.UTF_8));
byte[] signatureBytes = Base64.decodeBase64(signatureBase64);
return signature.verify(signatureBytes);
} catch (Exception e) {
throw new RuntimeException("Signature verification failed", e);
}
}
private void loadPublicKey() {
try {
RestTemplate restTemplate = new RestTemplate();
var headers = new HttpHeaders();
headers.set("X-API-KEY", config.getApiKey());
headers.set("X-API-SECRET", config.getApiSecret());
var entity = new HttpEntity<>(headers);
var response = restTemplate.exchange(
URI.create(config.getPublicKeyEndpoint()),
HttpMethod.GET,
entity,
String.class
);
String pem = response.getBody()
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\\\s+", "");
byte[] keyBytes = Base64.decodeBase64(pem);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
this.publicKey = keyFactory.generatePublic(keySpec);
} catch (Exception e) {
throw new RuntimeException("Failed to load public key", e);
}
}
}
WebhookController - 🛡️ Implement the webhook endpoint with the validation logic
WebhookController - 🛡️ Implement the webhook endpoint with the validation logicpackage com.example.cakewalk.controller;
import com.example.cakewalk.model.WebhookPayload;
import com.example.cakewalk.service.SignatureService;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.*;
import java.nio.charset.StandardCharsets;
import java.util.List;
@RestController
@RequestMapping("/api/webhook")
public class WebhookController {
@Autowired
private SignatureService signatureService;
@Autowired
private ObjectMapper objectMapper;
@PostMapping
public ResponseEntity<String> receiveWebhook(HttpServletRequest request) {
try {
String signature = request.getHeader("X-SIGNATURE");
if (signature == null) {
return ResponseEntity.status(401).body("Missing signature.");
}
String rawBody = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
boolean isValid = signatureService.verify(rawBody, signature);
if (!isValid) {
return ResponseEntity.status(401).body("Invalid signature.");
}
// ✅ Deserialize the array
List<WebhookPayload> payloadList = objectMapper.readValue(
rawBody,
objectMapper.getTypeFactory().constructCollectionType(List.class, WebhookPayload.class)
);
for (WebhookPayload payload : payloadList) {
// Process each payload
}
return ResponseEntity.ok("Webhook received.");
} catch (Exception e) {
return ResponseEntity.status(400).body("Failed to process webhook.");
}
}
}
application.properties Example
application.properties Example <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
spring.application.name=test-webhook-validation-maven
cakewalk.api.api-key=11bd8aa5-df9c-4b49-9d4a-b8ad63879ad4
cakewalk.api.api-secret=VoImlKQZ1yJhSPj2rpgNy8Sts6Ba3oHGECYBxsxIyGSTKhtzDdApvB4Qt8DhfI0fjKsYF8Xrwcsaomz5V6btR3C6NCY6FmQ8soDHKVxTm0B3fn4dUgvYL5S0csH345qm
cakewalk.api.public-key-endpoint=https://open-api.getcakewalk.io/api/KeysYou can override via environment variables:
CAKEWALK_API_API_KEY=your-api-key
CAKEWALK_API_API_SECRET=your-api-secret
Dependencies (pom.xml)
pom.xml)Make sure you have these dependencies:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
Expected test payload
When you create the webhook in cakewalk, the webhook URL will be validated by sending automatically the following test event payload:
[
{
"eventType": "test",
"data": {
"message": "Hello, World!"
}
}
]🧱 Project Structure
bash
CopyEdit
cakewalk-webhook/
├── src/
│ ├── config/
│ │ └── cakewalkApiConfig.ts # 🔐 API credentials and endpoint
│ ├── controller/
│ │ └── webhookController.ts # 🛡️ Webhook handler
│ ├── model/
│ │ └── WebhookPayload.ts # 📦 Payload type definition
│ ├── service/
│ │ └── signatureService.ts # 🔐 RSA signature verification
│ ├── app.ts # ⚙️ Express setup
│ └── index.ts # 🚀 Entry point
├── .env # 🔑 API config
├── tsconfig.json
├── package.json
📁 src/config/cakewalkApiConfig.ts
src/config/cakewalkApiConfig.tsimport dotenv from 'dotenv';
dotenv.config();
export const cakewalkApiConfig = {
apiKey: process.env.CAKEWALK_API_KEY!,
apiSecret: process.env.CAKEWALK_API_SECRET!,
publicKeyEndpoint: process.env.CAKEWALK_PUBLIC_KEY_ENDPOINT || '<https://open-api.getcakewalk.io/api/Keys>',
};
📁 src/model/WebhookPayload.ts
src/model/WebhookPayload.tsexport interface WebhookPayload {
eventType: string;
data: {
message: string;
};
}
📁 src/service/signatureService.ts
src/service/signatureService.tsimport axios from 'axios';
import crypto from 'crypto';
import { cakewalkApiConfig } from '../config/cakewalkApiConfig';
let cachedPublicKey: string | null = null;
async function fetchPublicKey(): Promise<string> {
if (cachedPublicKey) return cachedPublicKey;
const response = await axios.get<string>(cakewalkApiConfig.publicKeyEndpoint, {
headers: {
'X-API-KEY': cakewalkApiConfig.apiKey,
'X-API-SECRET': cakewalkApiConfig.apiSecret,
},
});
const cleaned = response.data
.replace(/-----BEGIN PUBLIC KEY-----/, '')
.replace(/-----END PUBLIC KEY-----/, '')
.replace(/\\s+/g, '');
cachedPublicKey = cleaned;
return cleaned;
}
export async function verifySignature(payload: string, signatureBase64: string): Promise<boolean> {
try {
const pemKey = await fetchPublicKey();
const publicKey = crypto.createPublicKey({
key: Buffer.from(pemKey, 'base64'),
format: 'der',
type: 'spki',
});
const verifier = crypto.createVerify('RSA-SHA256');
verifier.update(payload);
verifier.end();
const signature = Buffer.from(signatureBase64, 'base64');
return verifier.verify(publicKey, signature);
} catch (err) {
console.error('Signature verification failed:', err);
return false;
}
}
📁 src/controller/webhookController.ts
src/controller/webhookController.tsimport { Request, Response } from 'express';
import { verifySignature } from '../service/signatureService';
import { WebhookPayload } from '../model/WebhookPayload';
export async function webhookHandler(req: Request, res: Response) {
const signature = req.headers['x-signature'] as string;
if (!signature) {
return res.status(401).send('Missing signature.');
}
const rawBody = (req as any).rawBody?.toString('utf8');
if (!rawBody) {
return res.status(400).send('Missing raw body for signature verification.');
}
const isValid = await verifySignature(rawBody, signature);
if (!isValid) {
return res.status(401).send('Invalid signature.');
}
try {
const payload: WebhookPayload = req.body;
console.log('✅ Webhook received:', payload);
return res.send('Webhook received.');
} catch (e) {
return res.status(400).send('Invalid JSON.');
}
}
⚙️ src/app.ts
src/app.tsimport express from 'express';
import bodyParser from 'body-parser';
import { webhookHandler } from './controller/webhookController';
const app = express();
// Capture raw body for signature verification
app.use(bodyParser.json({
verify: (req: any, res, buf) => {
req.rawBody = buf;
}
}));
app.post('/api/webhook', webhookHandler);
export default app;🔑 .env
.envCAKEWALK_API_KEY=your-api-key
CAKEWALK_API_API_SECRET=your-api-secret
CAKEWALK_PUBLIC_KEY_ENDPOINT=https://open-api.getcakewalk.io/api/KeysLast updated
Was this helpful?