knadh/listmonk · warning
invalid timestamp in signature: %v
Error message
invalid timestamp in signature: %v
What it means
parseLettermintSignature parses the Lettermint signature header of the form 't={timestamp},v1={hex}'. When the 't=' component exists but its value cannot be parsed as an integer via fmt.Sscanf, the handler rejects the webhook with 'invalid timestamp in signature'. This guards the HMAC replay-window check that follows, which requires a numeric Unix timestamp.
Source
Thrown at internal/bounce/webhooks/lettermint.go:134
}}, nil
}
// parseLettermintSignature parses a signature header of the form "t={timestamp},v1={hex}".
func parseLettermintSignature(sig string) (int64, string, error) {
var (
ts int64
hash string
)
for _, part := range strings.Split(sig, ",") {
kv := strings.SplitN(strings.TrimSpace(part), "=", 2)
if len(kv) != 2 {
continue
}
switch kv[0] {
case "t":
if _, err := fmt.Sscanf(kv[1], "%d", &ts); err != nil {
return 0, "", fmt.Errorf("invalid timestamp in signature: %v", err)
}
case "v1":
hash = kv[1]
}
}
if ts == 0 || hash == "" {
return 0, "", fmt.Errorf("invalid signature format")
}
return ts, hash, nil
}
View on GitHub (pinned to 670c01717d)
Solutions
- Log the raw signature header and confirm it matches 't=<unix-ts>,v1=<hex>' exactly
- Check that no proxy/CDN is modifying, truncating, or URL-encoding the signature header
- Re-send the webhook from Lettermint (or replay the official test event) so a fresh, intact signature is generated
- Verify the correct header is being read and passed to ProcessBounce in the HTTP handler wiring
Example fix
// before (client test request) curl -H 'X-Lettermint-Signature: t=abc,v1=deadbeef' ... // after curl -H "X-Lettermint-Signature: t=$(date +%s),v1=$(computed_hmac_hex)" ...
Defensive patterns
Strategy: validation
Validate before calling
func validLettermintSigHeader(sig string) bool {
hasT, hasV1 := false, false
for _, part := range strings.Split(sig, ",") {
kv := strings.SplitN(strings.TrimSpace(part), "=", 2)
if len(kv) != 2 { continue }
switch kv[0] {
case "t":
if _, err := strconv.ParseInt(kv[1], 10, 64); err == nil { hasT = true }
case "v1":
if kv[1] != "" { hasV1 = true }
}
}
return hasT && hasV1
}
// call before invoking ProcessBounce; reject with 400 if false Try / catch
bounces, err := handler.ProcessBounce(sigHeader, body)
if err != nil {
if strings.HasPrefix(err.Error(), "invalid timestamp in signature") {
http.Error(w, "bad signature header", http.StatusBadRequest)
return
}
http.Error(w, "webhook error", http.StatusInternalServerError)
} Prevention
- Never hand-edit or truncate the signature header; forward it verbatim
- Test the webhook pipeline with real events from Lettermint, not hand-built headers
- Check reverse-proxy configs (e.g. nginx underscores_in_headers, header size limits) for header mangling
When it happens
Trigger: ProcessBounce is called with a signature header whose t= value is non-numeric or truncated, e.g. 't=abc,v1=...' or 't=17<..,v1=...', commonly caused by the signature header being cut off, URL-encoded, or overwritten by a proxy/reverse-proxy.
Common situations: A reverse proxy mangles or truncates the X-signature header; the webhook secret/header name is misconfigured so a different header value is passed in; the payload is forwarded through a gateway that re-encodes commas; testing with a hand-crafted cURL command that omits the real header format.
Related errors
- invalid signature format
- error asn1 unmarshal of signature: %v
- invalid azure event grid shared secret
- webhook key is not configured
- invalid signature
AI-assisted analysis of knadh/listmonk@670c01717d (2026-09-01).
Data as JSON: /api/errors/57dc7a49ae9f26fa.
Report an issue: GitHub.