{"record":{"id":"d4250570ed494a1a","repo":"knadh/listmonk","slug":"invalid-signature-encoding-v-d42505","errorCode":null,"errorMessage":"invalid signature encoding: %v","messagePattern":"invalid signature encoding: (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/bounce/webhooks/lettermint.go","lineNumber":64,"sourceCode":"\tif len(l.hmacKey) == 0 {\n\t\treturn nil, fmt.Errorf(\"webhook key is not configured\")\n\t}\n\n\t// Parse the signature header: t={timestamp},v1={hex_signature}.\n\tts, sigHex, err := parseLettermintSignature(sig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Verify timestamp tolerance (300 seconds).\n\tif math.Abs(float64(time.Now().Unix()-ts)) > 300 {\n\t\treturn nil, fmt.Errorf(\"signature timestamp expired\")\n\t}\n\n\t// Decode the hex signature from the header.\n\tsigB, err := hex.DecodeString(strings.TrimSpace(sigHex))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid signature encoding: %v\", err)\n\t}\n\n\t// Compute HMAC-SHA256 of \"{timestamp}.{body}\" and compare.\n\tmac := hmac.New(sha256.New, l.hmacKey)\n\tmac.Write([]byte(fmt.Sprintf(\"%d.%s\", ts, body)))\n\n\tif !hmac.Equal(mac.Sum(nil), sigB) {\n\t\treturn nil, fmt.Errorf(\"invalid signature\")\n\t}\n\n\tvar n lettermintNotif\n\tif err := json.Unmarshal(body, &n); err != nil {\n\t\treturn nil, fmt.Errorf(\"error unmarshalling Lettermint notification: %v\", err)\n\t}\n\n\t// Map event to bounce type.\n\tvar typ string\n\tswitch n.Event {","sourceCodeStart":46,"sourceCodeEnd":82,"githubUrl":"https://github.com/knadh/listmonk/blob/670c01717d48647093335cc23a6be6f4b79c3b6b/internal/bounce/webhooks/lettermint.go#L46-L82","documentation":"Lettermint's signature header carries the HMAC as a hex string (v1=...). This error is thrown when that hex portion cannot be decoded — before HMAC comparison — indicating the v1 value is not valid hexadecimal.","triggerScenarios":"Calling ProcessBounce with a sig header whose v1 value is: base64 instead of hex, empty, contains non-hex characters, has odd length, or the header was assembled with the wrong format entirely (e.g. just the raw digest without the t=...,v1=... scheme — though that usually fails earlier as invalid format).","commonSituations":"A gateway/proxy truncating or rewriting the signature header; manually constructed test headers with a base64 digest; copying a signature with whitespace or quotes; switching from another provider's (Stripe/Forwardemail-style) signature format to Lettermint's; header size limits cutting long signatures.","solutions":["Verify the v1 value decodes as hex: only [0-9a-f] characters and even length (64 chars for HMAC-SHA256).","Keep the header in Lettermint's exact scheme: \"t={unix_ts},v1={hex_hmac_of_timestamp.body}\".","Trim whitespace/quotes when extracting the header; check proxies aren't modifying it.","For tests, generate the signature with hex.EncodeToString(hmac.New(sha256.New, key).Sum(nil)) rather than pasting values."],"exampleFix":"// before: base64 digest in v1\nsig := fmt.Sprintf(\"t=%d,v1=%s\", ts, base64.StdEncoding.EncodeToString(mac.Sum(nil)))\n\n// after: hex digest in v1\nsig := fmt.Sprintf(\"t=%d,v1=%s\", ts, hex.EncodeToString(mac.Sum(nil)))","handlingStrategy":"try-catch","validationCode":"func isHexSignatureV1(sig string) bool {\n    for _, part := range strings.Split(sig, \",\") {\n        if kv := strings.SplitN(strings.TrimSpace(part), \"=\", 2); len(kv) == 2 && kv[0] == \"v1\" {\n            _, err := hex.DecodeString(strings.TrimSpace(kv[1]))\n            return err == nil\n        }\n    }\n    return false\n}","typeGuard":null,"tryCatchPattern":"bounces, err := lm.ProcessBounce(sig, body)\nif err != nil {\n    if strings.Contains(err.Error(), \"invalid signature encoding\") {\n        log.Printf(\"lettermint v1 signature is not hex: header=%q\", sig)\n        http.Error(w, \"bad signature format\", http.StatusBadRequest)\n        return\n    }\n    http.Error(w, \"unauthorized\", http.StatusUnauthorized)\n}","preventionTips":["Generate test signatures with hex.EncodeToString, never base64.","Verify proxies/gateways don't truncate or rewrite the signature header.","Reject requests with missing/short signature headers before invoking the handler."],"tags":["webhook","hmac","hex","signature","lettermint"],"backgroundTag":"webhook-signature-verification-failed","analyzedSha":"670c01717d48647093335cc23a6be6f4b79c3b6b","analyzedAt":"2026-09-01T03:39:35.452Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}