AlexxIT/go2rtc · error

hap: PairVerify with unknown client_id:

Error message

hap: PairVerify with unknown client_id: 

What it means

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).

Solutions

  1. Re-run Pair-Setup (Pairing) between controller and accessory so a pairing record with this Identifier is created before Pair-Verify.
  2. Check the GetClientPublic implementation/lookup store: ensure it is backed by the same database the pairing process writes to and that the record exists.
  3. If the accessory was reset, remove the stale pairing on the controller and pair from scratch.
  4. In tests, seed the pairing store (or set GetClientPublic to nil to skip verification) before exercising PairVerify.

Example fix

// before: controller retries Pair-Verify with stale identity
client.VerifyConnection(accessory) // hap: PairVerify with unknown client_id: ...

// after: detect unknown-client and fall back to Pair-Setup
if err := client.VerifyConnection(accessory); err != nil && strings.Contains(err.Error(), "unknown client_id") {
    if err := client.PairSetup(accessory); err != nil { return err }
    return client.VerifyConnection(accessory)
}
Defensive patterns

Strategy: validation

Validate before calling

// before invoking Pair-Verify against the accessory, ensure the pairing exists
if acc.GetClientPublic == nil || acc.GetClientPublic(clientID) == nil {
    // no pairing record: run Pair-Setup first instead of Pair-Verify
    return client.PairSetup(acc)
}

Type guard

func hasPairing(store PairingStore, id string) bool {
    return store != nil && store.LookupLTPK(id) != nil
}

Prevention

When it happens

Trigger: 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.

Common situations: 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).

Understand the failure class

Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.

Related errors


AI-assisted analysis of AlexxIT/go2rtc@c245815e75 (2026-09-07). Data as JSON: /api/errors/3ebe096b3b29023f. Report an issue: GitHub.

Appendix: source

Thrown at pkg/hap/server.go:341

	}

	b, err = chacha20poly1305.Decrypt(encryptKey, "PV-Msg03", []byte(cipherM3.EncryptedData))
	if err != nil {
		return
	}

	var plainM3 struct {
		Identifier string `tlv8:"1"`
		Signature  string `tlv8:"10"`
	}
	if err = tlv8.Unmarshal(b, &plainM3); err != nil {
		return
	}

	if s.GetClientPublic != nil {
		clientPublic := s.GetClientPublic(plainM3.Identifier)
		if clientPublic == nil {
			err = errors.New("hap: PairVerify with unknown client_id: " + plainM3.Identifier)
			return
		}

		b = Append(plainM1.PublicKey, plainM3.Identifier, sessionPublic)
		if !ed25519.ValidateSignature(clientPublic, b, []byte(plainM3.Signature)) {
			err = errors.New("hap: ValidateSignature")
			return
		}
	}

	// STEP M4. Response to iPhone
	payloadM4 := struct {
		State byte `tlv8:"6"`
	}{
		State: StateM4,
	}
	if body, err = tlv8.Marshal(payloadM4); err != nil {
		return

View on GitHub (pinned to c245815e75)