feat(k1): recover exact application bootstrap

This commit is contained in:
DCCONSTRUCTIONS
2026-07-18 13:54:25 +03:00
parent af9319a33b
commit d7749208a7
15 changed files with 906 additions and 215 deletions
@@ -0,0 +1,502 @@
from __future__ import annotations
import hashlib
import hmac
import re
from dataclasses import dataclass, field
from typing import Literal
from k1link.device_plugins.xgrids_k1.protocol.modeling_control import OPENAPI_SUCCESS
from k1link.device_plugins.xgrids_k1.protocol.protobuf_wire import (
ProtobufWireError,
ProtoField,
iter_fields,
)
MAX_APPLICATION_PAYLOAD_BYTES = 64 * 1024
MAX_HEADER_BYTES = 4 * 1024
MAX_TEXT_BYTES = 4 * 1024
APPLICATION_AUTHORITY_SOURCE = "owner-captured-lixelgo-application"
COMPATIBILITY_PROFILE_ID = "xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1"
DEVICE_CONFIG_TIME_CONTEXT = "Publish_Proto_DeviceConfig_SetTime"
SHADOW_BLOCKERS = ("vendor-writes-disabled", "publisher-not-installed")
DEVICE_INFO_REQUEST_TOPIC = "lixel/application/request/device_info"
DEVICE_INFO_RESPONSE_TOPIC = "lixel/application/response/device_info"
MODELING_STATUS_REQUEST_TOPIC = "lixel/application/request/modeling_status"
MODELING_STATUS_RESPONSE_TOPIC = "lixel/application/response/modeling_status"
GET_RTK_ADVANCE_REQUEST_TOPIC = "lixel/application/request/get_rtk_advance"
GET_RTK_ADVANCE_RESPONSE_TOPIC = "lixel/application/response/get_rtk_advance"
DEVICE_CONFIG_REQUEST_TOPIC = "lixel/application/request/device_config"
DEVICE_CONFIG_RESPONSE_TOPIC = "lixel/application/response/device_config"
GET_NTRIP_PROFILE_REQUEST_TOPIC = "lixel/application/request/get_ntrip_profile"
GET_NTRIP_PROFILE_RESPONSE_TOPIC = "lixel/application/response/get_ntrip_profile"
GET_CLOUD_CONFIG_REQUEST_TOPIC = "lixel/application/request/get_cloud_server_config"
GET_CLOUD_CONFIG_RESPONSE_TOPIC = "lixel/application/response/get_cloud_server_config"
class ApplicationBootstrapError(ValueError):
"""The recovered K1 application bootstrap contract was violated."""
@dataclass(frozen=True, slots=True)
class ApplicationControlAuthority:
"""Private authority belonging to the reviewed LixelGO application profile.
The retained application and wire evidence show one OpenAPI value shared by
all request types. It is not a scanner serial or a per-device enrollment.
"""
openapi_key: str = field(repr=False)
source: Literal["owner-captured-lixelgo-application"] = "owner-captured-lixelgo-application"
compatibility_profile_id: Literal["xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1"] = (
"xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1"
)
def __post_init__(self) -> None:
if (
self.source != APPLICATION_AUTHORITY_SOURCE
or self.compatibility_profile_id != COMPATIBILITY_PROFILE_ID
):
raise ApplicationBootstrapError(
"application authority does not match the reviewed K1 profile"
)
_validate_ascii_identity(self.openapi_key, "openapi_key")
@dataclass(frozen=True, slots=True)
class LiveDeviceControlBinding:
"""Identity and compatibility facts learned from the selected live K1."""
vendor_device_id: str = field(repr=False)
device_serial: str = field(repr=False)
software_version: str = field(repr=False)
system_version: str = field(repr=False)
device_model: str = field(repr=False)
device_type: str = field(repr=False)
is_activated: bool
def __post_init__(self) -> None:
_validate_ascii_identity(self.vendor_device_id, "vendor_device_id")
_validate_ascii_identity(self.device_serial, "device_serial")
for name, value in (
("software_version", self.software_version),
("system_version", self.system_version),
("device_model", self.device_model),
("device_type", self.device_type),
):
_validate_text(value, name)
@property
def matches_reviewed_firmware(self) -> bool:
return _contains_version(self.software_version, "3.0.2") and _contains_version(
self.system_version, "3.0.2"
)
@property
def ready_for_reviewed_profile(self) -> bool:
return self.is_activated and self.matches_reviewed_firmware
@dataclass(frozen=True, slots=True)
class ApplicationRequestHeader:
message_type: str
authority: ApplicationControlAuthority = field(repr=False)
binding: LiveDeviceControlBinding | None = field(default=None, repr=False)
context: str | None = None
def __post_init__(self) -> None:
_validate_ascii_identity(self.message_type, "message_type")
if self.context is not None:
_validate_ascii_identity(self.context, "context")
if len(self.session_id.encode("ascii")) > MAX_TEXT_BYTES:
raise ApplicationBootstrapError("derived session_id exceeds configured limit")
@property
def session_id(self) -> str:
device_id = self.binding.vendor_device_id if self.binding is not None else ""
session = f"{device_id}:{self.message_type}"
if self.context is not None:
session = f"{session}:{self.context}"
return session
@dataclass(frozen=True, slots=True)
class EncodedApplicationRequest:
ordinal: int
phase: Literal["identity-discovery", "bound-preparation"]
message_type: str
topic: str
response_topic: str
payload: bytes = field(repr=False)
payload_sha256: str
payload_bytes: int
mutates_device: bool
requires_live_binding: bool
qos: int = 2
retain: bool = False
def as_dict(self) -> dict[str, object]:
return {
"ordinal": self.ordinal,
"phase": self.phase,
"message_type": self.message_type,
"topic": self.topic,
"response_topic": self.response_topic,
"payload_sha256": self.payload_sha256,
"payload_bytes": self.payload_bytes,
"mutates_device": self.mutates_device,
"requires_live_binding": self.requires_live_binding,
"qos": self.qos,
"retain": self.retain,
}
@dataclass(frozen=True, slots=True)
class ShadowApplicationBootstrap:
"""Exact observed pre-START request order, with no execution authority."""
requests: tuple[EncodedApplicationRequest, ...] = field(repr=False)
executable: bool = False
blockers: tuple[str, ...] = SHADOW_BLOCKERS
retry_policy: Literal["never-automatic"] = "never-automatic"
def as_dict(self) -> dict[str, object]:
return {
"mode": "shadow-only",
"request_count": len(self.requests),
"mutation_count": sum(request.mutates_device for request in self.requests),
"requests": [request.as_dict() for request in self.requests],
"executable": self.executable,
"blockers": list(self.blockers),
"retry_policy": self.retry_policy,
}
@dataclass(frozen=True, slots=True)
class DeviceInfoResponse:
binding: LiveDeviceControlBinding = field(repr=False)
session_id: str = field(repr=False)
openapi_key: str = field(repr=False)
result_code: int
def build_shadow_bootstrap(
authority: ApplicationControlAuthority,
binding: LiveDeviceControlBinding,
*,
epoch_seconds: int,
timezone_name: str,
) -> ShadowApplicationBootstrap:
"""Build the ten pre-START requests observed in the clean LixelGO cycle.
The first three requests intentionally omit the vendor device ID. The
remaining requests are bound to identity returned by ``DeviceInfoResponse``.
The only preparatory mutation is the observed clock/timezone sync.
"""
if not binding.ready_for_reviewed_profile:
raise ApplicationBootstrapError(
"live DeviceInfo does not attest the reviewed activated FW 3.0.2 profile"
)
if not isinstance(epoch_seconds, int) or isinstance(epoch_seconds, bool):
raise ApplicationBootstrapError("epoch_seconds must be an integer")
if epoch_seconds < 0 or epoch_seconds > 0x7FFF_FFFF_FFFF_FFFF:
raise ApplicationBootstrapError("epoch_seconds is outside recovered int64 range")
_validate_text(timezone_name, "timezone_name")
specs = (
(
"identity-discovery",
"DeviceInfoRequest",
DEVICE_INFO_REQUEST_TOPIC,
DEVICE_INFO_RESPONSE_TOPIC,
None,
False,
),
(
"identity-discovery",
"ModelingStatusRequest",
MODELING_STATUS_REQUEST_TOPIC,
MODELING_STATUS_RESPONSE_TOPIC,
None,
False,
),
(
"identity-discovery",
"GetRtkAdvanceRequest",
GET_RTK_ADVANCE_REQUEST_TOPIC,
GET_RTK_ADVANCE_RESPONSE_TOPIC,
None,
False,
),
(
"bound-preparation",
"DeviceConfigRequest",
DEVICE_CONFIG_REQUEST_TOPIC,
DEVICE_CONFIG_RESPONSE_TOPIC,
DEVICE_CONFIG_TIME_CONTEXT,
True,
),
(
"bound-preparation",
"DeviceInfoRequest",
DEVICE_INFO_REQUEST_TOPIC,
DEVICE_INFO_RESPONSE_TOPIC,
None,
False,
),
(
"bound-preparation",
"GetRtkAdvanceRequest",
GET_RTK_ADVANCE_REQUEST_TOPIC,
GET_RTK_ADVANCE_RESPONSE_TOPIC,
None,
False,
),
(
"bound-preparation",
"GetNtripProfileRequest",
GET_NTRIP_PROFILE_REQUEST_TOPIC,
GET_NTRIP_PROFILE_RESPONSE_TOPIC,
None,
False,
),
(
"bound-preparation",
"GetCloudServerConfigRequest",
GET_CLOUD_CONFIG_REQUEST_TOPIC,
GET_CLOUD_CONFIG_RESPONSE_TOPIC,
None,
False,
),
(
"bound-preparation",
"GetRtkAdvanceRequest",
GET_RTK_ADVANCE_REQUEST_TOPIC,
GET_RTK_ADVANCE_RESPONSE_TOPIC,
None,
False,
),
(
"bound-preparation",
"DeviceInfoRequest",
DEVICE_INFO_REQUEST_TOPIC,
DEVICE_INFO_RESPONSE_TOPIC,
None,
False,
),
)
requests: list[EncodedApplicationRequest] = []
for ordinal, (phase, message_type, topic, response_topic, context, mutates) in enumerate(
specs, start=1
):
request_binding = None if ordinal <= 3 else binding
header = ApplicationRequestHeader(
message_type=message_type,
authority=authority,
binding=request_binding,
context=context,
)
body = b""
if message_type == "DeviceConfigRequest":
time_config = _varint_field(1, epoch_seconds) + _text_field(2, timezone_name)
body = _bytes_field(4, time_config)
payload = _bytes_field(1, _encode_header(header)) + body
_check_payload(payload, "application request")
requests.append(
EncodedApplicationRequest(
ordinal=ordinal,
phase=phase, # type: ignore[arg-type]
message_type=message_type,
topic=topic,
response_topic=response_topic,
payload=payload,
payload_sha256=hashlib.sha256(payload).hexdigest(),
payload_bytes=len(payload),
mutates_device=mutates,
requires_live_binding=ordinal > 3,
)
)
return ShadowApplicationBootstrap(tuple(requests))
def decode_and_bind_device_info_response(
payload: bytes,
authority: ApplicationControlAuthority,
) -> DeviceInfoResponse:
"""Correlate the bootstrap response and derive a live per-device binding."""
_check_payload(payload, "DeviceInfoResponse")
top = _unique_fields(payload, "DeviceInfoResponse", max_fields=64)
header = _unique_fields(_required_bytes(top, 1, "response.header"), "header", max_fields=32)
device_id = _identity_field(header, 4, "header.device_id")
session_id = _identity_field(header, 5, "header.session_id")
openapi_key = _identity_field(header, 6, "header.openapi_key")
if session_id != ":DeviceInfoRequest":
raise ApplicationBootstrapError("DeviceInfoResponse session correlation failed")
if not hmac.compare_digest(openapi_key, authority.openapi_key):
raise ApplicationBootstrapError("DeviceInfoResponse application authority mismatch")
error = _unique_fields(_required_bytes(top, 15, "response.error"), "error", max_fields=8)
result_code = _required_uint(error, 1, "response.error.code")
if result_code != OPENAPI_SUCCESS:
raise ApplicationBootstrapError(
f"DeviceInfoResponse rejected with result code {result_code}"
)
device_info = _unique_fields(
_required_bytes(top, 2, "response.device_info"), "device_info", max_fields=32
)
base_info = _unique_fields(
_required_bytes(device_info, 2, "device_info.base_info"),
"base_info",
max_fields=32,
allow_repeated={11},
)
working_status = _unique_fields(
_required_bytes(device_info, 7, "device_info.working_status"),
"working_status",
max_fields=16,
)
binding = LiveDeviceControlBinding(
vendor_device_id=device_id,
device_serial=_required_text(base_info, 7, "base_info.device_sn"),
software_version=_required_text(base_info, 2, "base_info.software_version"),
system_version=_required_text(base_info, 3, "base_info.system_version"),
device_model=_required_text(base_info, 6, "base_info.device_model"),
device_type=_required_text(base_info, 8, "base_info.device_type"),
is_activated=bool(_required_bool(working_status, 1, "working_status.is_activated")),
)
return DeviceInfoResponse(binding, session_id, openapi_key, result_code)
def _encode_header(header: ApplicationRequestHeader) -> bytes:
parts: list[bytes] = []
if header.binding is not None:
parts.append(_text_field(4, header.binding.vendor_device_id))
parts.extend(
(
_text_field(5, header.session_id),
_text_field(6, header.authority.openapi_key),
)
)
payload = b"".join(parts)
if len(payload) > MAX_HEADER_BYTES:
raise ApplicationBootstrapError("encoded request header exceeds configured limit")
return payload
def _contains_version(value: str, expected: str) -> bool:
return re.search(rf"(?<!\d){re.escape(expected)}(?!\d)", value) is not None
def _validate_ascii_identity(value: object, name: str) -> None:
if not isinstance(value, str) or not value:
raise ApplicationBootstrapError(f"{name} must be a non-empty string")
try:
encoded = value.encode("ascii")
except UnicodeEncodeError as exc:
raise ApplicationBootstrapError(f"{name} must use printable ASCII") from exc
if len(encoded) > MAX_TEXT_BYTES:
raise ApplicationBootstrapError(f"{name} exceeds configured limit")
if any(byte <= 0x20 or byte > 0x7E for byte in encoded):
raise ApplicationBootstrapError(f"{name} must use printable ASCII without spaces")
def _validate_text(value: object, name: str) -> None:
if not isinstance(value, str) or not value:
raise ApplicationBootstrapError(f"{name} must be a non-empty string")
encoded = value.encode("utf-8")
if len(encoded) > MAX_TEXT_BYTES:
raise ApplicationBootstrapError(f"{name} exceeds configured limit")
if any(ord(character) < 0x20 or ord(character) == 0x7F for character in value):
raise ApplicationBootstrapError(f"{name} contains a control character")
def _check_payload(payload: bytes, name: str) -> None:
if len(payload) > MAX_APPLICATION_PAYLOAD_BYTES:
raise ApplicationBootstrapError(f"{name} exceeds configured limit")
def _unique_fields(
payload: bytes,
name: str,
*,
max_fields: int,
allow_repeated: set[int] | None = None,
) -> dict[int, ProtoField]:
result: dict[int, ProtoField] = {}
repeated = allow_repeated or set()
try:
for item in iter_fields(payload, max_fields=max_fields):
if item.number in result and item.number not in repeated:
raise ApplicationBootstrapError(f"{name} field {item.number} is duplicated")
result[item.number] = item
except ProtobufWireError as exc:
raise ApplicationBootstrapError(f"invalid {name}: {exc}") from exc
return result
def _required_bytes(fields: dict[int, ProtoField], number: int, name: str) -> bytes:
item = fields.get(number)
if item is None or item.wire_type != 2 or not isinstance(item.value, bytes):
raise ApplicationBootstrapError(f"{name} is missing or has the wrong wire type")
return item.value
def _required_text(fields: dict[int, ProtoField], number: int, name: str) -> str:
raw = _required_bytes(fields, number, name)
if len(raw) > MAX_TEXT_BYTES:
raise ApplicationBootstrapError(f"{name} exceeds configured limit")
try:
value = raw.decode("utf-8")
except UnicodeDecodeError as exc:
raise ApplicationBootstrapError(f"{name} is not valid UTF-8") from exc
_validate_text(value, name)
return value
def _identity_field(fields: dict[int, ProtoField], number: int, name: str) -> str:
value = _required_text(fields, number, name)
_validate_ascii_identity(value, name)
return value
def _required_uint(fields: dict[int, ProtoField], number: int, name: str) -> int:
item = fields.get(number)
if item is None or item.wire_type != 0 or not isinstance(item.value, int):
raise ApplicationBootstrapError(f"{name} is missing or has the wrong wire type")
return item.value
def _required_bool(fields: dict[int, ProtoField], number: int, name: str) -> int:
value = _required_uint(fields, number, name)
if value not in (0, 1):
raise ApplicationBootstrapError(f"{name} is not a protobuf boolean")
return value
def _varint(value: int) -> bytes:
encoded = bytearray()
while value > 0x7F:
encoded.append((value & 0x7F) | 0x80)
value >>= 7
encoded.append(value)
return bytes(encoded)
def _key(number: int, wire_type: int) -> bytes:
return _varint((number << 3) | wire_type)
def _varint_field(number: int, value: int) -> bytes:
return _key(number, 0) + _varint(value)
def _bytes_field(number: int, value: bytes) -> bytes:
return _key(number, 2) + _varint(len(value)) + value
def _text_field(number: int, value: str) -> bytes:
return _bytes_field(number, value.encode("utf-8"))
@@ -5,12 +5,15 @@ import threading
from dataclasses import dataclass, field
from typing import Literal
from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
ApplicationControlAuthority,
LiveDeviceControlBinding,
)
from k1link.device_plugins.xgrids_k1.protocol.modeling_control import (
CommandHeaderIdentity,
DeviceStatusReport,
EncodedModelingCommand,
ModelingAction,
ModelingEncodeError,
MountType,
RecordMode,
ScanMode,
@@ -25,59 +28,12 @@ from k1link.viewer.metrics import BridgeMetrics
DEVICE_STATUS_TOPIC = "lixel/application/report/device_status"
MODELING_REQUEST_TOPIC = "lixel/application/request/modeling"
SHADOW_BLOCKERS = ("vendor-writes-disabled", "publisher-not-installed")
CONTROL_ENROLLMENT_SOURCE = "owner-captured-device-bound"
CONTROL_COMPATIBILITY_PROFILE_ID = "xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1"
CONTROL_FIRMWARE_VERSION = "3.0.2"
CONTROL_TOPOLOGY = "direct-lan"
class ModelingControlSafetyError(RuntimeError):
"""A live K1 could not be bound to reviewed device-specific control input."""
@dataclass(frozen=True, slots=True)
class DeviceBoundControlEnrollment:
"""Private, device-specific values learned from owner-controlled evidence.
Mission Core's provisional device UUID, BLE transport UUID and K1 serial are
not interchangeable with the vendor header identity. An enrollment binds
all three vendor inputs explicitly and keeps them out of repr/API output.
"""
vendor_device_id: str = field(repr=False)
device_serial: str = field(repr=False)
openapi_key: str = field(repr=False)
source: Literal["owner-captured-device-bound"] = "owner-captured-device-bound"
compatibility_profile_id: Literal["xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1"] = (
"xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1"
)
firmware_version: Literal["3.0.2"] = "3.0.2"
topology: Literal["direct-lan"] = "direct-lan"
def __post_init__(self) -> None:
if (
self.source != CONTROL_ENROLLMENT_SOURCE
or self.compatibility_profile_id != CONTROL_COMPATIBILITY_PROFILE_ID
or self.firmware_version != CONTROL_FIRMWARE_VERSION
or self.topology != CONTROL_TOPOLOGY
):
raise ModelingControlSafetyError(
"control enrollment does not match the exact reviewed K1 profile"
)
CommandHeaderIdentity(
device_id=self.vendor_device_id,
openapi_key=self.openapi_key,
)
_validate_device_serial(self.device_serial)
@property
def command_header(self) -> CommandHeaderIdentity:
return CommandHeaderIdentity(
device_id=self.vendor_device_id,
openapi_key=self.openapi_key,
)
@dataclass(frozen=True, slots=True)
class ModelingControlSafetySnapshot:
status_reports_observed: int
@@ -215,13 +171,14 @@ class LiveModelingControlSafety:
def plan_start(
self,
enrollment: DeviceBoundControlEnrollment,
authority: ApplicationControlAuthority,
binding: LiveDeviceControlBinding,
*,
project_name: str,
) -> ShadowModelingCommand:
self._require_bound(enrollment, required_state=SessionState.READY, project_bound=False)
self._require_bound(binding, required_state=SessionState.READY, project_bound=False)
command = encode_modeling_start(
enrollment.command_header,
_command_header(authority, binding),
project_name=project_name,
record_mode=RecordMode.RECORD_AND_CALCULATE,
scan_mode=ScanMode.LCC,
@@ -231,14 +188,17 @@ class LiveModelingControlSafety:
def plan_stop(
self,
enrollment: DeviceBoundControlEnrollment,
authority: ApplicationControlAuthority,
binding: LiveDeviceControlBinding,
) -> ShadowModelingCommand:
self._require_bound(enrollment, required_state=SessionState.SCANNING, project_bound=True)
return ShadowModelingCommand.from_command(encode_modeling_stop(enrollment.command_header))
self._require_bound(binding, required_state=SessionState.SCANNING, project_bound=True)
return ShadowModelingCommand.from_command(
encode_modeling_stop(_command_header(authority, binding))
)
def _require_bound(
self,
enrollment: DeviceBoundControlEnrollment,
binding: LiveDeviceControlBinding,
*,
required_state: SessionState,
project_bound: bool,
@@ -250,11 +210,15 @@ class LiveModelingControlSafety:
if report is None or self._vendor_device_id is None or self._device_serial is None:
raise ModelingControlSafetyError("live K1 identity/status evidence is missing")
if (
enrollment.vendor_device_id != self._vendor_device_id
or enrollment.device_serial != self._device_serial
binding.vendor_device_id != self._vendor_device_id
or binding.device_serial != self._device_serial
):
raise ModelingControlSafetyError(
"device-bound control enrollment does not match the live K1"
"DeviceInfo binding does not match the live K1 status stream"
)
if not binding.ready_for_reviewed_profile:
raise ModelingControlSafetyError(
"live DeviceInfo does not attest the reviewed activated FW 3.0.2 profile"
)
if report.session_state is not required_state:
raise ModelingControlSafetyError(
@@ -264,12 +228,11 @@ class LiveModelingControlSafety:
raise ModelingControlSafetyError("live K1 project binding is not in the safe state")
def _validate_device_serial(value: object) -> None:
if not isinstance(value, str) or not value:
raise ModelingEncodeError("device_serial must be a non-empty string")
try:
encoded = value.encode("ascii")
except UnicodeEncodeError as exc:
raise ModelingEncodeError("device_serial must use printable ASCII") from exc
if len(encoded) > 256 or any(byte <= 0x20 or byte > 0x7E for byte in encoded):
raise ModelingEncodeError("device_serial must use bounded printable ASCII without spaces")
def _command_header(
authority: ApplicationControlAuthority,
binding: LiveDeviceControlBinding,
) -> CommandHeaderIdentity:
return CommandHeaderIdentity(
device_id=binding.vendor_device_id,
openapi_key=authority.openapi_key,
)