knadh/listmonk · error

error asn1 unmarshal of signature: %v

Error message

error asn1 unmarshal of signature: %v

What it means

SendGrid signs webhook payloads with ECDSA; the base64-decoded signature must be a DER/ASN.1-encoded ECDSA signature (R,S). verifyNotif asn1.Unmarshals the decoded bytes into an {R,S} struct, and returns 'error asn1 unmarshal of signature' when the bytes are not a valid ASN.1 ECDSA-Sig-Value structure.

Source

Thrown at internal/bounce/webhooks/sendgrid.go:102

	}

	return out, nil
}

// verifyNotif verifies the signature on a notification payload.
func (s *Sendgrid) verifyNotif(sig, timestamp string, b []byte) error {
	sigB, err := base64.StdEncoding.DecodeString(sig)
	if err != nil {
		return err
	}

	ecdsaSig := struct {
		R *big.Int
		S *big.Int
	}{}

	if _, err := asn1.Unmarshal(sigB, &ecdsaSig); err != nil {
		return fmt.Errorf("error asn1 unmarshal of signature: %v", err)
	}

	h := sha256.New()
	h.Write([]byte(timestamp))
	h.Write(b)
	hash := h.Sum(nil)

	if !ecdsa.Verify(s.pubKey, hash, ecdsaSig.R, ecdsaSig.S) {
		return errors.New("invalid signature")
	}

	return nil
}

View on GitHub (pinned to 670c01717d)

Solutions

  1. Confirm SendGrid Event Webhook signature verification is enabled (ECDSA v1) so the header contains a base64 ASN.1 signature
  2. Verify the signature header is passed through unmodified (base64, no truncation or added whitespace) by proxies
  3. Regenerate/copy the exact signature from the actual request headers when testing manually
  4. Ensure the ECDSA public key configured in NewSendgrid matches the one SendGrid currently signs with

Example fix

// before (test header with raw hex r||s)
sig := hex.EncodeToString(sigRS)
// after
sig := base64.StdEncoding.EncodeToString(asn1EncodedEcdsaSig) // DER ECDSA-Sig-Value
Defensive patterns

Strategy: validation

Validate before calling

func validEcdsaSigHeader(sig string) bool {
    sigB, err := base64.StdEncoding.DecodeString(sig)
    if err != nil { return false }
    var ecdsaSig struct {
        R *big.Int
        S *big.Int
    }
    _, err = asn1.Unmarshal(sigB, &ecdsaSig)
    return err == nil && ecdsaSig.R != nil && ecdsaSig.S != nil
}
// return 400 if !validEcdsaSigHeader(sigHeader)

Try / catch

bounces, err := handler.ProcessBounce(sig, ts, body)
if err != nil {
    if strings.Contains(err.Error(), "error asn1 unmarshal of signature") {
        log.Printf("bad ECDSA signature header (len=%d)", len(sig))
        http.Error(w, "invalid signature", http.StatusUnauthorized)
        return
    }
    http.Error(w, "webhook error", http.StatusInternalServerError)
}

Prevention

When it happens

Trigger: ProcessBounce receives an X-Webhook-Signature header whose base64-decoded bytes are not DER-encoded ECDSA (R,S): raw r||s concatenation, hex instead of base64, truncated signature, empty header, or a signature produced by a different scheme (e.g. HMAC).

Common situations: SendGrid webhook verification settings changed between signature schemes/versions; copying the verification key or signature incorrectly when testing; a proxy truncating the header; using the wrong SendGrid public key so the test signature was generated under a different format; SendGrid rotating their signing cert.

Related errors


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