wip(k1): checkpoint connection recovery rewrite

Capture the current unreleased K1 connection, recovery, lifecycle, viewer, and test work as a single known-bad baseline for subsequent fixes.
This commit is contained in:
DCCONSTRUCTIONS
2026-08-14 14:57:50 +03:00
parent aff331082f
commit 0ca7316a24
157 changed files with 152962 additions and 4036 deletions
+8
View File
@@ -1,10 +1,14 @@
"""Host-owned network adapters used by device plugins."""
from k1link.host_network.wifi import (
HostWifiAssociationIdentityProbe,
HostWifiAssociationIdentityResult,
HostWifiCredentialMaterialAvailabilityResult,
HostWifiCredentialMaterialStoreResult,
HostWifiProfileError,
associate_with_ephemeral_wifi_credentials_once,
associate_with_wifi_profile_once,
bind_route_fingerprint_to_wifi_association,
check_wifi_credential_material,
check_wifi_profile,
ensure_wifi_profile_from_credential_source,
@@ -12,10 +16,14 @@ from k1link.host_network.wifi import (
)
__all__ = [
"HostWifiAssociationIdentityProbe",
"HostWifiAssociationIdentityResult",
"HostWifiCredentialMaterialAvailabilityResult",
"HostWifiCredentialMaterialStoreResult",
"HostWifiProfileError",
"associate_with_ephemeral_wifi_credentials_once",
"associate_with_wifi_profile_once",
"bind_route_fingerprint_to_wifi_association",
"check_wifi_credential_material",
"check_wifi_profile",
"ensure_wifi_profile_from_credential_source",
+564 -236
View File
@@ -1,15 +1,17 @@
from __future__ import annotations
import hashlib
import hmac
import json
import os
import stat
import secrets
import signal
import subprocess
import sys
import tempfile
import threading
import time
from collections.abc import Callable, Iterator
from contextlib import contextmanager, suppress
from collections.abc import Callable
from contextlib import suppress
from pathlib import Path
from typing import Any, Literal, TypedDict
@@ -35,6 +37,7 @@ class HostWifiProfileAvailabilityResult(TypedDict):
schema_version: int
adapter: str
available: bool
credential_source: str | None
class HostWifiProfileEnsureResult(TypedDict):
@@ -57,6 +60,24 @@ class HostWifiCredentialMaterialAvailabilityResult(TypedDict):
available: bool
class HostWifiAssociationIdentityResult(TypedDict):
"""Secret-free continuity evidence for one host network interface.
``continuity_token`` is scoped to one probe instance and is safe to fold
into another local route fingerprint. It is not an SSID/BSSID digest that
can be compared between Mission Core processes.
"""
schema_version: int
adapter: str
wifi_interface: bool | None
association_state: Literal["associated", "not-wifi", "unavailable"]
evidence_quality: Literal["ssid+bssid", "bssid-only", "not-wifi", "unavailable"]
continuity_proven: bool
continuity_token: str
reason_code: str | None
_ERROR_MESSAGES = {
"unsupported-platform": (
"для этой ОС ещё не установлен адаптер системных Wi-Fi-профилей"
@@ -68,22 +89,28 @@ _ERROR_MESSAGES = {
"credential provider точной версии прошивки не установлен на этом устройстве"
),
"network-not-found": (
"точка доступа выбранного K1 не найдена; проверьте режим K1 и питание"
"K1 подтвердил режим точки доступа, но macOS не увидела её Wi-Fi-сеть "
"за отведённое время"
),
"profile-ssid-mismatch": "сохранённый профиль принадлежит другой точке доступа",
"credential-entry-cancelled": "ввод пароля точки доступа K1 отменён оператором",
"credential-invalid": "пароль точки доступа K1 имеет недопустимую длину",
"profile-credential-source-mismatch": (
"сохранённый профиль K1 не связан с точной версией firmware provider"
),
"host-wifi-operation-timeout": (
"оператор не завершил системное подключение Wi-Fi за отведённое время"
),
"host-wifi-helper-build-timeout": (
"локальный Wi-Fi helper не успел скомпилироваться за отведённое время"
),
"host-wifi-helper-build-failed": "локальный Wi-Fi helper не удалось скомпилировать",
"host-wifi-helper-compiler-unavailable": "компилятор локального Wi-Fi helper недоступен",
"host-wifi-helper-cache-unavailable": "кэш локального Wi-Fi helper недоступен",
"host-wifi-helper-missing": "исходный файл локального Wi-Fi helper не найден",
"host-wifi-helper-unavailable": "локальный Wi-Fi helper недоступен",
"keychain-authorization-required": (
"локальный профиль K1 требует отдельного разрешения связки ключей"
),
"keychain-authorization-denied": "доступ к локальному профилю K1 запрещён",
"keychain-authorization-cancelled": "разрешение связки ключей отменено оператором",
"keychain-access-failed": "локальный профиль K1 недоступен в связке ключей",
"corewlan-authorization-denied": (
"macOS не разрешила локальному сервису выполнять системную Wi-Fi-операцию"
),
"wifi-interface-inactive": "системный Wi-Fi-интерфейс выключен или неактивен",
"wifi-interface-unavailable": "системный Wi-Fi-интерфейс недоступен",
"corewlan-error": "системный Wi-Fi не смог подключиться к точке доступа K1",
}
@@ -111,7 +138,11 @@ class HostWifiProfileError(RuntimeError):
RunProcess = Callable[..., subprocess.CompletedProcess[bytes]]
DEFAULT_HELPER_BUILD_TIMEOUT_SECONDS = 120.0
REVIEWED_MACOS_HELPER_COMMAND_PREFIX = ("/usr/bin/xcrun", "swift")
_ASSOCIATION_INSPECTION_PROFILE_ID = "host-association-inspection.v1"
_EPHEMERAL_ASSOCIATION_PROFILE_ID = "bridge-operation-memory.v1"
_ASSOCIATION_IDENTITY_HEX_LENGTH = 64
MAX_ASSOCIATION_OBSERVATION_CACHE_AGE_SECONDS = 0.75
def _validate_profile_id(profile_id: str) -> None:
@@ -124,232 +155,45 @@ def _validate_profile_id(profile_id: str) -> None:
raise ValueError("host Wi-Fi profile id contains unsupported characters")
def _helper_cache_directory(helper_path: Path) -> Path:
"""Resolve the process-local helper cache without an environment override."""
source = helper_path.expanduser().resolve()
for ancestor in source.parents:
if ancestor.name != "plugins":
continue
try:
relative = source.relative_to(ancestor)
except ValueError: # pragma: no cover - guarded by Path.parents
continue
if relative.parts[:2] == ("xgrids-k1", "macos"):
return ancestor.parent / ".runtime" / "mission-core" / "helpers"
# Tests and separately packaged adapters still get a stable cache beside
# their source tree. The repository layout above is the production path.
return source.parent / ".runtime" / "mission-core" / "helpers"
def _compiled_macos_helper_path(helper_path: Path) -> Path:
source = helper_path.expanduser().resolve()
source_sha256 = hashlib.sha256(source.read_bytes()).hexdigest()
return _helper_cache_directory(source) / f"{source.stem}-{source_sha256}"
def _is_ready_executable(path: Path) -> bool:
try:
metadata = path.lstat()
except OSError:
return False
return (
stat.S_ISREG(metadata.st_mode)
and metadata.st_size > 0
and metadata.st_mode & 0o111 != 0
)
@contextmanager
def _exclusive_helper_build_lock(
path: Path,
*,
timeout_seconds: float,
) -> Iterator[None]:
if timeout_seconds <= 0:
raise ValueError("timeout_seconds must be positive")
try:
import fcntl
except ImportError as exc: # pragma: no cover - production target is macOS
raise HostWifiProfileError("host-wifi-helper-cache-unavailable") from exc
flags = (
os.O_RDWR
| os.O_CREAT
| getattr(os, "O_CLOEXEC", 0)
| getattr(os, "O_NOFOLLOW", 0)
)
try:
descriptor = os.open(path, flags, 0o600)
except OSError as exc:
raise HostWifiProfileError("host-wifi-helper-cache-unavailable") from exc
locked = False
try:
if not stat.S_ISREG(os.fstat(descriptor).st_mode):
raise HostWifiProfileError("host-wifi-helper-cache-unavailable")
lock_started = time.monotonic()
while True:
try:
fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
break
except BlockingIOError as exc:
elapsed_seconds = max(0.0, time.monotonic() - lock_started)
if elapsed_seconds >= timeout_seconds:
raise HostWifiProfileError(
"host-wifi-helper-build-timeout",
helper_stage="compile-lock",
helper_elapsed_ms=int(elapsed_seconds * 1000),
) from exc
time.sleep(min(0.05, timeout_seconds - elapsed_seconds))
except OSError as exc:
raise HostWifiProfileError("host-wifi-helper-cache-unavailable") from exc
locked = True
yield
finally:
if locked:
with suppress(OSError):
fcntl.flock(descriptor, fcntl.LOCK_UN)
os.close(descriptor)
def _fsync_directory(path: Path) -> None:
try:
descriptor = os.open(path, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
except OSError:
return
try:
os.fsync(descriptor)
finally:
os.close(descriptor)
def _ensure_compiled_macos_helper(
helper_path: Path,
*,
build_timeout_seconds: float,
runner: RunProcess,
) -> Path:
"""Build the source-hash-addressed helper once and reuse it thereafter."""
if build_timeout_seconds <= 0:
raise ValueError("build_timeout_seconds must be positive")
source = helper_path.expanduser().resolve()
if not source.is_file():
raise HostWifiProfileError("host-wifi-helper-missing")
try:
executable = _compiled_macos_helper_path(source)
executable.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
except OSError as exc:
raise HostWifiProfileError("host-wifi-helper-cache-unavailable") from exc
if _is_ready_executable(executable):
return executable
build_started_ns = time.monotonic_ns()
build_deadline = time.monotonic() + build_timeout_seconds
def build_error(
reason_code: str,
*,
helper_stage: str = "compile",
) -> HostWifiProfileError:
elapsed_ms = max(0, (time.monotonic_ns() - build_started_ns) // 1_000_000)
return HostWifiProfileError(
reason_code,
helper_stage=helper_stage,
helper_elapsed_ms=elapsed_ms,
)
lock_path = executable.with_name(f".{executable.name}.lock")
with _exclusive_helper_build_lock(
lock_path,
timeout_seconds=max(0.001, build_deadline - time.monotonic()),
):
if _is_ready_executable(executable):
return executable
try:
descriptor, staging_name = tempfile.mkstemp(
prefix=f".{executable.name}.",
suffix=".tmp",
dir=executable.parent,
)
os.close(descriptor)
staging = Path(staging_name)
staging.unlink()
except OSError as exc:
raise HostWifiProfileError("host-wifi-helper-cache-unavailable") from exc
try:
remaining_build_seconds = build_deadline - time.monotonic()
if remaining_build_seconds <= 0:
raise build_error("host-wifi-helper-build-timeout")
try:
completed = runner(
[
"/usr/bin/xcrun",
"swiftc",
str(source),
"-o",
str(staging),
],
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=remaining_build_seconds,
check=False,
)
except subprocess.TimeoutExpired as exc:
raise build_error("host-wifi-helper-build-timeout") from exc
except OSError as exc:
raise build_error("host-wifi-helper-compiler-unavailable") from exc
if completed.returncode != 0 or not _is_ready_executable(staging):
raise build_error("host-wifi-helper-build-failed")
try:
staging.chmod(0o700)
with staging.open("rb") as stream:
os.fsync(stream.fileno())
os.replace(staging, executable)
_fsync_directory(executable.parent)
except OSError as exc:
raise HostWifiProfileError("host-wifi-helper-cache-unavailable") from exc
finally:
with suppress(OSError):
staging.unlink(missing_ok=True)
return executable
def _run_macos_helper(
helper_path: Path,
request: dict[str, object],
*,
timeout_seconds: float,
runner: RunProcess,
build_timeout_seconds: float = DEFAULT_HELPER_BUILD_TIMEOUT_SECONDS,
process_fence_descriptor_factory: Callable[[], int] | None = None,
) -> dict[str, Any]:
if sys.platform != "darwin":
raise HostWifiProfileError("unsupported-platform")
source = helper_path.expanduser().resolve()
if not source.is_file():
raise HostWifiProfileError("host-wifi-helper-missing")
if timeout_seconds <= 0:
raise ValueError("timeout_seconds must be positive")
executable = _ensure_compiled_macos_helper(
helper_path,
build_timeout_seconds=build_timeout_seconds,
runner=runner,
)
request_bytes = bytearray(
json.dumps(request, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
)
try:
completed = runner(
[str(executable)],
input=request_bytes,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=timeout_seconds,
check=False,
command = [*REVIEWED_MACOS_HELPER_COMMAND_PREFIX, str(source)]
completed = (
_run_macos_helper_with_process_fence(
command,
request_bytes=request_bytes,
timeout_seconds=timeout_seconds,
process_fence_descriptor_factory=(
process_fence_descriptor_factory
),
)
if process_fence_descriptor_factory is not None
else runner(
command,
input=request_bytes,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=timeout_seconds,
check=False,
)
)
except subprocess.TimeoutExpired as exc:
raise HostWifiProfileError("host-wifi-operation-timeout") from exc
@@ -389,22 +233,422 @@ def _run_macos_helper(
return response
def _run_macos_helper_with_process_fence(
command: list[str],
*,
request_bytes: bytearray,
timeout_seconds: float,
process_fence_descriptor_factory: Callable[[], int],
) -> subprocess.CompletedProcess[bytes]:
"""Run one mutating helper in a fenced, killable POSIX process group."""
if os.name != "posix":
raise HostWifiProfileError("host-wifi-helper-unavailable")
inherited_descriptor = process_fence_descriptor_factory()
process: subprocess.Popen[bytes] | None = None
try:
process = subprocess.Popen(
command,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
start_new_session=True,
pass_fds=(inherited_descriptor,),
)
finally:
# Popen has either inherited the descriptor into the child or failed.
# The parent-side duplicate must never extend ownership on its own.
os.close(inherited_descriptor)
try:
stdout, stderr = process.communicate(
# subprocess accepts any bytes-like object at runtime. Keep the
# mutable buffer so the caller can zero credentials in-place.
input=request_bytes, # type: ignore[arg-type]
timeout=timeout_seconds,
)
except BaseException:
_kill_and_reap_process_group(process)
raise
return subprocess.CompletedProcess(
args=command,
returncode=process.returncode,
stdout=stdout,
stderr=stderr,
)
def _kill_and_reap_process_group(process: subprocess.Popen[bytes]) -> None:
"""Prove the helper and every descendant are gone before dropping a fence."""
with suppress(ProcessLookupError, PermissionError):
os.killpg(process.pid, signal.SIGKILL)
try:
process.communicate(timeout=5.0)
except subprocess.TimeoutExpired:
with suppress(OSError):
process.kill()
process.wait(timeout=5.0)
class HostWifiAssociationIdentityProbe:
"""Observe macOS Wi-Fi association continuity without exporting its name.
The Swift helper HMACs its in-process SSID/BSSID evidence with a random key
supplied over stdin. This object retains that key only for its own process
lifetime, so equal networks cannot be correlated across service restarts.
When CoreWLAN cannot expose an association (for example because privacy
authorization hides both identifiers), observations for the same
interface/failure scope receive one process-scoped fallback token. The
token is not association proof and callers must keep that distinction, but
it prevents an unchanged kernel route from being misclassified as a new
network on every monitor poll. Route loss, interface/source changes,
recovery of exact BSSID evidence, or a service restart still changes the
combined route fingerprint.
"""
def __init__(
self,
helper_path: Path,
*,
runner: RunProcess = subprocess.run,
continuity_key: bytes | None = None,
max_cache_age_seconds: float = MAX_ASSOCIATION_OBSERVATION_CACHE_AGE_SECONDS,
monotonic_clock: Callable[[], float] = time.monotonic,
wall_clock: Callable[[], float] = time.time,
) -> None:
key = secrets.token_bytes(32) if continuity_key is None else bytes(continuity_key)
if len(key) != 32:
raise ValueError("host Wi-Fi continuity key must contain exactly 32 bytes")
if not 0 <= max_cache_age_seconds <= MAX_ASSOCIATION_OBSERVATION_CACHE_AGE_SECONDS:
raise ValueError(
"host Wi-Fi association cache age must be between 0 and 0.75 seconds"
)
self._helper_path = helper_path
self._runner = runner
self._continuity_key = key
self._max_cache_age_seconds = max_cache_age_seconds
self._monotonic_clock = monotonic_clock
self._wall_clock = wall_clock
self._cached_observations: dict[
str,
tuple[float, float, HostWifiAssociationIdentityResult],
] = {}
self._last_interface_name: str | None = None
self._fallback_tokens: dict[str, str] = {}
self._lock = threading.Lock()
def observe(
self,
interface_name: str | None,
*,
timeout_seconds: float = 30.0,
) -> HostWifiAssociationIdentityResult:
"""Return one opaque association token, failing closed when uncertain."""
if timeout_seconds <= 0:
raise ValueError("timeout_seconds must be positive")
if interface_name is not None and (
not 1 <= len(interface_name) <= 32
or not all(
character.isascii()
and (character.isalnum() or character in "._-")
for character in interface_name
)
):
raise ValueError("host Wi-Fi interface name contains unsupported characters")
deadline = time.monotonic() + timeout_seconds
if not self._lock.acquire(timeout=timeout_seconds):
return self._fallback_without_lock(
adapter="unavailable",
wifi_interface=None,
reason_code="host-wifi-operation-timeout",
scope=f"interface:{interface_name or 'none'}",
)
try:
if interface_name is None:
self._invalidate_for_interface_transition_locked(None)
return self._fallback_locked(
adapter="unavailable",
wifi_interface=None,
reason_code="host-route-interface-unavailable",
scope="interface:none",
)
self._invalidate_for_interface_transition_locked(interface_name)
cached = self._cached_observation_locked(interface_name)
if cached is not None:
return cached
remaining_seconds = deadline - time.monotonic()
if remaining_seconds <= 0:
return self._remember_observation_locked(
interface_name,
self._fallback_locked(
adapter="unavailable",
wifi_interface=None,
reason_code="host-wifi-operation-timeout",
scope=f"interface:{interface_name}",
),
)
try:
response = _run_macos_helper(
self._helper_path,
{
"action": "inspect-association",
"profile_id": _ASSOCIATION_INSPECTION_PROFILE_ID,
"interface_name": interface_name,
"continuity_key_hex": self._continuity_key.hex(),
},
timeout_seconds=remaining_seconds,
runner=self._runner,
)
except HostWifiProfileError as exc:
return self._remember_observation_locked(
interface_name,
self._fallback_locked(
adapter="unavailable",
wifi_interface=None,
reason_code=exc.reason_code,
scope=f"interface:{interface_name}",
),
)
adapter = response.get("adapter")
wifi_interface = response.get("wifi_interface")
association_identity = response.get("association_identity")
evidence_quality = response.get("association_evidence")
reason_code = response.get("reason_code")
if (
not isinstance(adapter, str)
or not adapter
or not isinstance(wifi_interface, bool)
or evidence_quality
not in {"ssid+bssid", "bssid-only", "not-wifi", "unavailable"}
or (reason_code is not None and not isinstance(reason_code, str))
):
return self._remember_observation_locked(
interface_name,
self._fallback_locked(
adapter="unavailable",
wifi_interface=None,
reason_code="host-wifi-response-invalid",
scope=f"interface:{interface_name}",
),
)
continuity_proven = evidence_quality in {
"ssid+bssid",
"bssid-only",
"not-wifi",
}
if not continuity_proven:
return self._remember_observation_locked(
interface_name,
self._fallback_locked(
adapter=adapter,
wifi_interface=wifi_interface,
reason_code=reason_code or "association-identity-unavailable",
scope=f"interface:{interface_name}",
),
)
if (
not isinstance(association_identity, str)
or len(association_identity) != _ASSOCIATION_IDENTITY_HEX_LENGTH
or any(character not in "0123456789abcdef" for character in association_identity)
):
return self._remember_observation_locked(
interface_name,
self._fallback_locked(
adapter="unavailable",
wifi_interface=wifi_interface,
reason_code="host-wifi-response-invalid",
scope=f"interface:{interface_name}",
),
)
return self._remember_observation_locked(
interface_name,
{
"schema_version": 1,
"adapter": adapter,
"wifi_interface": wifi_interface,
"association_state": (
"not-wifi" if evidence_quality == "not-wifi" else "associated"
),
"evidence_quality": evidence_quality,
"continuity_proven": True,
"continuity_token": association_identity,
"reason_code": None,
},
)
finally:
self._lock.release()
def _invalidate_for_interface_transition_locked(
self,
interface_name: str | None,
) -> None:
"""Make an interface transition an immediate cache barrier."""
if interface_name == self._last_interface_name:
return
self._cached_observations.clear()
self._last_interface_name = interface_name
def _cached_observation_locked(
self,
interface_name: str,
) -> HostWifiAssociationIdentityResult | None:
"""Reuse only a sub-second read-only association observation.
Runtime compilation is deliberately not used here: the field-approved
credential/CoreWLAN path relies on Apple's signed Swift interpreter
identity. The cache is therefore limited to the read-only association
result, never a credential or mutation result. Both clocks must remain
monotonic within the 750 ms safety window; sleep, a wall-clock jump or
an interface transition forces a fresh source-runner observation before
the caller can publish.
"""
cached = self._cached_observations.get(interface_name)
if cached is None or self._max_cache_age_seconds == 0:
return None
observed_monotonic, observed_wall, observation = cached
monotonic_age = self._monotonic_clock() - observed_monotonic
wall_age = self._wall_clock() - observed_wall
if (
monotonic_age < 0
or wall_age < 0
or monotonic_age > self._max_cache_age_seconds
or wall_age > self._max_cache_age_seconds
):
self._cached_observations.pop(interface_name, None)
return None
return observation.copy()
def _remember_observation_locked(
self,
interface_name: str,
observation: HostWifiAssociationIdentityResult,
) -> HostWifiAssociationIdentityResult:
if self._max_cache_age_seconds > 0:
self._cached_observations[interface_name] = (
self._monotonic_clock(),
self._wall_clock(),
observation.copy(),
)
return observation.copy()
def _fallback_locked(
self,
*,
adapter: str,
wifi_interface: bool | None,
reason_code: str,
scope: str,
) -> HostWifiAssociationIdentityResult:
token_scope = "\x1f".join(
(
scope,
adapter,
"unknown" if wifi_interface is None else str(wifi_interface).lower(),
reason_code,
)
)
token = self._fallback_tokens.get(token_scope)
if token is None:
material = (
"mission-core/host-wifi-association-fallback/v2\x1f"
f"{token_scope}"
).encode()
token = hmac.new(self._continuity_key, material, hashlib.sha256).hexdigest()
self._fallback_tokens[token_scope] = token
return {
"schema_version": 1,
"adapter": adapter,
"wifi_interface": wifi_interface,
"association_state": "unavailable",
"evidence_quality": "unavailable",
"continuity_proven": False,
"continuity_token": token,
"reason_code": reason_code,
}
def _fallback_without_lock(
self,
*,
adapter: str,
wifi_interface: bool | None,
reason_code: str,
scope: str,
) -> HostWifiAssociationIdentityResult:
"""Build the deterministic process-local fallback under lock contention."""
token_scope = "\x1f".join(
(
scope,
adapter,
"unknown" if wifi_interface is None else str(wifi_interface).lower(),
reason_code,
)
)
material = (
"mission-core/host-wifi-association-fallback/v2\x1f"
f"{token_scope}"
).encode()
return {
"schema_version": 1,
"adapter": adapter,
"wifi_interface": wifi_interface,
"association_state": "unavailable",
"evidence_quality": "unavailable",
"continuity_proven": False,
"continuity_token": hmac.new(
self._continuity_key,
material,
hashlib.sha256,
).hexdigest(),
"reason_code": reason_code,
}
def bind_route_fingerprint_to_wifi_association(
route_fingerprint: str,
association: HostWifiAssociationIdentityResult,
) -> str:
"""Bind route facts to one opaque association observation."""
if not route_fingerprint:
raise ValueError("route fingerprint must be nonblank")
token = association["continuity_token"]
if (
len(token) != _ASSOCIATION_IDENTITY_HEX_LENGTH
or any(character not in "0123456789abcdef" for character in token)
):
raise ValueError("association continuity token is invalid")
material = (
"mission-core/host-route+wifi-association/v1\x1f"
f"{route_fingerprint}\x1f{token}"
)
return hashlib.sha256(material.encode()).hexdigest()
def associate_with_wifi_profile_once(
helper_path: Path,
profile_id: str,
expected_ssid: str,
*,
scan_timeout_seconds: float = 15.0,
scan_timeout_seconds: float = 30.0,
timeout_seconds: float = 180.0,
runner: RunProcess = subprocess.run,
process_fence_descriptor_factory: Callable[[], int] | None = None,
) -> HostWifiProfileAssociationResult:
"""Associate through one device-scoped OS profile without exposing its secret.
The selected device supplies the expected, operator-visible AP SSID. The
The selected device supplies the expected, operator-visible AP SSID. The
platform helper performs bounded exact-SSID discovery followed by at most
one association. It resolves a device-scoped secret from the OS credential
stores, or asks for it through a native secure prompt on first use. The
secret never crosses the helper boundary.
one association using the already materialized Mission Core profile. It
never falls back to another system credential or a post-write password
prompt, and the secret never crosses the helper boundary.
"""
_validate_profile_id(profile_id)
@@ -422,6 +666,7 @@ def associate_with_wifi_profile_once(
},
timeout_seconds=timeout_seconds,
runner=runner,
process_fence_descriptor_factory=process_fence_descriptor_factory,
)
already_associated = response.get("already_associated")
profile_enrolled = response.get("profile_enrolled")
@@ -439,12 +684,7 @@ def associate_with_wifi_profile_once(
or type(scan_elapsed_ms) is not int
or scan_elapsed_ms < 0
or not isinstance(credential_source, str)
or credential_source not in {
"mission-core-keychain",
"system-wifi-keychain",
"native-secure-prompt",
"exact-firmware-profile",
}
or credential_source != "exact-firmware-profile"
):
raise HostWifiProfileError("host-wifi-response-invalid")
return {
@@ -459,6 +699,78 @@ def associate_with_wifi_profile_once(
}
def associate_with_ephemeral_wifi_credentials_once(
helper_path: Path,
expected_ssid: str,
password: str,
*,
scan_timeout_seconds: float = 30.0,
timeout_seconds: float = 180.0,
runner: RunProcess = subprocess.run,
process_fence_descriptor_factory: Callable[[], int] | None = None,
) -> HostWifiProfileAssociationResult:
"""Join one operator-selected Bridge network without persisting its secret.
This host-side handoff is used after K1 has already applied the Bridge
topology and the Mac no longer has a direct route to it (most notably when
switching from the K1 access point back to the shared network). The
password crosses only the helper's stdin, is zeroed from the mutable Python
request buffer, and is neither stored in the Mission Core Keychain profile
store nor returned in evidence.
The call performs at most one CoreWLAN association after a bounded exact
SSID scan. It never repeats the preceding BLE/device mutation.
"""
if not 1 <= len(expected_ssid.encode("utf-8")) <= 32:
raise ValueError("SSID must contain between 1 and 32 UTF-8 bytes")
if not 1 <= len(password.encode("utf-8")) <= 64:
raise ValueError("Wi-Fi password must contain between 1 and 64 UTF-8 bytes")
if not 0 <= scan_timeout_seconds <= 60:
raise ValueError("scan_timeout_seconds must be between 0 and 60")
response = _run_macos_helper(
helper_path,
{
"action": "associate-ephemeral",
"profile_id": _EPHEMERAL_ASSOCIATION_PROFILE_ID,
"ssid": expected_ssid,
"password": password,
"scan_timeout_seconds": scan_timeout_seconds,
},
timeout_seconds=timeout_seconds,
runner=runner,
process_fence_descriptor_factory=process_fence_descriptor_factory,
)
already_associated = response.get("already_associated")
profile_enrolled = response.get("profile_enrolled")
scan_attempt_count = response.get("scan_attempt_count")
scan_elapsed_ms = response.get("scan_elapsed_ms")
credential_source = response.get("credential_source")
adapter = response.get("adapter")
if (
not isinstance(already_associated, bool)
or profile_enrolled is not False
or not isinstance(adapter, str)
or not adapter
or type(scan_attempt_count) is not int
or scan_attempt_count < 1
or type(scan_elapsed_ms) is not int
or scan_elapsed_ms < 0
or credential_source != "operation-memory"
):
raise HostWifiProfileError("host-wifi-response-invalid")
return {
"schema_version": 1,
"adapter": adapter,
"outcome": "already-associated" if already_associated else "associated",
"already_associated": already_associated,
"profile_enrolled": False,
"scan_attempt_count": scan_attempt_count,
"scan_elapsed_ms": scan_elapsed_ms,
"credential_source": "operation-memory",
}
def check_wifi_profile(
helper_path: Path,
profile_id: str,
@@ -483,13 +795,27 @@ def check_wifi_profile(
runner=runner,
)
available = response.get("profile_available")
credential_source = response.get("credential_source")
adapter = response.get("adapter")
if not isinstance(available, bool) or not isinstance(adapter, str) or not adapter:
if (
not isinstance(available, bool)
or not isinstance(adapter, str)
or not adapter
or (
available
and credential_source != "exact-firmware-profile"
)
or (
not available
and credential_source is not None
)
):
raise HostWifiProfileError("host-wifi-response-invalid")
return {
"schema_version": 1,
"adapter": adapter,
"available": available,
"credential_source": credential_source,
}
@@ -501,6 +827,7 @@ def ensure_wifi_profile_from_credential_source(
*,
timeout_seconds: float = 30.0,
runner: RunProcess = subprocess.run,
process_fence_descriptor_factory: Callable[[], int] | None = None,
) -> HostWifiProfileEnsureResult:
"""Materialize one device profile from a firmware-scoped secure-store item.
@@ -522,6 +849,7 @@ def ensure_wifi_profile_from_credential_source(
},
timeout_seconds=timeout_seconds,
runner=runner,
process_fence_descriptor_factory=process_fence_descriptor_factory,
)
available = response.get("profile_available")
profile_enrolled = response.get("profile_enrolled")