knadh/listmonk · error

invalid signature encoding: %v

Error message

invalid signature encoding: %v

What it means

Forwardemail's ProcessBounce expects the webhook signature as a hex-encoded HMAC-SHA256 digest. This error is thrown when the signature header value cannot be decoded from hex — before any HMAC comparison happens — meaning the header is not valid hexadecimal.

Source

Thrown at internal/bounce/webhooks/forwardemail.go:57

// Forwardemail handles webhook notifications (mainly bounce notifications).
type Forwardemail struct {
	hmacKey []byte
}

func NewForwardemail(key []byte) *Forwardemail {
	return &Forwardemail{hmacKey: key}
}

func (p *Forwardemail) ProcessBounce(sigHex string, body []byte) ([]models.Bounce, error) {
	if len(p.hmacKey) == 0 {
		return nil, errors.New("webhook key is not configured")
	}

	// Decode the hex-encoded signature from the webhook
	sig, err := hex.DecodeString(sigHex)
	if err != nil {
		return nil, fmt.Errorf("invalid signature encoding: %v", err)
	}

	// Generate HMAC using the request body and secret key
	mac := hmac.New(sha256.New, p.hmacKey)
	mac.Write(body)
	expectedSignature := mac.Sum(nil)

	// Compare the generated signature with the provided signature
	if !hmac.Equal(expectedSignature, sig) {
		return nil, errors.New("invalid signature")
	}

	// Parse the JSON payload
	var n forwardemailNotif
	if err := json.Unmarshal(body, &n); err != nil {
		return nil, fmt.Errorf("error unmarshalling Forwardemail notification: %v", err)
	}

View on GitHub (pinned to 670c01717d)

Solutions

  1. Pass only the raw hex portion of the signature header; strip any "sha256=" or similar prefix before calling ProcessBounce.
  2. Verify the header actually contains hex: all characters in [0-9a-fA-F] and even length.
  3. Check with `echo <sig> | xxd -r -p` (or equivalent) that the value decodes as hex.
  4. Log the header value (length + first chars) on failure to spot encoding or prefix issues; base64 signatures from another provider will fail here.

Example fix

// before: passing prefixed header value
sig := r.Header.Get("X-Signature") // "sha256=9f86d081..."
bounces, err := fw.ProcessBounce(sig, body) // invalid signature encoding

// after: strip prefix, pass pure hex
sig := strings.TrimPrefix(r.Header.Get("X-Signature"), "sha256=")
bounces, err := fw.ProcessBounce(sig, body)
Defensive patterns

Strategy: try-catch

Validate before calling

func isHex(s string) bool {
    _, err := hex.DecodeString(strings.TrimSpace(s))
    return err == nil && len(s)%2 == 0
}
// usage before calling: if !isHex(sigHeader) { reject request }

Try / catch

bounces, err := fw.ProcessBounce(sig, body)
if err != nil {
    switch {
    case strings.Contains(err.Error(), "invalid signature encoding"):
        http.Error(w, "signature must be hex-encoded", http.StatusBadRequest)
    case strings.Contains(err.Error(), "invalid signature"):
        http.Error(w, "unauthorized", http.StatusUnauthorized)
    default:
        http.Error(w, "bad request", http.StatusBadRequest)
    }
    return
}

Prevention

When it happens

Trigger: Calling ProcessBounce with a sigHex value that is: base64-encoded instead of hex, contains a scheme prefix (e.g. "sha256=abcd"), has odd length or non-hex characters (whitespace, 0x prefix, uppercase g-z), or is empty/garbage from a missing header defaulting to something else.

Common situations: Forwardemail changing their signature format; a proxy or framework normalizing/transforming the header; developer passing the whole Authorization header value including a prefix; config mistake reading a base64 secret-signature instead of the hex one; HTTP header containing trailing newline not trimmed.

Related errors


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