knadh/listmonk · error
signature timestamp expired
Error message
signature timestamp expired
What it means
Lettermint signs webhooks with a timestamped scheme (t={timestamp},v1={hex}). To prevent replay attacks, ProcessBounce rejects signatures whose timestamp is more than 300 seconds (5 minutes) away from the server's current time. This error means the signature header parsed fine but is stale (or skewed).
Source
Thrown at internal/bounce/webhooks/lettermint.go:58
func NewLettermint(key []byte) *Lettermint {
return &Lettermint{hmacKey: key}
}
// ProcessBounce processes an incoming Lettermint webhook payload and returns a bounce object.
func (l *Lettermint) ProcessBounce(sig string, body []byte) ([]models.Bounce, error) {
if len(l.hmacKey) == 0 {
return nil, fmt.Errorf("webhook key is not configured")
}
// Parse the signature header: t={timestamp},v1={hex_signature}.
ts, sigHex, err := parseLettermintSignature(sig)
if err != nil {
return nil, err
}
// Verify timestamp tolerance (300 seconds).
if math.Abs(float64(time.Now().Unix()-ts)) > 300 {
return nil, fmt.Errorf("signature timestamp expired")
}
// Decode the hex signature from the header.
sigB, err := hex.DecodeString(strings.TrimSpace(sigHex))
if err != nil {
return nil, fmt.Errorf("invalid signature encoding: %v", err)
}
// Compute HMAC-SHA256 of "{timestamp}.{body}" and compare.
mac := hmac.New(sha256.New, l.hmacKey)
mac.Write([]byte(fmt.Sprintf("%d.%s", ts, body)))
if !hmac.Equal(mac.Sum(nil), sigB) {
return nil, fmt.Errorf("invalid signature")
}
var n lettermintNotif
if err := json.Unmarshal(body, &n); err != nil {View on GitHub (pinned to 670c01717d)
Solutions
- Sync the server clock with NTP (chrony/systemd-timesyncd) — clock skew is the most common cause of false expirations.
- Ensure webhooks are processed promptly on receipt; if queueing, process within 5 minutes or accept that stale events will be rejected.
- Replay only fresh signed requests when testing; re-request a test webhook from Lettermint instead of reusing old captures.
- If delays are unavoidable, consider a proxy that re-validates/forwards immediately rather than buffering signed bodies.
Example fix
// before: stale signature reused in a test
sig := "t=1700000000,v1=abcd..." // days old
bounces, err := lm.ProcessBounce(sig, body) // signature timestamp expired
// after: generate a fresh signature at request time
ts := time.Now().Unix()
mac := hmac.New(sha256.New, key)
mac.Write([]byte(fmt.Sprintf("%d.%s", ts, body)))
sig := fmt.Sprintf("t=%d,v1=%s", ts, hex.EncodeToString(mac.Sum(nil))) Defensive patterns
Strategy: try-catch
Validate before calling
func isFreshLettermintSignature(sig string) (bool, error) {
// parse t=... and compare against now within 300s before calling the handler
for _, part := range strings.Split(sig, ",") {
if kv := strings.SplitN(strings.TrimSpace(part), "=", 2); len(kv) == 2 && kv[0] == "t" {
var ts int64
if _, err := fmt.Sscanf(kv[1], "%d", &ts); err != nil {
return false, err
}
return math.Abs(float64(time.Now().Unix()-ts)) <= 300, nil
}
}
return false, errors.New("no timestamp in signature")
} Try / catch
bounces, err := lm.ProcessBounce(sig, body)
if err != nil {
if err.Error() == "signature timestamp expired" {
// stale or clock-skewed; optionally reject old events without alerting
log.Printf("stale lettermint signature (ts too old or clock skew): %v", err)
http.Error(w, "expired signature", http.StatusUnauthorized)
return
}
http.Error(w, "bad request", http.StatusBadRequest)
} Prevention
- Run NTP time sync on all servers processing webhooks.
- Process webhook requests immediately; don't queue them longer than the 5-minute tolerance.
- Monitor for spikes of this error — a sudden rise usually means clock drift, not an attack.
- In tests, always generate fresh signatures at request time.
When it happens
Trigger: Processing a webhook whose signature was generated >300s ago: delayed delivery/retries from Lettermint, a queued/replayed request, clock drift between your server and Lettermint, or replaying a captured request in tests.
Common situations: Server clock out of sync (no NTP) causing even fresh webhooks to fail; webhook queue backlog delaying processing beyond 5 minutes; debugging with an old saved request; container with wrong timezone/clock; Lettermint retrying a failed delivery after their own backoff.
Related errors
- webhook key is not configured
- invalid signature encoding: %v
- invalid signature
- invalid azure event grid shared secret
- webhook key is not configured
AI-assisted analysis of knadh/listmonk@670c01717d (2026-09-01).
Data as JSON: /api/errors/c8e6097afc277024.
Report an issue: GitHub.