{"record":{"id":"3ebe096b3b29023f","repo":"AlexxIT/go2rtc","slug":"hap-pairverify-with-unknown-client-id","errorCode":null,"errorMessage":"hap: PairVerify with unknown client_id: ","messagePattern":"hap: PairVerify with unknown client_id: ","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/hap/server.go","lineNumber":341,"sourceCode":"\t}\n\n\tb, err = chacha20poly1305.Decrypt(encryptKey, \"PV-Msg03\", []byte(cipherM3.EncryptedData))\n\tif err != nil {\n\t\treturn\n\t}\n\n\tvar plainM3 struct {\n\t\tIdentifier string `tlv8:\"1\"`\n\t\tSignature  string `tlv8:\"10\"`\n\t}\n\tif err = tlv8.Unmarshal(b, &plainM3); err != nil {\n\t\treturn\n\t}\n\n\tif s.GetClientPublic != nil {\n\t\tclientPublic := s.GetClientPublic(plainM3.Identifier)\n\t\tif clientPublic == nil {\n\t\t\terr = errors.New(\"hap: PairVerify with unknown client_id: \" + plainM3.Identifier)\n\t\t\treturn\n\t\t}\n\n\t\tb = Append(plainM1.PublicKey, plainM3.Identifier, sessionPublic)\n\t\tif !ed25519.ValidateSignature(clientPublic, b, []byte(plainM3.Signature)) {\n\t\t\terr = errors.New(\"hap: ValidateSignature\")\n\t\t\treturn\n\t\t}\n\t}\n\n\t// STEP M4. Response to iPhone\n\tpayloadM4 := struct {\n\t\tState byte `tlv8:\"6\"`\n\t}{\n\t\tState: StateM4,\n\t}\n\tif body, err = tlv8.Marshal(payloadM4); err != nil {\n\t\treturn","sourceCodeStart":323,"sourceCodeEnd":359,"githubUrl":"https://github.com/AlexxIT/go2rtc/blob/c245815e75e2a5fd60b4290f12bfc04e55a984d3/pkg/hap/server.go#L323-L359","documentation":"During HAP Pair-Verify step M3, the accessory decrypts the client's message and looks up the client's persisted LTPK (Ed25519 public key) via the server's GetClientPublic callback using the Identifier (client_id) sent by the controller. This error is returned when the callback returns nil, i.e. the accessory has no pairing record for that client_id. It means the controller claims an identity the accessory was never paired with (or pairing records were deleted).","triggerScenarios":"Server.PairVerify is called (via Handle) with an M3 payload whose decrypted Identifier is not present in the accessory's pairing store, so s.GetClientPublic(identifier) returns nil. This fires only when GetClientPublic is non-nil; with a nil callback the check is skipped.","commonSituations":"Client re-paired with a different accessory; accessory database wiped/reset but controller still has old keys; multiple accessories sharing one pairing store; test harness sending a fabricated Identifier; stale controller cache after removing the pairing on the accessory side (which should trigger the controller to re-run Pair-Setup).","solutions":["Re-run Pair-Setup (Pairing) between controller and accessory so a pairing record with this Identifier is created before Pair-Verify.","Check the GetClientPublic implementation/lookup store: ensure it is backed by the same database the pairing process writes to and that the record exists.","If the accessory was reset, remove the stale pairing on the controller and pair from scratch.","In tests, seed the pairing store (or set GetClientPublic to nil to skip verification) before exercising PairVerify."],"exampleFix":"// before: controller retries Pair-Verify with stale identity\nclient.VerifyConnection(accessory) // hap: PairVerify with unknown client_id: ...\n\n// after: detect unknown-client and fall back to Pair-Setup\nif err := client.VerifyConnection(accessory); err != nil && strings.Contains(err.Error(), \"unknown client_id\") {\n    if err := client.PairSetup(accessory); err != nil { return err }\n    return client.VerifyConnection(accessory)\n}","handlingStrategy":"validation","validationCode":"// before invoking Pair-Verify against the accessory, ensure the pairing exists\nif acc.GetClientPublic == nil || acc.GetClientPublic(clientID) == nil {\n    // no pairing record: run Pair-Setup first instead of Pair-Verify\n    return client.PairSetup(acc)\n}","typeGuard":"func hasPairing(store PairingStore, id string) bool {\n    return store != nil && store.LookupLTPK(id) != nil\n}","tryCatchPattern":null,"preventionTips":["Keep controller and accessory pairing databases in sync; never wipe one side alone.","Treat unknown-client errors as a signal to fall back to Pair-Setup, not to retry Pair-Verify.","In tests, seed the pairing store with the controller's LTPK before Pair-Verify."],"tags":["hap","pair-verify","authentication","homekit"],"backgroundTag":"resource-not-found","analyzedSha":"c245815e75e2a5fd60b4290f12bfc04e55a984d3","analyzedAt":"2026-09-07T11:47:02.965Z","contentChangedAt":"2026-09-07T11:47:02.965Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}