RBC_Test_Sim_Core 0.1.0 build 51fd23e
Shared transport-layer plumbing for the ADR-029 Train/IL/CTC RBC simulators
Loading...
Searching...
No Matches
saf.py
Go to the documentation of this file.
1"""SaF - Python mirror of the safe-communication Safety Functional layer.
2
3Byte-identical to ``Platform_Protocol_SafeComm`` (``src/mac/mac.c``, ``src/saf/saf.c``), the same way
4``simcore.rbc_wire`` mirrors the C wire codec. The construction follows Subset-037-2 v4.0.0
5("EuroRadio FIS - Safety Layer") directly:
6
7* DES, pure Python so a sim needs no crypto package.
8* CBC-MAC (Subset-037-2 SS6.2.2, "single DES with modified MAC algorithm 3"): every block but the last is
9 single-DES under K1; the last block alone gets E(K3, D(K2, E(K1, .))). ISO/IEC 9797-1 Padding Method 1
10 (zero bits, no extra block when the message is already a multiple of 64 bits). Verified against the
11 official worked example in Subset-037-2 Annex B.
12* KSMAC derivation (SS6.2.3.2.3.13-16): RA and RB are each split into 32-bit left/right halves; KS1/KS2 use
13 the normal key order over the left/right halves, KS3 reuses the left half with the key order reversed.
14* Frames AU1 (13) / AU2 (21) / AU3 (9) / AR (9) / DT (1+n+8) / DI (3, unauthenticated) per Subset-037-2
15 SS6.2.5's tables: a packed ETY|MTI|DF header octet and 24-bit ETCS IDs. The MAC input for AU2/AU3/DT/AR
16 follows SS6.2.3.2.1 (message origin authentication: length|DA|body) and SS6.2.3.2.2/Figure 6 (peer entity
17 authentication) - DA is always the *receiver's own* identity.
18* Sa-DISCONNECT carries no MAC or identity field at all (Table 6/13): SS6.2.3.3.3.2 says release-phase
19 authentication is not required, since a forged one can only end the connection early, never inject or
20 alter safety data. Any well-formed DI is honoured.
21
22One deliberate, non-conformant addition: the official DT SaPDU carries no sequence number (Table 12) - that
23defence is the Subset-098 SAI sub-layer's job (sequence numbering / Triple Time Stamping / EC counter,
24SS5.4), which this module does not implement. In its place a 4-byte sequence number is carried inside the
25MAC-covered user-data area, matching what ``saf.c`` does. This is a project extension, not itself
26conformant to any subset - see ``docs/safety/SUBSET_MAPPING.md`` in the SafeComm library.
27
28The session is transport-agnostic: the caller supplies ``send(bytes)`` and ``recv() -> bytes`` (one
29frame per call). ``recv`` raises ``TimeoutError`` when nothing arrived in time.
30
31This is a test tool. The MAC is cryptographically dated (see the C library's SUBSET_MAPPING.md).
32"""
33import os
34
35# ---------------------------------------------------------------------------------------------------
36# DES
37# ---------------------------------------------------------------------------------------------------
38_IP = (58, 50, 42, 34, 26, 18, 10, 2, 60, 52, 44, 36, 28, 20, 12, 4,
39 62, 54, 46, 38, 30, 22, 14, 6, 64, 56, 48, 40, 32, 24, 16, 8,
40 57, 49, 41, 33, 25, 17, 9, 1, 59, 51, 43, 35, 27, 19, 11, 3,
41 61, 53, 45, 37, 29, 21, 13, 5, 63, 55, 47, 39, 31, 23, 15, 7)
42_FP = (40, 8, 48, 16, 56, 24, 64, 32, 39, 7, 47, 15, 55, 23, 63, 31,
43 38, 6, 46, 14, 54, 22, 62, 30, 37, 5, 45, 13, 53, 21, 61, 29,
44 36, 4, 44, 12, 52, 20, 60, 28, 35, 3, 43, 11, 51, 19, 59, 27,
45 34, 2, 42, 10, 50, 18, 58, 26, 33, 1, 41, 9, 49, 17, 57, 25)
46_E = (32, 1, 2, 3, 4, 5, 4, 5, 6, 7, 8, 9, 8, 9, 10, 11, 12, 13,
47 12, 13, 14, 15, 16, 17, 16, 17, 18, 19, 20, 21, 20, 21, 22, 23, 24, 25,
48 24, 25, 26, 27, 28, 29, 28, 29, 30, 31, 32, 1)
49_P = (16, 7, 20, 21, 29, 12, 28, 17, 1, 15, 23, 26, 5, 18, 31, 10,
50 2, 8, 24, 14, 32, 27, 3, 9, 19, 13, 30, 6, 22, 11, 4, 25)
51_PC1 = (57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18,
52 10, 2, 59, 51, 43, 35, 27, 19, 11, 3, 60, 52, 44, 36,
53 63, 55, 47, 39, 31, 23, 15, 7, 62, 54, 46, 38, 30, 22,
54 14, 6, 61, 53, 45, 37, 29, 21, 13, 5, 28, 20, 12, 4)
55_PC2 = (14, 17, 11, 24, 1, 5, 3, 28, 15, 6, 21, 10, 23, 19, 12, 4,
56 26, 8, 16, 7, 27, 20, 13, 2, 41, 52, 31, 37, 47, 55, 30, 40,
57 51, 45, 33, 48, 44, 49, 39, 56, 34, 53, 46, 42, 50, 36, 29, 32)
58_SHIFTS = (1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1)
59_SBOX = (
60 (14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7,
61 0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8,
62 4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0,
63 15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13),
64 (15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10,
65 3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5,
66 0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15,
67 13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9),
68 (10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8,
69 13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1,
70 13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7,
71 1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12),
72 (7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15,
73 13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9,
74 10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4,
75 3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14),
76 (2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9,
77 14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6,
78 4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14,
79 11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3),
80 (12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11,
81 10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8,
82 9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6,
83 4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13),
84 (4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1,
85 13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6,
86 1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2,
87 6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12),
88 (13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7,
89 1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2,
90 7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8,
91 2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11),
92)
93
94
95def _permute(value, table, width):
96 """Bit permutation: ``table`` lists 1-based source bit positions, MSB first."""
97 out = 0
98 for pos in table:
99 out = (out << 1) | ((value >> (width - pos)) & 1)
100 return out
101
102
103def _des_subkeys(key8):
104 k = _permute(int.from_bytes(key8, "big"), _PC1, 64)
105 c, d = k >> 28, k & 0xFFFFFFF
106 keys = []
107 for shift in _SHIFTS:
108 c = ((c << shift) | (c >> (28 - shift))) & 0xFFFFFFF
109 d = ((d << shift) | (d >> (28 - shift))) & 0xFFFFFFF
110 keys.append(_permute((c << 28) | d, _PC2, 56))
111 return keys
112
113
114def _feistel(r, subkey):
115 x = _permute(r, _E, 32) ^ subkey
116 out = 0
117 for i in range(8):
118 chunk = (x >> (42 - 6 * i)) & 0x3F
119 row = ((chunk & 0x20) >> 4) | (chunk & 1)
120 col = (chunk >> 1) & 0xF
121 out = (out << 4) | _SBOX[i][row * 16 + col]
122 return _permute(out, _P, 32)
123
124
125def _des_block(block8, subkeys):
126 b = _permute(int.from_bytes(block8, "big"), _IP, 64)
127 left, right = b >> 32, b & 0xFFFFFFFF
128 for sk in subkeys:
129 left, right = right, left ^ _feistel(right, sk)
130 return _permute((right << 32) | left, _FP, 64).to_bytes(8, "big")
131
132
133def des_encrypt_block(key8, block8):
134 """Single DES, one 8-byte block."""
135 return _des_block(block8, _des_subkeys(key8))
136
137
138def des_decrypt_block(key8, block8):
139 """Single DES decrypt, one 8-byte block."""
140 return _des_block(block8, _des_subkeys(key8)[::-1])
141
142
143def _algo3(key24, block8):
144 """Subset-037-2 SS6.2.2.1 "single DES with modified MAC algorithm 3": E(K3, D(K2, E(K1, block)))."""
145 t = des_encrypt_block(key24[0:8], block8)
146 t = des_decrypt_block(key24[8:16], t)
147 return des_encrypt_block(key24[16:24], t)
148
149
150def _algo3_reversed(key24, block8):
151 """The same construction with K1 and K3 swapped: E(K1, D(K2, E(K3, block))). Used only for KS3."""
152 t = des_encrypt_block(key24[16:24], block8)
153 t = des_decrypt_block(key24[8:16], t)
154 return des_encrypt_block(key24[0:8], t)
155
156
157# ---------------------------------------------------------------------------------------------------
158# KSMAC and CBC-MAC (Subset-037-2 SS6.2.2 / SS6.2.3.2.3)
159# ---------------------------------------------------------------------------------------------------
160MAC_LEN = 8
161RAND_LEN = 8
162KEY_LEN = 24
163
164
165def derive_ksmac(kmac, ra, rb):
166 """Session key (SS6.2.3.2.3.13-16): RA = RA_L|RA_R, RB = RB_L|RB_R (32-bit halves).
167 KS1 = algo3(RA_L|RB_L), KS2 = algo3(RA_R|RB_R), KS3 = algo3_reversed(RA_L|RB_L)."""
168 if len(kmac) != KEY_LEN or len(ra) != RAND_LEN or len(rb) != RAND_LEN:
169 raise ValueError("kmac 24 bytes, ra and rb 8 bytes")
170 block_l = ra[0:4] + rb[0:4]
171 block_r = ra[4:8] + rb[4:8]
172 return _algo3(kmac, block_l) + _algo3(kmac, block_r) + _algo3_reversed(kmac, block_l)
173
174
175def compute_mac(ksmac, msg):
176 """CBC-MAC (SS6.2.2): every block but the last under K1 only; the last block via _algo3. ISO/IEC 9797-1
177 Padding Method 1 (zero bits), no extra block when len(msg) is already a multiple of 8."""
178 padded_len = len(msg) if (len(msg) % 8 == 0) else (len(msg) + (8 - len(msg) % 8))
179 padded_len = padded_len or 8
180 padded = bytes(msg) + bytes(padded_len - len(msg))
181 nblocks = padded_len // 8
182 state = bytes(8)
183 for i in range(nblocks):
184 block = padded[i * 8:(i + 1) * 8]
185 state = bytes(a ^ b for a, b in zip(state, block))
186 if i < nblocks - 1:
187 state = des_encrypt_block(ksmac[0:8], state)
188 else:
189 state = _algo3(ksmac, state)
190 return state
191
192
193def _equal_ct(a, b):
194 if len(a) != len(b):
195 return False
196 diff = 0
197 for x, y in zip(a, b):
198 diff |= x ^ y
199 return diff == 0
200
201
202def mac_over_da_body(ksmac, da_id, body):
203 """[length(2, BE) | DA(3) | body], MACed (SS6.2.3.2.1.1 "Procedure 1"). compute_mac() zero-pads itself."""
204 length = 3 + len(body)
205 buf = length.to_bytes(2, "big") + _pack_id24(da_id) + bytes(body)
206 return compute_mac(ksmac, buf)
207
208
209# ---------------------------------------------------------------------------------------------------
210# PDU framing (Subset-037-2 SS6.2.5)
211# ---------------------------------------------------------------------------------------------------
212MTI_AU1, MTI_AU2, MTI_AU3, MTI_DT, MTI_DI, MTI_AR = 1, 2, 3, 5, 8, 9
213SEQ_WINDOW = 8
214MAX_PDU = 1024
215
216
217def _pack_id24(id_):
218 return ((id_ & 0xFFFFFF)).to_bytes(3, "big")
219
220
222 return int.from_bytes(buf, "big")
223
224
225def _pack_header(ety, mti, df):
226 return bytes([((ety & 0x7) << 5) | ((mti & 0xF) << 1) | (df & 1)])
227
228
229def _header_mti(header_byte):
230 return (header_byte >> 1) & 0xF
231
232
233def _header_df(header_byte):
234 return header_byte & 1
235
236
237class SafError(Exception):
238 """A SaF failure; ``status`` names the matching C ``SAFECOMM_STATUS_*`` value."""
239
240 def __init__(self, status, detail=""):
241 super().__init__(f"{status}{': ' + detail if detail else ''}")
242 self.status = status
243
244
245IDLE, WAIT_AU2, WAIT_AU3, WAIT_AR, DATA = "IDLE", "WAIT_AU2", "WAIT_AU3", "WAIT_AR", "DATA"
246
247
249 """One SaF connection. ``role`` is "initiator" or "responder".
250
251 ``send(frame: bytes)`` transmits one frame; ``recv() -> bytes`` returns one frame or raises
252 ``TimeoutError``. ``clock()`` returns milliseconds (default: monotonic). ``rng(n)`` returns n random
253 bytes (default ``os.urandom``); tests inject a fixed one. ``ety`` is the 3-bit ETCS ID type
254 (Subset-037-2 Table 8: 1=RBC, 2=Engine, ...).
255 """
256
257 def __init__(self, role, id_self, id_peer, kmac, send, recv, *, ety=0, t_connect_ms=0,
258 t_supervision_ms=0, clock=None, rng=None):
259 if role not in ("initiator", "responder"):
260 raise ValueError("role must be initiator or responder")
261 if len(kmac) != KEY_LEN:
262 raise ValueError("kmac must be 24 bytes")
263 self.role, self.id_self, self.id_peer, self.ety = role, id_self, id_peer, ety
264 self._kmac, self._send, self._recv = bytes(kmac), send, recv
265 self.t_connect_ms, self.t_supervision_ms = t_connect_ms, t_supervision_ms
266 self._clock = clock or _monotonic_ms
267 self._rng = rng or os.urandom
268 self.state = IDLE
269 self.seq_tx = self.seq_rx = 0
270 self.ksmac = None
271 self.ra = self.rb = None
272 self._last_rx_ms = 0
274
275 # -- helpers ------------------------------------------------------------------------------------
276 def _fail(self, status, detail=""):
277 self.state = IDLE
278 raise SafError(status, detail)
279
280 def _recv_frame(self):
281 try:
282 return self._recv()
283 except TimeoutError:
284 self._fail("TIMEOUT", "no frame")
285
287 return (self._clock(), self.t_connect_ms) if self.t_connect_ms else None
288
289 def _check_deadline(self, deadline):
290 if deadline is not None and (self._clock() - deadline[0]) & 0xFFFFFFFF > deadline[1]:
291 self._fail("TIMEOUT", "connect timeout")
292
293 # -- handshake (SS6.2.4.2 / Figure 6, initiator = "B", responder = "A") -------------------------
294 def connect(self):
295 """Run AU1-AU2-AU3-AR. Raises ``SafError`` (state returns to IDLE) on any failure or timeout."""
296 deadline = self._connect_deadline()
297 if self.role == "initiator":
298 self._connect_initiator(deadline)
299 else:
300 self._connect_responder(deadline)
301 self.seq_tx = self.seq_rx = 1
302 self.state = DATA
303 self._last_rx_ms = self._clock()
304
305 def _connect_initiator(self, deadline):
306 self._send(self.initiator_start())
307 au2 = self._recv_frame()
308 self._check_deadline(deadline)
309 self._send(self.initiator_on_frame(au2))
310 self._check_deadline(deadline)
311 ar = self._recv_frame()
312 self._check_deadline(deadline)
313 self.initiator_on_frame(ar)
314
315 # -- frame-driven initiator: for a caller that cannot block (e.g. a sim whose receive loop serves many trains).
316 # Same checks as connect(); each call consumes one received frame and returns the next frame to send, if any.
318 """Begin a handshake: returns AU1 to send; the session waits for AU2."""
319 if self.role != "initiator":
320 raise SafError("STATE", "not an initiator")
321 self.state = WAIT_AU2
322 self.ra = self._rng(RAND_LEN)
323 self._handshake_start_ms = self._clock()
324 return _pack_header(self.ety, MTI_AU1, 0) + _pack_id24(self.id_self) + b"\x01" + self.ra
325
327 """True while a handshake is in progress and has run longer than ``t_connect_ms`` (0 = never)."""
328 return (self.state in (WAIT_AU2, WAIT_AR) and bool(self.t_connect_ms)
329 and (self._clock() - self._handshake_start_ms) & 0xFFFFFFFF > self.t_connect_ms)
330
331 def initiator_on_frame(self, frame):
332 """AU2 -> returns AU3 (waits for AR); AR -> returns None (now DATA). Raises ``SafError`` (to IDLE)."""
333 if self.state == WAIT_AU2:
334 if len(frame) != 21 or _header_mti(frame[0]) != MTI_AU2 or _header_df(frame[0]) != 1:
335 self._fail("AUTH_FAILED", "AU2 shape")
336 if _unpack_id24(frame[1:4]) != self.id_peer:
337 self._fail("AUTH_FAILED", "AU2 sender")
338 self.rb = frame[5:13]
339 self.ksmac = derive_ksmac(self._kmac, self.ra, self.rb)
340 mac_input = (8 .to_bytes(2, "big") + _pack_id24(self.id_self) + frame[0:5] + self.rb + self.ra
341 + _pack_id24(self.id_self))
342 if not _equal_ct(compute_mac(self.ksmac, mac_input), frame[13:21]):
343 self._fail("AUTH_FAILED", "AU2 MAC")
344 au3_header = _pack_header(0, MTI_AU3, 0)
345 rmi = 4 .to_bytes(2, "big") + _pack_id24(self.id_peer) + au3_header + self.ra + self.rb
346 self.state = WAIT_AR
347 return au3_header + compute_mac(self.ksmac, rmi)
348 if self.state == WAIT_AR:
349 if len(frame) != 9 or _header_mti(frame[0]) != MTI_AR or _header_df(frame[0]) != 1:
350 self._fail("AUTH_FAILED", "AR shape")
351 if not _equal_ct(mac_over_da_body(self.ksmac, self.id_self, frame[0:1]), frame[1:9]):
352 self._fail("AUTH_FAILED", "AR MAC")
353 self.seq_tx = self.seq_rx = 1
354 self.state = DATA
355 self._last_rx_ms = self._clock()
356 return None
357 raise SafError("STATE", "no handshake in progress")
358
359 def _connect_responder(self, deadline):
360 au1 = self._recv_frame()
361 self._check_deadline(deadline)
362 if len(au1) != 13 or _header_mti(au1[0]) != MTI_AU1 or _header_df(au1[0]) != 0:
363 self._fail("AUTH_FAILED", "AU1 shape")
364 if _unpack_id24(au1[1:4]) != self.id_peer:
365 self._fail("AUTH_FAILED", "AU1 sender")
366 self.ra = au1[5:13]
367 self.state = WAIT_AU3
368 self.rb = self._rng(RAND_LEN)
369 self.ksmac = derive_ksmac(self._kmac, self.ra, self.rb)
370
371 au2_body = _pack_header(self.ety, MTI_AU2, 1) + _pack_id24(self.id_self) + b"\x01" + self.rb
372 mac_input = (8 .to_bytes(2, "big") + _pack_id24(self.id_peer) + au2_body[0:5]
373 + self.rb + self.ra + _pack_id24(self.id_peer))
374 au2 = au2_body + compute_mac(self.ksmac, mac_input)
375 self._send(au2)
376
377 au3 = self._recv_frame()
378 self._check_deadline(deadline)
379 if len(au3) != 9 or _header_mti(au3[0]) != MTI_AU3 or _header_df(au3[0]) != 0:
380 self._fail("AUTH_FAILED", "AU3 shape")
381 rmi = 4 .to_bytes(2, "big") + _pack_id24(self.id_self) + au3[0:1] + self.ra + self.rb
382 if not _equal_ct(compute_mac(self.ksmac, rmi), au3[1:9]):
383 self._fail("AUTH_FAILED", "AU3 MAC")
384
385 ar_header = _pack_header(0, MTI_AR, 1)
386 ar = ar_header + mac_over_da_body(self.ksmac, self.id_peer, ar_header)
387 self._send(ar)
388
389 # -- data (Sa-DATA / DT, SS6.2.5.3, with the project's own sequence extension - see module docstring) --
390 def build_frame(self, pdu):
391 """The DT frame for ``pdu`` at the current transmit sequence number (does not advance it)."""
392 if self.state != DATA:
393 raise SafError("STATE", "not connected")
394 if len(pdu) > MAX_PDU:
395 raise SafError("INVALID_PARAM", "pdu too long")
396 df = 0 if self.role == "initiator" else 1
397 body = _pack_header(0, MTI_DT, df) + self.seq_tx.to_bytes(4, "big") + bytes(pdu)
398 return body + mac_over_da_body(self.ksmac, self.id_peer, body)
399
400 def send_pdu(self, pdu):
401 frame = self.build_frame(pdu)
402 self._send(frame)
403 self.seq_tx = (self.seq_tx + 1) & 0xFFFFFFFF
404
405 def recv_pdu(self):
406 """Receive one authenticated PDU. Any protection failure returns the session to IDLE."""
407 if self.state != DATA:
408 raise SafError("STATE", "not connected")
409 try:
410 frame = self._recv()
411 except TimeoutError:
412 raise SafError("TIMEOUT", "no frame") from None
413 return self.accept_frame(frame)
414
415 def accept_frame(self, frame):
416 """Verify one already-received Sa-DATA / Sa-DISCONNECT frame and return its PDU (``recv_pdu``'s checks)."""
417 if self.state != DATA:
418 raise SafError("STATE", "not connected")
419 if len(frame) < 1:
420 self._fail("TRANSPORT", "empty frame")
421 if _header_mti(frame[0]) == MTI_DI:
422 # Sa-DISCONNECT: no MAC, no identity field (SS6.2.3.3.3.2). Any well-formed one is honoured.
423 if len(frame) != 3:
424 raise SafError("TRANSPORT", "malformed disconnect")
425 self.state = IDLE
426 raise SafError("PEER_DISCONNECT")
427 expected_df = 1 if self.role == "initiator" else 0
428 if _header_mti(frame[0]) != MTI_DT or len(frame) < (1 + 4 + MAC_LEN):
429 self._fail("STATE", "unexpected pdu type")
430 if _header_df(frame[0]) != expected_df:
431 self._fail("AUTH_FAILED", "direction flag (reflection)")
432 body = frame[:-MAC_LEN]
433 if not _equal_ct(mac_over_da_body(self.ksmac, self.id_self, body), frame[-MAC_LEN:]):
434 self._fail("MAC_MISMATCH")
435 rx_seq = int.from_bytes(frame[1:5], "big")
436 if (rx_seq - self.seq_rx) & 0xFFFFFFFF > SEQ_WINDOW:
437 self._fail("SEQ_ERROR", f"got {rx_seq}, expected {self.seq_rx}..+{SEQ_WINDOW}")
438 self.seq_rx = (rx_seq + 1) & 0xFFFFFFFF
439 self._last_rx_ms = self._clock()
440 return body[5:]
441
442 def supervise(self):
443 """Call periodically: drop the session when nothing valid arrived within ``t_supervision_ms``."""
444 if self.state != DATA:
445 raise SafError("STATE", "not connected")
446 if self.t_supervision_ms and (self._clock() - self._last_rx_ms) & 0xFFFFFFFF > self.t_supervision_ms:
447 self._fail("TIMEOUT", "supervision")
448
449 def disconnect(self):
450 """Send an unauthenticated Sa-DISCONNECT (reason 0, sub-reason 0) and go to IDLE."""
451 if self.state == DATA:
452 df = 0 if self.role == "initiator" else 1
453 frame = _pack_header(0, MTI_DI, df) + b"\x00\x00"
454 try:
455 self._send(frame)
456 except OSError:
457 pass
458 self.state = IDLE
459
460
462 import time
463 return int(time.monotonic() * 1000) & 0xFFFFFFFF
__init__(self, status, detail="")
Definition saf.py:240
_check_deadline(self, deadline)
Definition saf.py:289
build_frame(self, pdu)
Definition saf.py:390
_connect_initiator(self, deadline)
Definition saf.py:305
_connect_responder(self, deadline)
Definition saf.py:359
__init__(self, role, id_self, id_peer, kmac, send, recv, *, ety=0, t_connect_ms=0, t_supervision_ms=0, clock=None, rng=None)
Definition saf.py:258
initiator_on_frame(self, frame)
Definition saf.py:331
initiator_start(self)
Definition saf.py:317
_connect_deadline(self)
Definition saf.py:286
send_pdu(self, pdu)
Definition saf.py:400
handshake_expired(self)
Definition saf.py:326
accept_frame(self, frame)
Definition saf.py:415
_fail(self, status, detail="")
Definition saf.py:276
_header_df(header_byte)
Definition saf.py:233
_des_subkeys(key8)
Definition saf.py:103
_algo3(key24, block8)
Definition saf.py:143
_header_mti(header_byte)
Definition saf.py:229
_pack_header(ety, mti, df)
Definition saf.py:225
_algo3_reversed(key24, block8)
Definition saf.py:150
_permute(value, table, width)
Definition saf.py:95
_pack_id24(id_)
Definition saf.py:217
des_encrypt_block(key8, block8)
Definition saf.py:133
compute_mac(ksmac, msg)
Definition saf.py:175
_unpack_id24(buf)
Definition saf.py:221
derive_ksmac(kmac, ra, rb)
Definition saf.py:165
_des_block(block8, subkeys)
Definition saf.py:125
des_decrypt_block(key8, block8)
Definition saf.py:138
_equal_ct(a, b)
Definition saf.py:193
_monotonic_ms()
Definition saf.py:461
_feistel(r, subkey)
Definition saf.py:114
mac_over_da_body(ksmac, da_id, body)
Definition saf.py:202