knadh/listmonk · error

invalid signature encoding: %v

Error message

invalid signature encoding: %v

What it means

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.

Source

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

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

	// Map event to bounce type.
	var typ string
	switch n.Event {

View on GitHub (pinned to 670c01717d)

Solutions

  1. Verify the v1 value decodes as hex: only [0-9a-f] characters and even length (64 chars for HMAC-SHA256).
  2. Keep the header in Lettermint's exact scheme: "t={unix_ts},v1={hex_hmac_of_timestamp.body}".
  3. Trim whitespace/quotes when extracting the header; check proxies aren't modifying it.
  4. For tests, generate the signature with hex.EncodeToString(hmac.New(sha256.New, key).Sum(nil)) rather than pasting values.

Example fix

// before: base64 digest in v1
sig := fmt.Sprintf("t=%d,v1=%s", ts, base64.StdEncoding.EncodeToString(mac.Sum(nil)))

// after: hex digest in v1
sig := fmt.Sprintf("t=%d,v1=%s", ts, hex.EncodeToString(mac.Sum(nil)))
Defensive patterns

Strategy: try-catch

Validate before calling

func isHexSignatureV1(sig string) bool {
    for _, part := range strings.Split(sig, ",") {
        if kv := strings.SplitN(strings.TrimSpace(part), "=", 2); len(kv) == 2 && kv[0] == "v1" {
            _, err := hex.DecodeString(strings.TrimSpace(kv[1]))
            return err == nil
        }
    }
    return false
}

Try / catch

bounces, err := lm.ProcessBounce(sig, body)
if err != nil {
    if strings.Contains(err.Error(), "invalid signature encoding") {
        log.Printf("lettermint v1 signature is not hex: header=%q", sig)
        http.Error(w, "bad signature format", http.StatusBadRequest)
        return
    }
    http.Error(w, "unauthorized", http.StatusUnauthorized)
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


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