{"record":{"id":"340c83f7cbb4c309","repo":"calcom/cal.diy","slug":"signature-mismatch","errorCode":null,"errorMessage":"signature mismatch","messagePattern":"signature mismatch","errorType":"http","errorClass":"HttpCode","httpStatus":400,"severity":"error","filePath":"packages/app-store/btcpayserver/api/webhook.ts","lineNumber":24,"sourceCode":"import { handlePaymentSuccess } from \"@calcom/app-store/_utils/payments/handlePaymentSuccess\";\r\nimport { distributedTracing } from \"@calcom/lib/tracing/factory\";\r\nimport { IS_PRODUCTION } from \"@calcom/lib/constants\";\r\nimport { HttpError as HttpCode } from \"@calcom/lib/http-error\";\r\nimport { getServerErrorFromUnknown } from \"@calcom/lib/server/getServerErrorFromUnknown\";\r\nimport { PrismaBookingPaymentRepository as BookingPaymentRepository } from \"@calcom/features/bookings/repositories/PrismaBookingPaymentRepository\";\r\n\r\nimport appConfig from \"../config.json\";\r\nimport { btcpayCredentialKeysSchema } from \"../lib/btcpayCredentialKeysSchema\";\r\n\r\nexport const config = { api: { bodyParser: false } };\r\n\r\nfunction verifyBTCPaySignature(rawBody: Buffer, expectedSignature: string, webhookSecret: string): string {\r\n  const hmac = crypto.createHmac(\"sha256\", webhookSecret);\r\n  hmac.update(rawBody);\r\n  const computedSignature = hmac.digest(\"hex\");\r\n  const hexRegex = /^[0-9a-fA-F]+$/;\r\n  if (!hexRegex.test(computedSignature) || !hexRegex.test(expectedSignature)) {\r\n    throw new HttpCode({ statusCode: 400, message: \"signature mismatch\" });\r\n  }\r\n  return computedSignature;\r\n}\r\n\r\nconst btcpayWebhookSchema = z.object({\r\n  deliveryId: z.string(),\r\n  webhookId: z.string(),\r\n  originalDeliveryId: z.string().optional(),\r\n  isRedelivery: z.boolean(),\r\n  type: z.string(),\r\n  timestamp: z.number(),\r\n  storeId: z.string(),\r\n  invoiceId: z.string(),\r\n  metadata: z.object({}).optional(),\r\n  manuallyMarked: z.boolean().optional(),\r\n  overPaid: z.boolean(),\r\n});\r\nconst SUPPORTED_INVOICE_EVENTS = [\"InvoiceSettled\", \"InvoiceProcessing\"];\r","sourceCodeStart":6,"sourceCodeEnd":42,"githubUrl":"https://github.com/calcom/cal.diy/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/app-store/btcpayserver/api/webhook.ts#L6-L42","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\nconst hexRegex = /^[0-9a-fA-F]+$/;\nif (!hexRegex.test(computedSignature) || !hexRegex.test(expectedSignature)) {\n  throw new HttpCode({ statusCode: 400, message: \"signature mismatch\" });\n}\n\n// after\nconst HEX64 = /^[0-9a-fA-F]{64}$/;\nif (!HEX64.test(expectedSignature)) {\n  throw new HttpCode({ statusCode: 401, message: \"Malformed signature header\" });\n}\n// computedSignature is hex-bytes(32) so it is always valid; no need to re-test it.","handlingStrategy":"validation","validationCode":"const HEX64 = /^[0-9a-fA-F]{64}$/;\nconst expected = signature.split(\"=\")[1];\nif (!HEX64.test(expected)) {\n  return res.status(401).json({ message: \"Malformed signature\" });\n}","typeGuard":"function isHex64(s: unknown): s is string {\n  return typeof s === \"string\" && /^[0-9a-fA-F]{64}$/.test(s);\n}","tryCatchPattern":"try {\n  verifyBTCPaySignature(rawBody, expectedSignature, webhookSecret);\n} catch (e) {\n  if (e instanceof HttpCode && e.statusCode === 400 && e.message === \"signature mismatch\") {\n    return res.status(401).json({ message: \"Invalid signature format\" });\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["webhook","hmac","signature","security","btcpay"],"backgroundTag":null,"analyzedSha":"176037d0afbe572f870a3c702985e7cd83fe6c0c","analyzedAt":"2026-08-12T19:12:41.464Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}