Time synchronization#
Firmware-level notes on one subsystem: how the reader obtains wall-clock time, how that interacts with Aliro's time-based credential checks, and the fixes this repo carries. This is the implementation-and-debugging companion to protocol-research.md, which documents the on-air protocol itself (discovery, authentication, the M1-M4 ranging setup, the STS ladder, and hopping).
File references use paths inside the fetched workspace (workspace/); upstream line numbers are as of the pins in west.yml / bootstrap.sh.
Time synchronization and credential validity#
How the reader gets wall-clock time#
- The wall clock on the Zephyr CHIP port is RAM-only:
SetClock_RealTime()stores an offset against the monotonic clock (gBootRealTime,modules/lib/matter/src/platform/Zephyr/SystemTimeSupport.cpp). Any reset, warm or cold, erases it. - The only things that set it are the Matter Time Synchronization cluster's
SetUTCTimecommand and a configured trusted time source. Apple Home sendsSetUTCTimeexactly once, at commissioning (observed in a re-pair capture, 2026-07-17). It configures noTrustedTimeSourceand noDefaultNTP, so the clock is never set again. The device emitsTimeFailureat every boot. - Matter's Last Known Good Time (LKGT) is persisted in the fabric table, but it is only written at NOC install (certificate notBefore) and inside the cluster's
UpdateUTCTime(src/app/clusters/time-synchronization-server/time-synchronization-cluster.cpp:132). Nothing refreshes it periodically, so it stays frozen at the commissioning timestamp, which lands roughly 30 s before the first Access Document is minted. - The Nordic add-on's
GetCurrentTime()(subsys/aliro/time_utils/src/time_utils_chip.cpp) returns the wall clock when valid and otherwise silently substitutes LKGT, converting "time unknown" into a confidently wrong answer.GetCurrentUnixTime()has no fallback and simply fails.
Access Document validity#
Access Documents are minted on demand with validFrom set to the phone's current time and validUntil effectively unbounded (year 4001). The check in VerifyValidityPeriod (applications/matter-aliro-door-lock-app/src/aliro/interface_impl/access_document.cpp), which runs after the document signature has been verified, therefore degenerates to: the reader clock must not be behind the phone clock.
Failure chain after any reboot, without the fixes below:
- RAM clock is gone;
GetCurrentTime()falls back to LKGT. - LKGT predates every document ever minted.
- Every fresh document is rejected as not-yet-valid (
Current time is outside the Access Document validity period). - Step-up fails closed, permanently, until the lock is re-commissioned.
Fixes carried by this repo (in our copy of the door-lock application)#
- Document time ratchet (
CONFIG_DOOR_LOCK_TIME_CONCEPT_RATCHET, default y): when a signature-verified document'svalidFromlies ahead of the reader clock, ratchet the system clock forward tovalidFrom(never backward) instead of failing. Heals from issuer-signed material with no infrastructure. Trade-off: an issuer that signs a future-dated document can advance the clock early, weakening not-before enforcement. - Wall-clock persistence (
CONFIG_DOOR_LOCK_TIME_CONCEPT_PERSIST, default y): a 60 s poll persists the wall clock through the app'sKeyValueStorage(settings keydl/UT) once per hour, immediately when the clock first becomes valid, and after any backward correction; atAliroInitthe system clock is seeded from the stored value if (and only if) it is invalid. Restored time understates real time by at most one hour plus the power-off duration. It deliberately does not refresh Matter's fabric LKGT: LKGT feeds CASE certificate-validity checking, and a ratchet-poisoned time must not be persisted into that path. - Interplay: persist alone fixes the common case (reboot after long uptime); the ratchet alone covers documents minted while the lock was powered off. Each covers exactly the other's blind spot. Because of persist, a ratchet advance now survives reboot; this is documented in the ratchet Kconfig help.
Interaction: the BLE advertisement's Dynamic Tag (found on bench, 2026-07-17)#
The Aliro BLE advertisement embeds an expiry timestamp of lock unix time + CONFIG_DOOR_LOCK_ULTRAWIDELOCK_BLE_SERVICE_DYNAMIC_TAG_EXPIRY_DURATION_S (default 900 s) when the wall clock is valid (ultrawidelock_service.cpp, PrepareAdvertisingDataLocked), and "expiry unavailable" when it is not. iPhones accept the unavailable form but silently ignore an advertisement whose expiry lies in their past. A clock that is valid but behind real time by more than the window therefore kills unlock-on-approach for every phone, while NFC and Matter keep working. Both of this repo's time fixes create exactly that state (the restored clock understates real time by the accumulated power-off duration; a ratcheted clock understates it by the presented document's age), so apps/nrf5340dk-lock/overlays/ultrawidelock-cred.conf disabled the Dynamic Tag while no real time source existed. With the Matter-timesync seeding merged (the clock steps up on sync) the overlay is back to =y, bench-gated on the seeded clock. Stock firmware never hits this: its clock is either fresh (before the first reboot) or invalid (after), never valid-but-stale. The ESP32 port derives the same tag from its live clock (SNTP-fed; ultrawidelock_ble.c + ultrawidelock_advtag.c), re-derives at half the 900 s window, immediately on a time step, and falls back to the "expiry unavailable" form whenever the clock is not yet valid.
Deferred: network time (SNTP / DefaultNTP)#
Real network time is the only fix that makes expiry and schedule enforcement accurate, but plain SNTP is unauthenticated: it would be the only input to the trust chain controllable by an attacker on the home network or DNS path (backward spoof: unlock denial; forward spoof: weakened not-before without issuer keys). It also depends on the border router offering a routable path (IPv6 NTP or NAT64/DNS64) and must be strictly fail-open for offline homes. If built later: clamp to never-before-firmware-build-time, cap backward steps, run async with backoff, and rank it below SetUTCTime as a source.
Bench validation#
- Commission, wait ~1 min after the clock becomes valid (first persist write), reboot: boot log shows
Wall clock seeded from persisted value, and the first step-up passes with no ratchet warning. - Power off for a while, mint a fresh document (unlock from a second same-account iPhone), power on: first step-up logs
system time ratcheted forwardand unlocks.
The temporary DBG overlay lines in apps/nrf5340dk-lock/overlays/ultrawidelock-cred.conf (DOOR_LOCK_APP_LOG_LEVEL_DBG, DOOR_LOCK_ULTRAWIDELOCK_TIME_UTILS_LOG_LEVEL_DBG) surface the decisive Current time / validFrom / validUntil values during these tests and should be reverted afterwards.