larksuite/cli · error
HMAC signature mismatch
Error message
HMAC signature mismatch
What it means
Verify() computes the expected HMAC-SHA256 over the canonical request string and compares it (constant-time, hmac.Equal) to the provided signature. This error means the bytes differ: the signature was not produced by the shared key over exactly the same CanonicalRequest fields. Field order and content of CanonicalRequest are the protocol contract; any mismatch in any field invalidates the signature.
Source
Thrown at sidecar/hmac.go:80
mac.Write([]byte(req.canonicalString()))
return hex.EncodeToString(mac.Sum(nil))
}
// Verify checks that signature matches the HMAC-SHA256 of the canonical
// request and that the timestamp is within MaxTimestampDrift seconds of now.
// Returns nil on success.
func Verify(key []byte, req CanonicalRequest, signature string) error {
ts, err := strconv.ParseInt(req.Timestamp, 10, 64)
if err != nil {
return fmt.Errorf("invalid timestamp %q: %w", req.Timestamp, err)
}
drift := math.Abs(float64(time.Now().Unix() - ts))
if drift > MaxTimestampDrift {
return fmt.Errorf("timestamp drift %.0fs exceeds limit %ds", drift, MaxTimestampDrift)
}
expected := Sign(key, req)
if !hmac.Equal([]byte(expected), []byte(signature)) {
return fmt.Errorf("HMAC signature mismatch")
}
return nil
}
// Timestamp returns the current Unix epoch seconds as a string.
func Timestamp() string {
return strconv.FormatInt(time.Now().Unix(), 10)
}
View on GitHub (pinned to 7fd6ef3c07)
Solutions
- Confirm both sides load the identical shared key bytes (same env var/secret value; beware trailing whitespace/newlines from files).
- Verify every CanonicalRequest field is identical between sign and verify: same Version constant, Method, Host, PathAndQuery (raw query preserved), BodySHA256 of the exact body bytes, Timestamp, Identity, AuthHeader.
- Re-sign the request immediately before sending after any mutation of method, URL, body, or headers.
- Ensure the signature is the hex string returned by sidecar.Sign, transmitted unmodified in X-Lark-Proxy-Signature.
- Compare canonical strings on both sides (temporarily log req.canonicalString() inputs via the public fields) to spot the differing field.
Example fix
// before req.BodySHA256 = sidecar.BodySHA256(body) req.PathAndQuery = url.Path // path only, query dropped sig := sidecar.Sign(key, req) // after req.BodySHA256 = sidecar.BodySHA256(body) req.PathAndQuery = u.Path + "?" + u.RawQuery // exact path + raw query as sent sig := sidecar.Sign(key, req)
Defensive patterns
Strategy: validation
Validate before calling
// Ensure the exact same CanonicalRequest fields are signed and sent:
req := sidecar.CanonicalRequest{
Version: sidecar.ProtocolV1,
Method: method,
Host: host,
PathAndQuery: u.Path + "?" + u.RawQuery,
BodySHA256: sidecar.BodySHA256(body),
Timestamp: sidecar.Timestamp(),
Identity: identity,
AuthHeader: authHeader,
}
if len(key) == 0 { return errors.New("empty HMAC key") }
sig := sidecar.Sign(key, req) Type guard
func signatureKeyConfigured(key []byte) bool { return len(key) > 0 } Try / catch
if err := sidecar.Verify(key, req, sig); err != nil {
if strings.Contains(err.Error(), "HMAC signature mismatch") {
// do not blind-retry; log non-secret diagnostics:
// key source, and each CanonicalRequest field signed on the client
return fmt.Errorf("signature rejected: check shared key and signed fields: %w", err)
}
return err
} Prevention
- Load the shared key identically on both sides; trim trailing whitespace/newlines from secret files.
- Never mutate method, URL, query, body, or headers after signing — re-sign instead.
- Send the hex signature from sidecar.Sign verbatim in X-Lark-Proxy-Signature.
- Pin Version to sidecar.ProtocolV1 on both client and server.
- Remember PathAndQuery must include the raw query string exactly as transmitted.
When it happens
Trigger: sidecar.Verify is given a signature computed with a different key, or the CanonicalRequest passed to Verify differs in ANY field (Version, Method, Host, PathAndQuery, BodySHA256, Timestamp, Identity, AuthHeader) from what was signed — e.g. body bytes changed after signing, query string re-encoded, different AuthHeader default, or the signature header got altered/truncated in transit.
Common situations: Client and sidecar configured with different shared secrets; a proxy or middleware rewriting the URL/body/Host header after signing; signing with an empty/nil key by mistake; encoding the signature as base64 instead of hex (Sign returns hex); forwarding headers with different casing through a stack that lowercases or drops them; tampering attempts.
Related errors
- invalid timestamp %q: %w
- timestamp drift %.0fs exceeds limit %ds
- HTTP %d: [%d] %s
- [%d] %s
- insert_signature: no HTML body part found; use set_body firs
AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04).
Data as JSON: /api/errors/f43bf4840497f696.
Report an issue: GitHub.