{"record":{"id":"0be5d215d4d8ae2c","repo":"gastownhall/beads","slug":"identity-reply-authentication-failed","errorCode":null,"errorMessage":"identity: reply authentication failed","messagePattern":"identity: reply authentication failed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/dbproxy/identity/control.go","lineNumber":116,"sourceCode":"\treturn reply, nil\n}\n\n// VerifyIdentReply verifies the authenticated reply for the request nonce.\nfunc VerifyIdentReply(reply IdentReply, secret, nonce string) error {\n\tgot, err := hex.DecodeString(reply.MAC)\n\tif err != nil {\n\t\treturn errors.New(\"identity: invalid reply MAC\")\n\t}\n\tsigned, err := SignIdentReply(reply, secret, nonce)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"identity: authenticate reply: %w\", err)\n\t}\n\twant, err := hex.DecodeString(signed.MAC)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"identity: decode expected reply MAC: %w\", err)\n\t}\n\tif !hmac.Equal(got, want) {\n\t\treturn errors.New(\"identity: reply authentication failed\")\n\t}\n\treturn nil\n}\n\nfunc decodeIdentNonce(nonce string) ([]byte, error) {\n\traw, err := hex.DecodeString(nonce)\n\tif err != nil || len(raw) != identNonceBytes {\n\t\treturn nil, errors.New(\"identity: invalid request nonce\")\n\t}\n\treturn raw, nil\n}\n\nfunc canonicalIdentReply(reply IdentReply) ([]byte, error) {\n\tpayload := struct {\n\t\tSchema      int    `json:\"schema\"`\n\t\tRole        string `json:\"role\"`\n\t\tRootID      string `json:\"root_id\"`\n\t\tUpstreamID  string `json:\"upstream_id\"`","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/dbproxy/identity/control.go#L98-L134","documentation":"VerifyIdentReply computes the expected MAC (SignIdentReply over the canonical reply, secret, and nonce) and compares it to the reply's MAC using hmac.Equal. This error means both MACs decoded as hex but differ — the reply was tampered with, or was signed with a different secret or nonce than the request used.","triggerScenarios":"Calling VerifyIdentReply on a reply whose MAC decodes cleanly but does not match: wrong shared secret on the proxy side, a stale/reused nonce, mutated reply fields (role/root_id changed in transit), or a replay from a previous handshake.","commonSituations":"Secret rotation where the proxy still holds the old secret; a man-in-the-middle or rogue socket peer altering reply contents; reusing a captured reply for a new nonce; config drift between beads and its managed proxy.","solutions":["Confirm both sides read the same proxy secret (re-run secret rotation atomically and restart the proxy)","Always generate a fresh nonce per Identify call and verify the reply for that exact nonce","Treat as an authentication failure: reject the peer and do not retry with the same connection","Check for stale proxy processes still running with an old secret after rotation"],"exampleFix":"// before\nif err := identity.VerifyIdentReply(reply, secret, nonce); err != nil { return err }\n// after\nif err := identity.VerifyIdentReply(reply, freshNonce(), secret); err != nil {\n    if strings.Contains(err.Error(), \"authentication failed\") { rotateSecret(); return ErrUntrustedPeer }\n    return err\n}","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"func isAuthFailed(err error) bool { return strings.Contains(err.Error(), \"reply authentication failed\") }","tryCatchPattern":"if err := identity.VerifyIdentReply(reply, secret, nonce); err != nil {\n    if isAuthFailed(err) { rotateSecretAndRedial(); return ErrUntrustedPeer }\n    return err\n}","preventionTips":["Rotate the proxy secret atomically and restart both sides together","Generate a fresh nonce for every Identify call","Never replay captured replies across handshakes","Reject peers immediately on MAC mismatch — do not retry the same connection"],"tags":["identity","hmac","security","tampering"],"backgroundTag":"signature-verification-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}