knadh/listmonk · error

invalid signature

Error message

invalid signature

What it means

Lettermint signs the string "{timestamp}.{body}" with HMAC-SHA256 using the shared webhook key. After decoding the hex signature, ProcessBounce compares it against the computed MAC with hmac.Equal. This error means the digests don't match: the body or timestamp was modified in transit, or the wrong signing key is configured.

Source

Thrown at internal/bounce/webhooks/lettermint.go:72

	}

	// 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 {
		return nil, fmt.Errorf("error unmarshalling Lettermint notification: %v", err)
	}

	// Map event to bounce type.
	var typ string
	switch n.Event {
	case "message.hard_bounced":
		typ = models.BounceTypeHard
	case "message.soft_bounced":
		typ = models.BounceTypeSoft
	case "message.spam_complaint":
		typ = models.BounceTypeComplaint
	default:
		// Ignore irrelevant events (e.g. webhook.test).

View on GitHub (pinned to 670c01717d)

Solutions

  1. Verify the configured key exactly matches the webhook signing secret shown in your Lettermint dashboard (same environment).
  2. Ensure the HMAC is computed over "{timestamp}.{body}" using the exact bytes of the request body — pass the raw body untouched to ProcessBounce.
  3. Check no middleware modifies the body between receipt and processing; read the body once and pass those exact bytes.
  4. For local testing, recompute the signature with the documented scheme and current timestamp rather than reusing captured headers.

Example fix

// before: signing body only
digest := hmac.New(sha256.New, key); digest.Write(body)

// after: sign "{timestamp}.{body}" as Lettermint does
ts := time.Now().Unix()
digest := hmac.New(sha256.New, key)
digest.Write([]byte(fmt.Sprintf("%d.%s", ts, body)))
sig := fmt.Sprintf("t=%d,v1=%s", ts, hex.EncodeToString(digest.Sum(nil)))
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-check the signing key and header presence before calling the handler:
func canVerifyLettermint(key []byte, sigHeader string) bool {
    return len(key) > 0 &&
        strings.Contains(sigHeader, "t=") &&
        strings.Contains(sigHeader, "v1=")
}

Try / catch

bounces, err := lm.ProcessBounce(sig, body)
if err != nil {
    if err.Error() == "invalid signature" {
        log.Printf("lettermint HMAC mismatch: check signing key and that body/timestamp bytes are unmodified")
        http.Error(w, "unauthorized", http.StatusUnauthorized) // never 500 — this is an auth failure
        return
    }
    http.Error(w, "bad request", http.StatusBadRequest)
}

Prevention

When it happens

Trigger: Any of: the request body was re-encoded/altered by middleware after signing; the t= timestamp in the header doesn't match the one used when signing; the configured hmacKey differs from Lettermint's actual signing secret; the v1 value was computed over the body alone (no "{ts}." prefix); replayed/templated test requests.

Common situations: A proxy or WAF modifying the body (re-compression, charset transcoding); reading the body from req.Body then passing a modified copy; copying the signing key from the wrong provider/environment (staging key vs production webhooks); signing with JSON re-serialization that differs byte-for-byte from the sent body; framework middleware that normalizes line endings.

Related errors


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