commit 94e32c34840a07687276e81b7030d224d10a413f
| author | 斟酌 鵬兄 <tgckpg@gmail.com> |
| date | 2026-10-05T08:19:32Z |
| subject | Express Mode routing |
commit 94e32c34840a07687276e81b7030d224d10a413f
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date: 2026-10-05T08:19:32Z
Express Mode routing
---
README.md | 10 +++++++++-
build.env | 2 +-
cmd/homekey/main.go | 2 +-
ha-app/CHANGELOG.md | 7 +++++++
ha-app/NFC.md | 19 +++++++++++++------
ha-app/config.yaml | 2 +-
internal/blegateway/gateway.go | 28 +++++++++++++++++++++++++---
internal/blegateway/relay.go | 7 +++++++
internal/blegateway/relay_test.go | 25 +++++++++++++++++++++++++
internal/provision/auth.go | 14 ++++++++++++++
internal/provision/auth_test.go | 27 +++++++++++++++++++++++++++
11 files changed, 130 insertions(+), 13 deletions(-)
diff --git a/README.md b/README.md
index abb0c04..02c4b96 100644
--- a/README.md
+++ b/README.md
@@ -145,7 +145,7 @@ follow-up implementation before this controls a real lock. Active/inactive
credential state is enforced by NFC authentication.
NFC STANDARD authentication and the ESP32 BLE APDU bridge are implemented.
-FAST, ECP express wakeup, NFC attestation, a physical-lock adapter and a cloud
+FAST, NFC attestation, a physical-lock adapter and a cloud
API are not implemented. Unknown/inactive endpoints are rejected.
## Build and test
@@ -181,3 +181,11 @@ Protocol sources:
- https://github.com/ikalchev/HAP-python (Home Key characteristic metadata)
Apache-2.0; see LICENSE, NOTICE and the retained third-party license.
+
+### Home Key ECP
+
+Version 0.0.5 requires the matching Home Key ECP relay firmware. Go supplies the
+public Home group ID over BLE on reconnect and after provisioning changes; the
+ESP32 emits ECP before activating a card so iOS can route NFC to Home Key. The
+phone's Express Mode setting governs whether user approval is required.
+See [NFC deployment instructions](ha-app/NFC.md).
diff --git a/build.env b/build.env
index d3cd6ca..4c09e9b 100644
--- a/build.env
+++ b/build.env
@@ -1,4 +1,4 @@
IMAGE_NAME=ghcr.io/tgckpg/homekey-go
-IMAGE_TAG=0.0.4
+IMAGE_TAG=0.0.5
APT_PROXY=
diff --git a/cmd/homekey/main.go b/cmd/homekey/main.go
index cca5263..adbb438 100644
--- a/cmd/homekey/main.go
+++ b/cmd/homekey/main.go
@@ -180,7 +180,7 @@ func run() error {
success = true
log.Printf("Home Key authenticated reader=%s session=%d endpoint=%s; virtual lock unlocked", *bleReader, card.Session, result.EndpointID)
return nil
- })
+ }, store.ReaderGroupIdentifier)
}()
}
err = dev.Server.ListenAndServe(ctx)
diff --git a/ha-app/CHANGELOG.md b/ha-app/CHANGELOG.md
index 568095b..2b4d13c 100644
--- a/ha-app/CHANGELOG.md
+++ b/ha-app/CHANGELOG.md
@@ -1,3 +1,10 @@
+## 0.0.5
+
+- Supply the provisioned Home group identifier to matching ESP32 firmware over BLE.
+- Refresh ECP configuration after provisioning changes and every reconnect.
+- Enable Home Key ECP polling before NFC activation, including Express Mode routing.
+- Requires matching homekey-relay ECP firmware; no new app configuration.
+
## 0.0.4
DEBUG added logs
diff --git a/ha-app/NFC.md b/ha-app/NFC.md
index 4c5f868..fc85b0d 100644
--- a/ha-app/NFC.md
+++ b/ha-app/NFC.md
@@ -20,7 +20,7 @@ physical actuator integration in this release.
existing initialization order. NFC runs in its own task with an 8192-byte
stack; there is no need to edit an existing sdkconfig's main-task stack.
-2. Build and publish your usual `homekey-go` image tagged `0.0.4`, then stage
+2. Build and publish your usual `homekey-go` image tagged `0.0.5`, then stage
the HA app metadata using the existing repository workflow. Update the
installed app and restart it. Keep its existing persistent `/data/state`.
@@ -34,8 +34,9 @@ physical actuator integration in this release.
The HA app still needs host D-Bus and host networking. No new port is added.
The HACS integration is not involved in this direct Go-to-ESP32 connection.
-4. For the first test, open the already-provisioned Home Key in Apple Wallet
- and hold the phone at the reader until the exchange finishes. Remove it
+4. Hold the phone at the reader with its existing Home Key installed. ECP
+ should select the key; Express Mode can authenticate without opening Wallet.
+ If Express Mode is disabled, approve the presented Home Key. Remove it
between attempts. Set the virtual lock to locked first if you want to see
the successful transition in Home.
@@ -43,6 +44,7 @@ Expected Go milestones:
```text
BLE gateway: connected to ...; APDU write_size=...
+BLE gateway: Home Key ECP configured reader=... enabled=true
NFC card reader=... session=... uid_len=... sak=0x20
Home Key SELECT accepted; protocol=2.0
Home Key AUTH0 key exchange complete
@@ -72,9 +74,14 @@ or its provisioned keys.
- PING/PONG remains available; its polling pauses during authentication.
- No credential payloads, private/session keys or APDU contents in normal logs.
-FAST authentication, ECP express-mode wakeup and NFC attestation for previously
-unknown/shared devices are not implemented yet. Use Wallet explicitly for this
-release. The stored persistent key is preparation for FAST; this release still
+Home Key ECP routing is implemented in the matching firmware. It requires both
+updates: Go supplies the public Home group ID, and the ESP32 broadcasts it before
+card activation. No private key is sent over BLE. Firmware waits with RF off
+until configured and clears the configuration on reconnect. Go updates the
+group when provisioning changes. ECP itself does not authenticate a device.
+
+FAST authentication and NFC attestation for previously unknown/shared devices
+are not implemented yet. The stored persistent key is preparation for FAST; this release still
performs STANDARD on every tap.
The NFC session expires after 15 seconds. Go allows 12 seconds for authentication,
diff --git a/ha-app/config.yaml b/ha-app/config.yaml
index 5dbce2e..6092ba0 100644
--- a/ha-app/config.yaml
+++ b/ha-app/config.yaml
@@ -1,5 +1,5 @@
name: Home Key Go
-version: "0.0.4"
+version: "0.0.5"
slug: homekey_go
description: HomeKit virtual lock with Apple Home Key NFC authentication
url: https://sgit.k8s.astropenguin.net/penguin/homekey-go/
diff --git a/internal/blegateway/gateway.go b/internal/blegateway/gateway.go
index 0b9de0b..e15a222 100644
--- a/internal/blegateway/gateway.go
+++ b/internal/blegateway/gateway.go
@@ -60,9 +60,9 @@ func text(p map[string]dbus.Variant, k string) string { v, _ := p[k].Value().(st
// Run reconnects independently of HAP. A Bluetooth failure does not stop
// HomeKit provisioning. Each attempt gets a fresh system-bus connection.
-func Run(ctx context.Context, address, adapter string, interval time.Duration, handler Handler) {
+func Run(ctx context.Context, address, adapter string, interval time.Duration, handler Handler, groupProvider func() []byte) {
for ctx.Err() == nil {
- if err := session(ctx, strings.ToUpper(address), adapter, interval, handler); err != nil && ctx.Err() == nil {
+ if err := session(ctx, strings.ToUpper(address), adapter, interval, handler, groupProvider); err != nil && ctx.Err() == nil {
log.Printf("BLE gateway: %v; retrying in 5s", err)
}
if !wait(ctx, 5*time.Second) {
@@ -70,7 +70,7 @@ func Run(ctx context.Context, address, adapter string, interval time.Duration, h
}
}
}
-func session(ctx context.Context, address, adapter string, interval time.Duration, handler Handler) error {
+func session(ctx context.Context, address, adapter string, interval time.Duration, handler Handler, groupProvider func() []byte) error {
c, err := dbus.ConnectSystemBus()
if err != nil {
return fmt.Errorf("system D-Bus: %w", err)
@@ -195,6 +195,8 @@ func session(ctx context.Context, address, adapter string, interval time.Duratio
ch := c.Object("org.bluez", characteristic)
status := c.Object("org.bluez", statusPath)
apdu := c.Object("org.bluez", apduPath)
+ var configuredGroup []byte
+ configured := false
var lastSession uint32
seq := uint32(0)
nextPing := time.Time{}
@@ -203,6 +205,26 @@ func session(ctx context.Context, address, adapter string, interval time.Duratio
if e != nil {
return fmt.Errorf("NFC status: %w", e)
}
+ // Configure only between sessions; the NFC owner takes a snapshot for
+ // each scan. Every new BLE connection gets a fresh configuration.
+ if card.Session == 0 && groupProvider != nil {
+ group := groupProvider()
+ if !configured || !bytes.Equal(group, configuredGroup) {
+ b, err := ecpConfigPacket(group)
+ if err != nil {
+ return err
+ }
+ configCtx, done := context.WithTimeout(ctx, 3*time.Second)
+ err = (&Relay{characteristic: apdu}).write(configCtx, b)
+ done()
+ if err != nil {
+ return fmt.Errorf("ECP configuration (flash matching firmware): %w", err)
+ }
+ configuredGroup = append([]byte(nil), group...)
+ configured = true
+ log.Printf("BLE gateway: Home Key ECP configured reader=%s enabled=%t", address, len(group) == 8)
+ }
+ }
if card.Session != 0 && card.Session != lastSession {
lastSession = card.Session
relay := &Relay{characteristic: apdu, session: card.Session, writeSize: writeSize}
diff --git a/internal/blegateway/relay.go b/internal/blegateway/relay.go
index 45de6ce..376143b 100644
--- a/internal/blegateway/relay.go
+++ b/internal/blegateway/relay.go
@@ -146,3 +146,10 @@ func readCard(ctx context.Context, ch dbus.BusObject) (Card, error) {
}
return parseCard(b)
}
+
+func ecpConfigPacket(group []byte) ([]byte, error) {
+ if len(group) != 0 && len(group) != 8 {
+ return nil, fmt.Errorf("ECP group identifier must be 8 bytes")
+ }
+ return packet(3, 0, 0, 0, len(group), group), nil
+}
diff --git a/internal/blegateway/relay_test.go b/internal/blegateway/relay_test.go
index 6fe309f..142f645 100644
--- a/internal/blegateway/relay_test.go
+++ b/internal/blegateway/relay_test.go
@@ -42,3 +42,28 @@ func TestCardSession(t *testing.T) {
t.Fatal("bad UID accepted")
}
}
+
+func TestECPConfiguration(t *testing.T) {
+ group := []byte{1, 2, 3, 4, 5, 6, 7, 8}
+ b, err := ecpConfigPacket(group)
+ if err != nil || len(b) != 20 || b[1] != 3 || binary.BigEndian.Uint16(b[10:]) != 8 {
+ t.Fatal("bad ECP packet")
+ }
+ for _, v := range b[2:10] {
+ if v != 0 {
+ t.Fatal("configuration must be session-independent")
+ }
+ }
+ for i, v := range group {
+ if b[12+i] != v {
+ t.Fatal("wrong group")
+ }
+ }
+ b, err = ecpConfigPacket(nil)
+ if err != nil || len(b) != 12 {
+ t.Fatal("bad disable packet")
+ }
+ if _, err = ecpConfigPacket(group[:7]); err == nil {
+ t.Fatal("accepted invalid group")
+ }
+}
diff --git a/internal/provision/auth.go b/internal/provision/auth.go
index e9c0304..67a2cfb 100644
--- a/internal/provision/auth.go
+++ b/internal/provision/auth.go
@@ -54,3 +54,17 @@ func (s *Store) WithAuthorization(expected Credentials, iid, eid, pub string, fn
}
return fn()
}
+
+// ReaderGroupIdentifier is public ECP routing metadata derived from the
+// provisioned reader key, matching the first half of the NFC reader ID.
+// A nil result disables ECP while the Home has no provisioned reader key.
+func (s *Store) ReaderGroupIdentifier() []byte {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ scalar, err := hex.DecodeString(s.data.HomeKey.ReaderPrivateKey)
+ if err != nil || len(scalar) != 32 {
+ return nil
+ }
+ group, _ := hex.DecodeString(keyID(scalar))
+ return group
+}
diff --git a/internal/provision/auth_test.go b/internal/provision/auth_test.go
index fb16290..e99843f 100644
--- a/internal/provision/auth_test.go
+++ b/internal/provision/auth_test.go
@@ -2,6 +2,7 @@ package provision
import (
"bytes"
+ "encoding/hex"
"testing"
)
@@ -83,3 +84,29 @@ func TestAuthenticationRejectsReaderChangeAndInactive(t *testing.T) {
})
}
}
+
+func TestReaderGroupIdentifier(t *testing.T) {
+ s, _, _ := paired(t)
+ if s.ReaderGroupIdentifier() != nil {
+ t.Fatal("unprovisioned reader has ECP group")
+ }
+ if _, err := s.Handle(readerAdd()); err != nil {
+ t.Fatal(err)
+ }
+ // Independent SHA256("key-identifier" || scalar=1) vector.
+ want := mustHex("539a3f91ed603fef")
+ group := s.ReaderGroupIdentifier()
+ if !bytes.Equal(group, want) {
+ t.Fatalf("wrong group: %s", hex.EncodeToString(group))
+ }
+ group[0] ^= 1
+ if !bytes.Equal(s.ReaderGroupIdentifier(), want) {
+ t.Fatal("group snapshot aliases store")
+ }
+ if _, err := s.Handle(request(3, 6, TLV(4, want))); err != nil {
+ t.Fatal(err)
+ }
+ if s.ReaderGroupIdentifier() != nil {
+ t.Fatal("removed reader still has ECP group")
+ }
+}