Files
chimera-gfx-Public/tools/phase10y_shsrv_framing_model.py
T
Chimera GFX release export a6037502d7
phase0-ci / build-and-audit (push) Successful in 2m14s
Publish Chimera GFX source
2026-09-03 03:27:14 +02:00

205 lines
6.5 KiB
Python

#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
"""Offline source-family model for shsrv framing; never opens a transport."""
from __future__ import annotations
from dataclasses import dataclass
LEGACY_RAW = "LEGACY_RAW_V07_V08"
LIBTELNET_NVT = "LIBTELNET_NVT_V09_V019"
SOURCE_FAMILIES = {LEGACY_RAW, LIBTELNET_NVT}
MAX_MODEL_BYTES = 65_536
MAX_MODEL_CHUNKS = 256
IAC = 0xFF
SE = 0xF0
SB = 0xFA
WILL = 0xFB
WONT = 0xFC
DO = 0xFD
DONT = 0xFE
class FramingError(RuntimeError):
"""Fail-closed model error without input bytes."""
@dataclass(frozen=True)
class DecodedFraming:
application: bytes
negotiation_replies: tuple[bytes, ...]
iac_commands: tuple[int, ...]
source_family: str
source_model_only: bool = True
device_behavior_proven: bool = False
@dataclass(frozen=True)
class PromptAssessment:
candidate_offsets: tuple[int, ...]
ends_with_candidate: bool
classification: str
exact_completion_proven: bool = False
def initial_server_bytes(family: str) -> bytes:
"""Both audited families emit no proactive Telnet negotiation."""
_require_family(family)
return b""
def encode_server_text(family: str, application: bytes) -> bytes:
"""Model the source-family transformation from stdout to peer bytes."""
_require_family(family)
if not isinstance(application, bytes):
raise FramingError("application text must be bytes")
if len(application) > MAX_MODEL_BYTES:
raise FramingError("application text exceeds model limit")
if family == LEGACY_RAW:
return application
output = bytearray()
for value in application:
if value == IAC:
output.extend((IAC, IAC))
elif value == 0x0D:
output.extend((0x0D, 0x00))
elif value == 0x0A:
output.extend((0x0D, 0x0A))
else:
output.append(value)
return bytes(output)
class ClientWireDecoder:
"""Bounded incremental model of bytes received by the shell."""
def __init__(self, family: str) -> None:
_require_family(family)
self.family = family
self.state = "DATA"
self.negotiation_command: int | None = None
self.raw_bytes = 0
self.chunks = 0
self._application = bytearray()
self._replies: list[bytes] = []
self._iac_commands: list[int] = []
self._sealed = False
def _append(self, value: int) -> None:
self._application.append(value)
if len(self._application) > MAX_MODEL_BYTES:
raise FramingError("decoded application exceeds model limit")
def feed(self, chunk: bytes) -> None:
if self._sealed:
raise FramingError("model is sealed")
if not isinstance(chunk, bytes):
raise FramingError("wire chunk must be bytes")
if not chunk:
return
self.chunks += 1
self.raw_bytes += len(chunk)
if self.chunks > MAX_MODEL_CHUNKS:
raise FramingError("wire chunk limit exceeded")
if self.raw_bytes > MAX_MODEL_BYTES:
raise FramingError("wire byte limit exceeded")
if self.family == LEGACY_RAW:
self._application.extend(chunk)
return
for value in chunk:
self._feed_libtelnet(value)
def _feed_libtelnet(self, value: int) -> None:
if self.state == "DATA":
if value == IAC:
self.state = "IAC"
elif value == 0x0D:
self.state = "EOL"
else:
self._append(value)
elif self.state == "EOL":
if value == 0x0A:
self._append(0x0A)
else:
self._append(0x0D)
if value != 0x00:
self._append(value)
self.state = "DATA"
elif self.state == "IAC":
if value == IAC:
self._append(IAC)
self.state = "DATA"
elif value in {WILL, WONT, DO, DONT}:
self.negotiation_command = value
self.state = "NEGOTIATION"
elif value == SB:
self.state = "SUBNEGOTIATION_OPTION"
else:
self._iac_commands.append(value)
self.state = "DATA"
elif self.state == "NEGOTIATION":
command = self.negotiation_command
if command == WILL:
self._replies.append(bytes((IAC, DONT, value)))
elif command == DO:
self._replies.append(bytes((IAC, WONT, value)))
self.negotiation_command = None
self.state = "DATA"
elif self.state == "SUBNEGOTIATION_OPTION":
self.state = "SUBNEGOTIATION"
elif self.state == "SUBNEGOTIATION":
if value == IAC:
self.state = "SUBNEGOTIATION_IAC"
elif self.state == "SUBNEGOTIATION_IAC":
if value == SE:
self.state = "DATA"
elif value == IAC:
self.state = "SUBNEGOTIATION"
else:
self.state = "IAC"
self._feed_libtelnet(value)
else:
raise FramingError("unknown model state")
def finalize(self) -> DecodedFraming:
if self._sealed:
raise FramingError("model is sealed")
self._sealed = True
if self.family == LIBTELNET_NVT and self.state != "DATA":
raise FramingError("incomplete Telnet or NVT sequence")
return DecodedFraming(
application=bytes(self._application),
negotiation_replies=tuple(self._replies),
iac_commands=tuple(self._iac_commands),
source_family=self.family,
)
def assess_prompt_candidates(application: bytes) -> PromptAssessment:
"""Find source-shaped prompt suffixes without promoting them to proof."""
if not isinstance(application, bytes):
raise FramingError("application text must be bytes")
candidates: list[int] = []
start = 0
while True:
offset = application.find(b"$ ", start)
if offset < 0:
break
candidates.append(offset)
start = offset + 2
ends = application.endswith(b"$ ")
if not ends:
classification = "NO_TERMINAL_PROMPT_CANDIDATE"
elif len(candidates) == 1:
classification = "SOURCE_SHAPE_CANDIDATE_ONLY"
else:
classification = "AMBIGUOUS_PROMPT_CANDIDATES"
return PromptAssessment(tuple(candidates), ends, classification)
def _require_family(family: str) -> None:
if family not in SOURCE_FAMILIES:
raise FramingError("unknown source family")