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

  1. Confirm both sides load the identical shared key bytes (same env var/secret value; beware trailing whitespace/newlines from files).
  2. 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.
  3. Re-sign the request immediately before sending after any mutation of method, URL, body, or headers.
  4. Ensure the signature is the hex string returned by sidecar.Sign, transmitted unmodified in X-Lark-Proxy-Signature.
  5. 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

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


AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04). Data as JSON: /api/errors/f43bf4840497f696. Report an issue: GitHub.