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

  1. Log the raw signature header and confirm it matches 't=<unix-ts>,v1=<hex>' exactly
  2. Check that no proxy/CDN is modifying, truncating, or URL-encoding the signature header
  3. Re-send the webhook from Lettermint (or replay the official test event) so a fresh, intact signature is generated
  4. 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

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


AI-assisted analysis of knadh/listmonk@670c01717d (2026-09-01). Data as JSON: /api/errors/57dc7a49ae9f26fa. Report an issue: GitHub.