Phase D.6.6-research-a findings — Handshake Properties byte-diff: dict shape identical, 5 version-string drifts, 5 expected redactions, zero other diffs¶
Status: verified — 2026-04-24
Pure local analysis. Decoded the top-level Handshake dict
from both sides — our iter-01 SPIKE fixture (frame #8, the
14124-byte stream-1 DATA) and the real iPhone's iter-10
reference pcap (greeting session #1 on iPhone:52889, same
14124-byte frame). Byte-level compared the 46-key
Properties sub-dict. Result: both dicts have exactly
46 keys, identical insertion order, identical types per
key, and content that falls into three clean buckets —
36 bit-exact same, 5 intentionally-zeroed-by-us
(the fixture's pre-declared identity redactions:
UniqueDeviceID, SerialNumber, EthernetMacAddress,
UniqueChipID, BootSessionUUID), and 5 iOS
version-metadata drifts (the test iPhone received a
minor point update between iter-01 corpus capture and
iter-10 reference capture: 26.4.1 / build 23E254 →
26.4.2 / build 23E261). Zero only-ours or only-real
keys. The trust gate is not a separate trust/pair-state
field inside Properties — it is one of the 5 zeroed
identity fields, or it lives outside Properties entirely.
TL;DR¶
D.6.6-research-a scope completed. The Handshake Properties
dict is structurally a byte-clone between fixture and real
device. The iter-12 hypothesis H-HS ("some non-identity
Properties field carries the trust gate") is refuted for
the candidates iter-12 guessed at — HostAttached,
TrustedHostAttached, PairRecords, ActivationState, and
PasswordProtected are not in the Properties dict at
all. Those fields live in the 88-key GetValue response
from the classic-lockdown service on :50367, which CDS
never reaches in SPIKE mode (iter-12 falsification).
H-HS reduces to a very small attack surface: the 5 identity fields we zeroed (UDID / Serial / MAC / ChipID / BootSessionUUID). None of the 5 version-drift fields are plausible trust gates — a month-old OS is still a real iPhone. D.6.6-research-b becomes a 5-slot field-bisection instead of the open-ended search implied by iter-12.
What changed vs iter-12's causal model¶
iter-12 said: "46-entry Properties dict contains fields
like HostAttached, TrustedHostAttached, PairRecords,
ActivationState, etc. One of these may carry the trust
signal."
iter-13 says: none of those named fields exist in Properties. The 46 keys are:
AppleInternal, BoardId, BootSessionUUID, BuildVersion,
CPUArchitecture, CertificateProductionStatus,
CertificateSecurityMode, ChipID, DeviceClass, DeviceColor,
DeviceEnclosureColor, DeviceSupportsLockdown,
EffectiveProductionStatusAp, EffectiveProductionStatusSEP,
EffectiveSecurityModeAp, EffectiveSecurityModeSEP,
EthernetMacAddress, HWModel,
HWModelDescriptionForUserVisibility, HardwarePlatform,
HasSEP, HumanReadableProductVersionString,
Image4CryptoHashMethod, Image4Supported, IsUIBuild,
IsVirtualDevice, MobileDeviceMinimumVersion, ModelNumber,
OSInstallEnvironment, OSVersion, ProductName, ProductType,
ProductTypeDescForUserVisibility, RegionCode, RegionInfo,
RemoteXPCVersionFlags, RestoreLongVersion, SecurityDomain,
SensitivePropertiesVisible, SerialNumber, SigningFuse,
StoreDemoMode, SupplementalBuildVersion, ThinningProductType,
UniqueChipID, UniqueDeviceID
This is pure device-identity + hardware/firmware-capability metadata. It describes what the device IS, not who has paired with it. Pair state and host identity are served elsewhere (the classic-lockdown GetValue response on :50367, iter-11 section "The 3-verb protocol").
So H-HS, if it holds, must bite on what identity the device reports, not on a separate trust flag inside Properties.
Method¶
Script: /tmp/iosmux-d13-hs-props-diff.py (held local; reads
the held-local pcap). Mechanically:
- Import
IPHONE_REPLAY_FRAMES[8]from the committed fixture module; extract the 14124-byte payload. - Open the held-local iter-10 reference pcap with scapy; reassemble all iPhone→client flows sourced from :52889; walk each as HTTP/2; locate the 14124-byte stream-1 DATA frame in the greeting session.
- For both chunks: decode XpcWrapper → XpcPayload → plist
tree → Python
dict(same mechanics as iter-11's decoder). - Descend into
Properties. For each key in the union of the two sides, classify the value using a descriptor tuple(type_name, size_or_none, content_or_hash). Privacy-preserving:bytes, long strings, and UUID values are reported as SHA-256 prefix + length, never raw bytes. Scalars (bool,int, short ASCIIstr) report content verbatim — safe because these are device-capability constants, not identity. - Categorize each key as one of:
same,zeroed-by-us(key name in the pre-declared redaction list per the fixture's module docstring),differs,only-ours,only-real.
Privacy policy: the script prints sizes/types/hashes for redacted fields and never echoes raw identity bytes. The real pcap stays local; no identity values enter the repo.
Results¶
Our Properties: 46 keys
Real Properties: 46 keys
Union key count: 46
Insertion order identical: True
Category counts:
same: 36
zeroed-by-us: 5
differs: 5
only-ours: 0
only-real: 0
The 36 same keys (compact)¶
All device-capability / hardware-identity constants that
are identical between the two captures. Shown as
key: type content:
AppleInternal: bool False
BoardId: int 16
CPUArchitecture: str 'arm64e'
CertificateProductionStatus: bool True
CertificateSecurityMode: bool True
ChipID: int 33040
DeviceClass: str 'iPhone'
DeviceColor: str '1'
DeviceEnclosureColor: str '1'
DeviceSupportsLockdown: bool True
EffectiveProductionStatusAp: bool True
EffectiveProductionStatusSEP: bool True
EffectiveSecurityModeAp: bool True
EffectiveSecurityModeSEP: bool True
HWModel: str 'D49AP'
HWModelDescriptionForUserVisibility: str 'D49AP'
HardwarePlatform: str 't8110'
HasSEP: bool True
Image4CryptoHashMethod: str 'sha2-384'
Image4Supported: bool True
IsUIBuild: bool True
IsVirtualDevice: bool False
MobileDeviceMinimumVersion: str '1827.100.14'
ModelNumber: str 'MMX83'
OSInstallEnvironment: bool False
ProductName: str 'iPhone OS'
ProductType: str 'iPhone14,6'
ProductTypeDescForUserVisibility: str 'iPhone14,6'
RegionCode: str 'LL'
RegionInfo: str 'LL/A'
RemoteXPCVersionFlags: int 72057594037927942
SecurityDomain: int 1
SensitivePropertiesVisible: bool True
SigningFuse: bool True
StoreDemoMode: bool False
ThinningProductType: str 'iPhone14,6'
(All of the above values are present in the already-committed iter-01 fixture via normal decode, so echoing them here adds no privacy exposure beyond what is already in the repo.)
These are correct. Our fixture already reports a real iOS 17+ iPhone's production/security/SEP flags. Not the gate.
The 5 zeroed-by-us keys (expected redactions)¶
These are the identity fields the fixture's module docstring
explicitly declares zeroed in-place (see iphone_replay_bytes.py
REDACTED_AT_SOURCE). Types and lengths match; content is
all-zero on our side and non-zero on the real side.
| Key | Type | Size | Our value | Real value |
|---|---|---|---|---|
UniqueDeviceID |
str | 25 | 25-byte all-zero ASCII | 25-byte ASCII UDID (redacted) |
SerialNumber |
str | 10 | 10-byte all-zero ASCII | 10-byte ASCII serial (redacted) |
EthernetMacAddress |
str | 17 | 17-byte all-zero ASCII | 17-byte ASCII MAC (redacted) |
UniqueChipID |
uint64 | 8 | 0 |
non-zero uint64 (redacted) |
BootSessionUUID |
UUID | 16 | all-zero 16-byte UUID | random 16-byte UUID (redacted) |
These are the remaining candidate trust-gate fields — not separate trust flags, but the identity fields themselves. CDS may be checking "is UDID non-empty?" or "does Handshake UDID match tunneld-advertised UDID?" or similar.
The 5 differs keys (version drift, not gate candidates)¶
All 5 are iOS point-version metadata. The test iPhone received a minor update between iter-01 corpus capture and iter-10 reference capture.
| Key | Our | Real | Comment |
|---|---|---|---|
BuildVersion |
23E254 |
23E261 |
iOS 26.4.x minor-patch build ID |
SupplementalBuildVersion |
23E254 |
23E261 |
same drift |
OSVersion |
26.4.1 |
26.4.2 |
point release |
HumanReadableProductVersionString |
26.4.1 |
26.4.2 |
UI-facing point release |
RestoreLongVersion |
23.5.254.0.0,0 |
23.5.261.0.0,0 |
DFU-restore long version |
None of these are plausible trust gates. CDS does not refuse a month-old iOS as "untrusted"; that would break upgrade flows. Included in the diff only for completeness.
only-ours / only-real: zero¶
The 46 keys are exactly the same on both sides. No field was added or removed between capture dates.
Candidate trust-gate shortlist (ranked)¶
All candidates come from the zeroed-by-us bucket — that's
the only remaining H-HS attack surface.
| Rank | Field | Type / size | Why it's a likely gate |
|---|---|---|---|
| 1 | UniqueDeviceID |
25-byte ASCII | UDID is THE canonical device identity everywhere in the Apple mobile stack. Every pair record, trust record, tunneld advertisement, and CoreDevice internal state is keyed by UDID. If CDS cross-references "UDID in Handshake" against "UDID tunneld advertised on :49151" (or against its own paired-devices plist), an all-zero UDID breaks every check at once. Highest prior. |
| 2 | SerialNumber |
10-byte ASCII | Secondary identity; shown in devicectl output; likely cross-checked against ioreg cache on host. An all-zero serial is implausibly matching no known device in the host's history. |
| 3 | UniqueChipID (ECID) |
uint64 | Cryptographic identity for pairing (binds pair-record certificates). An all-zero ECID fails any "does this ECID have a pair-record?" lookup. |
| 4 | BootSessionUUID |
16-byte UUID | Per-boot freshness. CDS or devicectl may require a non-nil UUID to bind a session cookie — less likely than UDID/Serial/ECID to be the gate because it doesn't tie to pairing state, but still possible as a "is this device actually booted?" sanity check. |
| 5 | EthernetMacAddress |
17-byte ASCII | Lowest prior. iPhones don't expose MAC over host transports in any usual flow. An all-zero MAC might even be the expected value on a pure-USB session. Listed last. |
Why UDID is #1 by a wide margin¶
- Tunneld's HTTP API on
:49151advertises the device by UDID (see the backend's tunneld discovery code and pymobiledevice3'sRemoteServiceDiscoveryService). - CoreDevice's paired-devices database is keyed by UDID.
devicectl device info details --device <uuid>takes a UDID-shaped argument.- The Handshake Properties' UDID is the authoritative "who you are" field inside the RSD greeting. A mismatch between "UDID CDS requested" and "UDID Handshake returned" is exactly the kind of single-point check that produces Mercury 1000 errors.
Rank 1 is the natural place to start D.6.6-research-b.
Proposed D.6.6-research-b patch order¶
Plan: env-gated per-session byte patching of the fixture at
emit time, one field at a time. Same mechanism as D.6.1's
UUID patching (see IOSMUX_HANDSHAKE_UUID=random in backend
code). Each patch round re-runs the iter-12 wide-pcap trigger
(devicectl device info details) and looks for a SYN to
[::1]:50367.
-
Patch
UniqueDeviceIDwith the UDID that tunneld advertises on:49151for the connected device. The Go backend already knows this UDID (it queried tunneld to build the Handshake context). Env gate:IOSMUX_HANDSHAKE_UDID=from-tunneld(read at emit time). Expected outcome if H1 holds: SYN to :50367 appears; we observe CDS attempting the lockdown back-connect. -
Patch
SerialNumberwith a realistic 10-char serial (if tunneld advertises one — it typically does) or a known valid serial prefix. Env gate:IOSMUX_HANDSHAKE_SERIAL=<value>. Only run this if step 1 alone didn't unblock — most likely UDID alone will satisfy the gate, but CDS may require serial + UDID consistency. -
Patch
UniqueChipIDwith the ECID from tunneld's device record. Env gate:IOSMUX_HANDSHAKE_ECID=<value>. Only needed if 1 + 2 didn't suffice. -
BootSessionUUID and EthernetMacAddress: patch only if 1-3 failed. Lower priority.
Stopping rule for research-b: the first patch that
produces a SYN to [::1]:50367 (or any advertised lockdown
port) in the wide-pcap is the gate. Stop there, document,
move to D.6.6-impl (the original lockdown handler plan from
iter-11).
Hypothesis status update¶
- H-HS: narrowed. The original iter-12 wording implied
HostAttached/PairRecords/ etc. — refuted (those keys are not in Properties). Narrowed form: "one of the 5 zeroed identity fields in Properties is the trust gate, most likely UDID." Testable with D.6.6-research-b. - H-Adv (tunneld advertisement on :49151 is the gate): still untested; falls back to this if H-HS narrowed form fails.
- H-Tx (transport-level signal): still parked as low-likelihood.
What this iter did not inspect¶
Three places a gate could still hide that this diff didn't inspect:
- The Services sub-dict (62 entries, not compared in iter-13). iter-11 did a partial decode — enough to identify :50367 — but didn't byte-diff the full dict. If CDS validates port ranges, entitlement strings, or RemoteXPC flags, a difference in Services could gate before Properties is read. iter-13 assumed "same by construction" based on the Services list being advertised identically across iter-01 and iter-10 capture sessions, but that's a two-captures observation, not a fixture-vs-real observation. Checking this is cheap (extend the iter-13 decoder to descend into Services and diff key-by-key) and should be added as a research-a.2 if research-b with UDID-only patch fails.
- The top-level
MessageType/MessagingProtocolVersion/UUIDsiblings ofProperties. All three were compared: MessageType'Handshake'and MPV1match on both sides (verified via top-level-keys equality). Top-level UUID is all-zero in ours, random in real — but D.6.1 already addressed this viaIOSMUX_HANDSHAKE_UUID=randomper-session patching, currently on in the backend. Confirmed not the single-point gate (D.6.1-B result: patching UUID eliminated the 30s timeout but didn't progress CDS to the lockdown back-connect phase). - Fields outside the Handshake entirely — H-Adv (tunneld response content) is the clearest example. If H-HS narrowed form turns out to be wrong for all 5 candidates, H-Adv is the next frontier.
Reproduce¶
Script: /tmp/iosmux-d13-hs-props-diff.py (local only, not
committed — reads the held-local pcap). Invocation:
Supporting checks (also local):
/home/op/venvs/iosmux-research/bin/python3 /tmp/iosmux-d13-uuid-check.py
/home/op/venvs/iosmux-research/bin/python3 /tmp/iosmux-d13-keyorder.py
Inputs (paths):
- Fixture:
docs/research/protocol/s2c-self-experiment/iter-01/iphone_replay_bytes.py - Pcap:
/home/op/backups/iosmux/pcaps/iosmux-d10-real.pcap(held local, never committed)
Artifacts¶
Under iter-13-hs-props-diff/:
findings.md— this documentindex.md— summary
Local only (not committed, read the held-local pcap):
/tmp/iosmux-d13-hs-props-diff.py— the decode + diff script/tmp/iosmux-d13-uuid-check.py— UUID field sanity check/tmp/iosmux-d13-keyorder.py— insertion-order comparison
Status¶
- Step A (decode both sides + union-compare Properties): delivered
- Step B (categorize + privacy-safe report): delivered
- Step C (ranked shortlist + research-b patch plan): delivered
iter-13 delivers the research-a input needed for D.6.6-research-b. Next step: env-gated UDID patch in the Go backend Handshake emitter, re-run iter-12 wide-pcap trigger, observe SYN table.