penguin/homekey-go

homekey-go

README.md

raw ยท 8716 bytes

Home Key provisioning in Go

Basically kormax/apple-home-key-reader in go.

Quick start on macOS

brew install go
make
./bin/homekey -name "Test Lock"

Go 1.23 or newer is required. The first build downloads the pinned Go modules. The executable prints a generated pairing code, for example XXXX-XXXX. It saves that code and its accessory identity for later runs.

  1. Put your iPhone and the computer running this program on the same LAN.
  2. Open Home โ†’ + โ†’ Add Accessory โ†’ More Options (wording can vary by iOS).
  3. Select Penguin Door and enter the printed pairing code.
  4. Accept the uncertified-accessory prompt if displayed and complete setup.
  5. Look for the Home Key setup/Express Mode flow and the resulting key in Wallet.

Use a compatible iPhone with a passcode and Apple Home/iCloud configured. If setup creates only a normal lock, check the provisioning log and status below; pairing alone does not prove that Wallet provisioned a key.

This accessory simulates locked/unlocked state for the Home app. Changing it does not operate a physical lock, and the provisioned key cannot yet be used at an NFC reader. Those connections are the next development phase.

Linux

Install Go 1.23+ through your usual package manager or the Go distribution:

make
./bin/homekey -name "Penguin Door" -interface eth0

No Avahi daemon is required; the process advertises mDNS itself. For the first iPhone test, run directly on a machine on the phone's LAN. Containers and Kubernetes need additional multicast/network configuration; a published TCP port alone does not provide HomeKit discovery. Linux host networking is a possible container setup; host networking on Docker Desktop is not equivalent to a native macOS process for discovery.

Options

-name       Name in Home (default: Go Home Key)
-serial     Stable serial (default: GO-HOMEKEY-001)
-state      Persistent directory (default: ./state)
-pin        Eight-digit PIN, optionally with hyphens; generated if omitted
-addr       HAP TCP listen address (default: :51826)
-interface  LAN interface for mDNS; empty uses eligible interfaces
-finish     silver, black, gold, or tan (default: silver)
-status     Print credential counts and exit; safe while the server is running

The key artwork color may be determined by the first Home Key lock in the home, so changing -finish later may not change the existing Wallet artwork.

On macOS an explicit interface might be en0 or en1:

./bin/homekey -name "Penguin Door" -interface en0 -state ./state

Choose your actual LAN interface. Allow the executable through the firewall: TCP 51826 (or the port you select), and local mDNS UDP 5353. Keep this service on the LAN; HomeKit protocol traffic should not be put behind an HTTP reverse proxy or published as an Internet service. Discovery usually does not cross VLANs without an mDNS reflector and suitable routing.

What successful provisioning looks like

Logs contain summaries such as:

Provisioning exchange complete: reader=true issuers=1 endpoints=1

You can inspect the current counts without displaying any keys:

./bin/homekey -status -state ./state

Example:

{"paired_controllers":1,"reader_provisioned":true,"issuers":1,"endpoints":1}

Persistence and lifecycle

state/state.json contains both HAP pairing secrets and Home Key credentials. Keep the whole directory across restarts and back it up securely. Directory permissions are 0700 and state files are 0600. Writes use a synced temporary file plus atomic rename; directory syncing is best effort. A process lock prevents two servers from accidentally sharing the same state directory.

The reader private key is provided during HomeKit provisioning; this program does not invent a separate key and expect Wallet to accept it. Issuer public keys are derived from HAP pairing records. Provisioned endpoint public keys are validated on P-256 and saved under the corresponding issuer.

Removing a HAP pairing removes that issuer and its enrolled endpoints in the same state update. Removing the last pairing also clears the reader key. Re-adding an identical credential returns Duplicate. Disk errors reject the operation without updating live state. Corrupt saved state stops startup rather than silently losing the pairing.

No private keys, credential TLVs or endpoint public keys are logged. The printed setup PIN is intentionally visible in the startup output. Do not turn on the HAP library's raw debug logging when using real credentials.

To start over, first remove the accessory in Apple Home, stop the process, then move the complete state directory to a backup location and restart. That creates a new accessory identity. Do not delete state merely to restart.

Implemented protocol surface

Service / characteristic Type Behavior
Accessory Information / Hardware Finish 26C Read-only Wallet finish TLV
Lock Mechanism 45 Simulated current/target lock state
Lock Management 44 Version; management commands return unsupported
NFC Access 266 Home Key provisioning service
Configuration State 263 Read/events, fixed 0 as in Python reference
NFC Access Control Point 264 Read/write/write-response; nested TLV8
NFC Access Supported Configuration 265 Advertises 16 issuers and 16 credentials

Reader-key GET/ADD/REMOVE and endpoint ADD are implemented. Endpoint GET and REMOVE return Not Supported because their complete wire semantics are not established by the reference. Entire issuer removal through HomeKit unpairing is implemented. Per-device removal while keeping an issuer paired needs a follow-up implementation before this controls a real lock. Active/inactive credential state is preserved for later authentication policy.

There is no NFC authentication, attestation, ESP32 bridge, physical-lock adapter or cloud API in this project. First validate provisioning on the phone; then add the ESP32 transport and authentication service.

Build and test

make                 # bin/homekey
make test            # application tests + relevant patched HAP tests
make race            # same tests with Go race detection (requires C compiler)
make vet
make clean           # removes binaries; preserves credentials

The application builds without CGO. Linux amd64 was executed; macOS arm64 and amd64 were cross-compiled. Actual LAN discovery and Wallet setup on your Mac/iPhone remain to be verified.

Tests cover TLV fragmentation and malformed input, reader/device provisioning, unknown issuers, invalid keys, duplicate writes, atomic persistence failure, restart, unpair cleanup, concurrent access, characteristic metadata, and rejection of unauthenticated HAP requests.

An independent aiohomekit client was also used during development to verify SRP Pair Setup, Pair Verify, encrypted accessory discovery and provisioning, repeated requests, reconnect after server restart, and unpair/revocation. That client is a development test tool only and is not shipped or required. It does not emulate Apple's Wallet issuance service.

Dependencies and local patches

third_party/hap contains github.com/brutella/hap v0.0.35 at commit bc03ca629d44dd58570c2979843f9f677818e31c, with three focused changes recorded in patches/hap.patch:

  1. Opt-in stateless command characteristics: process repeated requests and never retain the incoming key-bearing TLV as a characteristic value.
  2. Reject missing timed-write context instead of dereferencing nil.
  3. Flush the complete unpair response before closing the controller connection.

The local replace in go.mod is intentional. Keep third_party/hap with the project; switching directly to the upstream module drops these fixes. Application tests plus selected HAP regression tests are run by make test. The upstream full test suite has an unrelated existing TestConfigHash service-ID reuse failure; this project does not claim it passes that suite.

Protocol sources:

Apache-2.0; see LICENSE, NOTICE and the retained third-party license.