{"record":{"id":"f43bf4840497f696","repo":"larksuite/cli","slug":"hmac-signature-mismatch","errorCode":null,"errorMessage":"HMAC signature mismatch","messagePattern":"HMAC signature mismatch","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"sidecar/hmac.go","lineNumber":80,"sourceCode":"\tmac.Write([]byte(req.canonicalString()))\n\treturn hex.EncodeToString(mac.Sum(nil))\n}\n\n// Verify checks that signature matches the HMAC-SHA256 of the canonical\n// request and that the timestamp is within MaxTimestampDrift seconds of now.\n// Returns nil on success.\nfunc Verify(key []byte, req CanonicalRequest, signature string) error {\n\tts, err := strconv.ParseInt(req.Timestamp, 10, 64)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"invalid timestamp %q: %w\", req.Timestamp, err)\n\t}\n\tdrift := math.Abs(float64(time.Now().Unix() - ts))\n\tif drift > MaxTimestampDrift {\n\t\treturn fmt.Errorf(\"timestamp drift %.0fs exceeds limit %ds\", drift, MaxTimestampDrift)\n\t}\n\texpected := Sign(key, req)\n\tif !hmac.Equal([]byte(expected), []byte(signature)) {\n\t\treturn fmt.Errorf(\"HMAC signature mismatch\")\n\t}\n\treturn nil\n}\n\n// Timestamp returns the current Unix epoch seconds as a string.\nfunc Timestamp() string {\n\treturn strconv.FormatInt(time.Now().Unix(), 10)\n}\n","sourceCodeStart":62,"sourceCodeEnd":89,"githubUrl":"https://github.com/larksuite/cli/blob/7fd6ef3c07182257ce776cdc5a614e122d5bd4b3/sidecar/hmac.go#L62-L89","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nreq.BodySHA256 = sidecar.BodySHA256(body)\nreq.PathAndQuery = url.Path // path only, query dropped\nsig := sidecar.Sign(key, req)\n// after\nreq.BodySHA256 = sidecar.BodySHA256(body)\nreq.PathAndQuery = u.Path + \"?\" + u.RawQuery // exact path + raw query as sent\nsig := sidecar.Sign(key, req)","handlingStrategy":"validation","validationCode":"// Ensure the exact same CanonicalRequest fields are signed and sent:\nreq := sidecar.CanonicalRequest{\n\tVersion: sidecar.ProtocolV1,\n\tMethod: method,\n\tHost: host,\n\tPathAndQuery: u.Path + \"?\" + u.RawQuery,\n\tBodySHA256: sidecar.BodySHA256(body),\n\tTimestamp: sidecar.Timestamp(),\n\tIdentity: identity,\n\tAuthHeader: authHeader,\n}\nif len(key) == 0 { return errors.New(\"empty HMAC key\") }\nsig := sidecar.Sign(key, req)","typeGuard":"func signatureKeyConfigured(key []byte) bool { return len(key) > 0 }","tryCatchPattern":"if err := sidecar.Verify(key, req, sig); err != nil {\n\tif strings.Contains(err.Error(), \"HMAC signature mismatch\") {\n\t\t// do not blind-retry; log non-secret diagnostics:\n\t\t// key source, and each CanonicalRequest field signed on the client\n\t\treturn fmt.Errorf(\"signature rejected: check shared key and signed fields: %w\", err)\n\t}\n\treturn err\n}","preventionTips":["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."],"tags":["hmac","signature","authentication","sidecar","key-mismatch"],"backgroundTag":"hmac-signature-mismatch","analyzedSha":"7fd6ef3c07182257ce776cdc5a614e122d5bd4b3","analyzedAt":"2026-09-04T21:17:44.649Z","contentChangedAt":"2026-09-04T21:17:44.649Z","schemaVersion":2},"datasetVersion":"2026-09-12T02:17:10.037Z"}