calcom/cal.diy · error · HttpCode
signature mismatch
Error message
signature mismatch
What it means
Thrown inside `verifyBTCPaySignature` as a defensive guard before `crypto.timingSafeEqual`. After computing the HMAC-SHA256 hex digest of the raw body, it asserts both the `computedSignature` and the caller-supplied `expectedSignature` are non-empty hex strings; if either fails the `/^[0-9a-fA-F]+$/` test it raises HttpCode 400. Because the HMAC digest is always hex by construction, in practice this branch fires only when the inbound `expectedSignature` is malformed.
Source
Thrown at packages/app-store/btcpayserver/api/webhook.ts:24
import { handlePaymentSuccess } from "@calcom/app-store/_utils/payments/handlePaymentSuccess";
import { distributedTracing } from "@calcom/lib/tracing/factory";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import { HttpError as HttpCode } from "@calcom/lib/http-error";
import { getServerErrorFromUnknown } from "@calcom/lib/server/getServerErrorFromUnknown";
import { PrismaBookingPaymentRepository as BookingPaymentRepository } from "@calcom/features/bookings/repositories/PrismaBookingPaymentRepository";
import appConfig from "../config.json";
import { btcpayCredentialKeysSchema } from "../lib/btcpayCredentialKeysSchema";
export const config = { api: { bodyParser: false } };
function verifyBTCPaySignature(rawBody: Buffer, expectedSignature: string, webhookSecret: string): string {
const hmac = crypto.createHmac("sha256", webhookSecret);
hmac.update(rawBody);
const computedSignature = hmac.digest("hex");
const hexRegex = /^[0-9a-fA-F]+$/;
if (!hexRegex.test(computedSignature) || !hexRegex.test(expectedSignature)) {
throw new HttpCode({ statusCode: 400, message: "signature mismatch" });
}
return computedSignature;
}
const btcpayWebhookSchema = z.object({
deliveryId: z.string(),
webhookId: z.string(),
originalDeliveryId: z.string().optional(),
isRedelivery: z.boolean(),
type: z.string(),
timestamp: z.number(),
storeId: z.string(),
invoiceId: z.string(),
metadata: z.object({}).optional(),
manuallyMarked: z.boolean().optional(),
overPaid: z.boolean(),
});
const SUPPORTED_INVOICE_EVENTS = ["InvoiceSettled", "InvoiceProcessing"];
View on GitHub (pinned to 176037d0af)
Solutions
- Inspect the inbound `btcpay-sig` header value verbatim in logs to confirm it has the form `sha256=<64 hex chars>`.
- Ensure the upstream BTCPay Server is configured with the same webhook secret and is using HMAC-SHA256 hex output.
- Reject/verify the header format at the boundary (length 64 + hex) before invoking `verifyBTCPaySignature`, returning a clearer 401.
- If a different signature scheme is intended, update `verifyBTCPaySignature` to handle it explicitly instead of relying on the regex guard.
Example fix
// before
const hexRegex = /^[0-9a-fA-F]+$/;
if (!hexRegex.test(computedSignature) || !hexRegex.test(expectedSignature)) {
throw new HttpCode({ statusCode: 400, message: "signature mismatch" });
}
// after
const HEX64 = /^[0-9a-fA-F]{64}$/;
if (!HEX64.test(expectedSignature)) {
throw new HttpCode({ statusCode: 401, message: "Malformed signature header" });
}
// computedSignature is hex-bytes(32) so it is always valid; no need to re-test it. Defensive patterns
Strategy: validation
Validate before calling
const HEX64 = /^[0-9a-fA-F]{64}$/;
const expected = signature.split("=")[1];
if (!HEX64.test(expected)) {
return res.status(401).json({ message: "Malformed signature" });
} Type guard
function isHex64(s: unknown): s is string {
return typeof s === "string" && /^[0-9a-fA-F]{64}$/.test(s);
} Try / catch
try {
verifyBTCPaySignature(rawBody, expectedSignature, webhookSecret);
} catch (e) {
if (e instanceof HttpCode && e.statusCode === 400 && e.message === "signature mismatch") {
return res.status(401).json({ message: "Invalid signature format" });
}
throw e;
} Prevention
- Validate the signature is 64-char hex before calling verify.
- Pin the BTCPay Server version and signature scheme in deployment docs.
- Reject unexpected header encodings at the ingress/proxy layer.
- Unit-test verifyBTCPaySignature against forged/non-hex inputs.
When it happens
Trigger: Webhook request where the `btcpay-sig` header (after stripping the `sha256=` prefix) contains non-hex characters, is empty, or is in a different encoding (e.g. base64 from a misconfigured BTCPay Server, or a signature produced with a different hash algorithm).
Common situations: BTCPay Server version change altering signature encoding; a proxy/load-balancer rewriting headers; an attacker or misconfigured client sending a forged payload; the `sha256=` prefix splitting logic in the caller yielding an empty string.
Related errors
- Missing or invalid signature format
- Cal.diy: Store ID mismatch
- Invoice not found
- Cal.diy: payment not found
- Cal.diy: credentials not found
AI-assisted analysis of calcom/cal.diy@176037d0af (2026-08-12).
Data as JSON: /api/errors/340c83f7cbb4c309.
Report an issue: GitHub.