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

  1. Inspect the inbound `btcpay-sig` header value verbatim in logs to confirm it has the form `sha256=<64 hex chars>`.
  2. Ensure the upstream BTCPay Server is configured with the same webhook secret and is using HMAC-SHA256 hex output.
  3. Reject/verify the header format at the boundary (length 64 + hex) before invoking `verifyBTCPaySignature`, returning a clearer 401.
  4. 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

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


AI-assisted analysis of calcom/cal.diy@176037d0af (2026-08-12). Data as JSON: /api/errors/340c83f7cbb4c309. Report an issue: GitHub.