Skip to content

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 atHostAttached, 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:

  1. Import IPHONE_REPLAY_FRAMES[8] from the committed fixture module; extract the 14124-byte payload.
  2. 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.
  3. For both chunks: decode XpcWrapper → XpcPayload → plist tree → Python dict (same mechanics as iter-11's decoder).
  4. 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 ASCII str) report content verbatim — safe because these are device-capability constants, not identity.
  5. 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 :49151 advertises the device by UDID (see the backend's tunneld discovery code and pymobiledevice3's RemoteServiceDiscoveryService).
  • 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.

  1. Patch UniqueDeviceID with the UDID that tunneld advertises on :49151 for 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.

  2. Patch SerialNumber with 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.

  3. Patch UniqueChipID with the ECID from tunneld's device record. Env gate: IOSMUX_HANDSHAKE_ECID=<value>. Only needed if 1 + 2 didn't suffice.

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

  1. 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.
  2. The top-level MessageType / MessagingProtocolVersion / UUID siblings of Properties. All three were compared: MessageType 'Handshake' and MPV 1 match 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 via IOSMUX_HANDSHAKE_UUID=random per-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).
  3. 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:

/home/op/venvs/iosmux-research/bin/python3 /tmp/iosmux-d13-hs-props-diff.py

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 document
  • index.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.