{"record":{"id":"0ad9e68c5834ea64","repo":"knadh/listmonk","slug":"invalid-signature","errorCode":null,"errorMessage":"invalid signature","messagePattern":"invalid signature","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/bounce/webhooks/forwardemail.go","lineNumber":67,"sourceCode":"func (p *Forwardemail) ProcessBounce(sigHex string, body []byte) ([]models.Bounce, error) {\n\tif len(p.hmacKey) == 0 {\n\t\treturn nil, errors.New(\"webhook key is not configured\")\n\t}\n\n\t// Decode the hex-encoded signature from the webhook\n\tsig, err := hex.DecodeString(sigHex)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid signature encoding: %v\", err)\n\t}\n\n\t// Generate HMAC using the request body and secret key\n\tmac := hmac.New(sha256.New, p.hmacKey)\n\tmac.Write(body)\n\texpectedSignature := mac.Sum(nil)\n\n\t// Compare the generated signature with the provided signature\n\tif !hmac.Equal(expectedSignature, sig) {\n\t\treturn nil, errors.New(\"invalid signature\")\n\t}\n\n\t// Parse the JSON payload\n\tvar n forwardemailNotif\n\tif err := json.Unmarshal(body, &n); err != nil {\n\t\treturn nil, fmt.Errorf(\"error unmarshalling Forwardemail notification: %v\", err)\n\t}\n\n\t// Categorize the bounce type\n\ttyp := models.BounceTypeSoft\n\thardBounceCategories := []string{\"block\", \"recipient\", \"virus\", \"spam\"}\n\tfor _, category := range hardBounceCategories {\n\t\tif n.Bounce.Category == category {\n\t\t\ttyp = models.BounceTypeHard\n\t\t\tbreak\n\t\t}\n\t}\n","sourceCodeStart":49,"sourceCodeEnd":85,"githubUrl":"https://github.com/knadh/listmonk/blob/670c01717d48647093335cc23a6be6f4b79c3b6b/internal/bounce/webhooks/forwardemail.go#L49-L85","documentation":"ProcessBounce computes HMAC-SHA256 over the raw body with the configured hmacKey and compares it to the hex-decoded signature using hmac.Equal. This error means the provided signature does not match the computed one, so the payload is not from Forwardemail (or was tampered with) and is rejected before JSON parsing.","triggerScenarios":"The sigHex supplied does not equal the HMAC-SHA256 of body under the shared key: wrong signing key, body modified before verification (e.g. re-serialized/re-indented JSON), signature not hex-encoded, or signature computed over different bytes.","commonSituations":"Rotated webhook keys where Forwardemail still signs with the old key; a proxy/gateway rewriting the body (compression, charset conversion, re-encoding) between receipt and verification; passing the signature with 0x prefix or base64 instead of raw hex; tests replaying a body captured with trailing newline stripped.","solutions":["Verify the configured hmacKey exactly matches the webhook secret in your Forwardemail account settings.","Sign/verify over the exact raw request bytes — read the body once and hash those bytes without re-marshalling JSON.","Ensure the signature is plain lowercase hex of the 32-byte HMAC (no prefixes, correct encoding).","Confirm no middleware modifies the request body before ProcessBounce; log both computed and received digests when debugging.","Check for clock-independent replay issues: hmac.Equal is constant-time, so a mismatch is a content/key problem, not timing."],"exampleFix":"// before\nbody, _ = json.Marshal(reparsedPayload) // mutates bytes before verify\nbounces, err := p.ProcessBounce(sigHex, body)\n// after\nraw, _ := io.ReadAll(c.Request().Body) // verify exact raw bytes\nbounces, err := p.ProcessBounce(sigHex, raw)","handlingStrategy":"validation","validationCode":"if _, err := hex.DecodeString(sigHex); err != nil || len(sigHex) != 64 {\n\treturn echo.NewHTTPError(http.StatusBadRequest, \"malformed signature\")\n}","typeGuard":"func isHexSHA256(sigHex string) bool {\n\tb, err := hex.DecodeString(sigHex)\n\treturn err == nil && len(b) == sha256.Size\n}","tryCatchPattern":"bounces, err := p.ProcessBounce(sigHex, rawBody)\nif err != nil {\n\tif strings.Contains(err.Error(), \"invalid signature\") {\n\t\t// return 401; log computed vs received HMAC digest (not the key) for diagnosis\n\t\treturn echo.NewHTTPError(http.StatusUnauthorized)\n\t}\n\treturn err\n}","preventionTips":["Verify HMAC over the exact raw request bytes; never re-marshal the JSON first.","Keep the webhook key in sync with Forwardemail when rotating.","Read the signature header exactly as sent (plain hex, no prefixes).","Disable any middleware that rewrites or re-encodes the request body before verification.","Replay-test with a captured payload and its original signature in CI."],"tags":["forwardemail","webhook","hmac","signature"],"backgroundTag":"webhook-signature-mismatch","analyzedSha":"670c01717d48647093335cc23a6be6f4b79c3b6b","analyzedAt":"2026-09-01T03:39:35.452Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}