4747from snap7 .connection import ISOTCPConnection
4848from .protocol import (
4949 FunctionCode ,
50- LegitimationId ,
5150 Opcode ,
5251 ProtocolVersion ,
5352 ElementID ,
6362logger = logging .getLogger (__name__ )
6463
6564
66- # V2 session-setup legitimation BLOB observed in TIA Portal V19 captures
67- # against an S7-1200 FW 4.2.2 (V1-initial firmware). 180 bytes; written to
68- # address 1830 alongside ServerSessionVersion (306) in a single
69- # SetMultiVariables. Without it the PLC drops the TCP connection silently
70- # after the setup write.
71- #
72- # Most of the bytes are opaque — likely identity/integrity material that
73- # TIA either pre-computes or signs with key material we don't have. The
74- # only field we know we have to make per-session is the embedded PLC OMS
75- # UUID at offset 32 (little-endian). Everything else is replayed verbatim
76- # from one TIA capture as a first-pass experiment; if a later test shows
77- # the PLC validates other byte ranges, we'll have to reverse those too.
78- #
79- # Reference capture: ``TIAPortalV19AccessibleDevices.pcapng`` frame 31
80- # (attached to PR #713 / issue #710).
81- _V2_SETUP_LEGITIMATION_BLOB_TEMPLATE = bytes .fromhex (
82- "ad de e1 fe b4 00 00 00 01 00 00 00 01 00 00 00"
83- "ce 9b 9f 3a 94 03 98 6b 01 01 00 00 00 00 00 00"
84- "00 00 00 00 00 00 00 00 10 01 00 00 00 00 00 00" # OMS UUID slot at offset 32
85- "3f c8 df c2 7c 03 7f d9 99 4c ea c7 e2 b9 bb 1a"
86- "53 be 2a cd 00 49 53 3a fc 0e 43 64 49 5a c2 8e"
87- "20 ce ef 14 09 fe b4 aa e8 1c 54 08 e4 53 4c 08"
88- "2d ed 4b e6 31 ff b4 c5 1e 3b 56 ee b3 d4 1a d9"
89- "4a 83 b7 bf ac 3c ec 28 9d 5c dd f9 2e 12 59 87"
90- "f1 03 c7 08 f1 0b dc e1 e9 40 63 b5 2b 84 dc 58"
91- "9b d1 4c b6 26 a1 16 36 12 22 8e 5b 3d da c0 a0"
92- "b8 37 97 76 2f b1 f1 bb b2 b0 fb 44 f4 6a 50 9e"
93- "03 42 d5 6f"
94- )
95- _V2_SETUP_LEGITIMATION_OMS_UUID_OFFSET = 32
96-
97-
98- def _build_v2_session_setup_legitimation_value (oms_session_uuid : bytes ) -> bytes :
99- """Build the typed value written to address 1830 during session setup.
100-
101- The PObject tree shape is:
102-
103- Struct(1800)
104- 1801: UDInt(0)
105- 1802: USInt(0)
106- 1803: Struct(1825)
107- 1826: ULInt (opaque, replayed)
108- 1827: UDInt(272)
109- 1828: UDInt(0)
110- 1804: Struct(1825)
111- 1826: ULInt (opaque, replayed)
112- 1827: UDInt(65793)
113- 1828: UDInt(0)
114- 1805: Blob(180 bytes — see template above; OMS UUID patched in LE)
115-
116- Args:
117- oms_session_uuid: 8-byte OMS session UUID parsed from the
118- CreateObject response (attribute 233 ``"01:HEX"`` string).
119-
120- Returns:
121- Encoded typed value (flags + datatype + struct body), ready to
122- append to a SetMultiVariables item.
123- """
124- if len (oms_session_uuid ) != 8 :
125- raise ValueError (f"OMS session UUID must be 8 bytes, got { len (oms_session_uuid )} " )
126-
127- blob = bytearray (_V2_SETUP_LEGITIMATION_BLOB_TEMPLATE )
128- blob [_V2_SETUP_LEGITIMATION_OMS_UUID_OFFSET : _V2_SETUP_LEGITIMATION_OMS_UUID_OFFSET + 8 ] = oms_session_uuid [::- 1 ]
129-
130- # PValue helpers. Each returns a complete typed value: flags + datatype
131- # + value bytes. _elem prepends the element-key VLQ for placement
132- # inside a struct.
133- def _elem (elem_id : int , typed_value : bytes ) -> bytes :
134- return encode_uint32_vlq (elem_id ) + typed_value
135-
136- def _udint (value : int ) -> bytes :
137- return bytes ([0x00 , DataType .UDINT ]) + encode_uint32_vlq (value )
138-
139- def _usint (value : int ) -> bytes :
140- return bytes ([0x00 , DataType .USINT , value & 0xFF ])
141-
142- def _ulint_raw (raw_vlq : bytes ) -> bytes :
143- return bytes ([0x00 , DataType .ULINT ]) + raw_vlq
144-
145- def _struct (struct_id : int , body : bytes ) -> bytes :
146- # Struct PValue: flags(0) + STRUCT + 4-byte ID + body + terminator(0)
147- return bytes ([0x00 , DataType .STRUCT ]) + struct .pack (">I" , struct_id ) + body + bytes ([0x00 ])
148-
149- def _blob (data : bytes ) -> bytes :
150- return bytes ([0x00 , DataType .BLOB ]) + encode_uint32_vlq (0 ) + encode_uint32_vlq (len (data )) + data
151-
152- # Opaque ULInt VLQs from the TIA reference capture. Each is a 9-byte
153- # VLQ. Treated as opaque payload rather than decoded numeric values
154- # since we don't know what they encode.
155- opaque_ulint_1 = bytes .fromhex ("de d0 cd b0 c8 fc 90 f3 1a" )
156- opaque_ulint_2 = bytes .fromhex ("b5 e6 80 b9 a1 ea bf 9b ce" )
157-
158- inner_1803 = _elem (1826 , _ulint_raw (opaque_ulint_1 )) + _elem (1827 , _udint (272 )) + _elem (1828 , _udint (0 ))
159- inner_1804 = _elem (1826 , _ulint_raw (opaque_ulint_2 )) + _elem (1827 , _udint (65793 )) + _elem (1828 , _udint (0 ))
160-
161- outer_body = b""
162- outer_body += _elem (1801 , _udint (0 ))
163- outer_body += _elem (1802 , _usint (0 ))
164- outer_body += _elem (1803 , _struct (1825 , inner_1803 ))
165- outer_body += _elem (1804 , _struct (1825 , inner_1804 ))
166- outer_body += _elem (1805 , _blob (bytes (blob )))
167-
168- return _struct (1800 , outer_body )
169-
170-
17165def _strip_paom_string_in_session_version (struct_bytes : bytes ) -> bytes :
17266 """Replace element 319 in a captured ServerSessionVersion struct with an empty WString.
17367
@@ -242,11 +136,13 @@ def __init__(
242136 self ._server_session_version : Optional [int ] = None
243137 self ._server_session_version_raw : Optional [bytes ] = None
244138 self ._session_setup_ok : bool = False
245- # PLC-provided 8-byte OMS session UUID (parsed from the
139+ # PLC-provided 8-byte public-key checksum (parsed from the
246140 # ObjectVariableTypeName "01:HEX" attribute in the CreateObject
247- # response). Required to build the V2 session-setup legitimation
248- # value on V1-initial S7-1200 firmware.
249- self ._oms_session_uuid : Optional [bytes ] = None
141+ # response). It identifies which Siemens RSA key the PLC expects
142+ # for the V2 SessionKey handshake. Captured for diagnostics and
143+ # future use; we don't generate the SessionKey blob ourselves
144+ # because we don't have the matching public key.
145+ self ._public_key_checksum : Optional [bytes ] = None
250146
251147 # V2+ IntegrityId tracking
252148 self ._integrity_id_read : int = 0
@@ -934,9 +830,12 @@ def _parse_create_object_response(self, payload: bytes) -> None:
934830
935831 elif attr_id == 233 :
936832 # ObjectVariableTypeName: a WString shaped "01:HEX",
937- # where HEX is the PLC's OMS session UUID encoded as
938- # 16 hex characters. Captured for the V2 session-setup
939- # legitimation value (address 1830).
833+ # where HEX is the 8-byte fingerprint of the Siemens
834+ # RSA public key the PLC expects for the V2 SessionKey
835+ # handshake (Wireshark s7comm-plus dissector calls
836+ # this "publickeychecksum"). Captured for diagnostics
837+ # and so future code can decide whether the matching
838+ # public key is available.
940839 if offset + 2 > len (payload ):
941840 break
942841 _flags = payload [offset ]
@@ -950,8 +849,8 @@ def _parse_create_object_response(self, payload: bytes) -> None:
950849 try :
951850 text = raw .decode ("utf-8" )
952851 if len (text ) == 19 and text [2 ] == ":" :
953- self ._oms_session_uuid = bytes .fromhex (text [3 :])
954- logger .info (f"OMS session UUID captured: { text } " )
852+ self ._public_key_checksum = bytes .fromhex (text [3 :])
853+ logger .info (f"Public key checksum captured: { text } " )
955854 except (UnicodeDecodeError , ValueError ):
956855 logger .debug (f"Unparseable ObjectVariableTypeName: { raw !r} " )
957856 else :
@@ -1070,7 +969,7 @@ def _skip_typed_value(self, data: bytes, offset: int, datatype: int, flags: int)
1070969 return offset
1071970
1072971 def _setup_session (self ) -> bool :
1073- """Send V2 SetMultiVariables to complete the session handshake .
972+ """Send V2 SetMultiVariables to echo ServerSessionVersion back to the PLC .
1074973
1075974 Always uses V2 framing (`72 02 ...`), transport flags 0x34, and no
1076975 IntegrityId — these are the established session-setup conventions
@@ -1080,16 +979,16 @@ def _setup_session(self) -> bool:
1080979 Without this step, the PLC rejects all subsequent data operations
1081980 with ERROR2 (0x05A9).
1082981
1083- Two-item form ( V1-initial S7-1200, FW < 4.07 ): writes both
1084- ``SESSION_SETUP_LEGITIMATION `` ( 1830) and ``SERVER_SESSION_VERSION``
1085- (306) in one SetMultiVariables. TIA Portal V19 does this on FW 4.2.2
1086- captures; without the legitimation item the PLC closes the TCP
1087- connection silently right after the setup write. We use the two-item
1088- form whenever the PLC sent us its OMS session UUID in the
1089- CreateObject response (a V1-initial-firmware behaviour).
1090-
1091- One-item form (everything else): just the ``SERVER_SESSION_VERSION``
1092- echo, matching the C# driver's ``SetSessionSetupData`` .
982+ Note on V1-initial S7-1200 ( FW < 4.5 ): TIA Portal V19 also writes a
983+ ``SessionKey `` value at address 1830 in the same frame, carrying an
984+ RSA-encrypted random seed and an AES-CBC-encrypted challenge.
985+ Without that key, the PLC drops the connection right after this
986+ write — so CommPlus data ops (incl. ``browse()``) never come up on
987+ V1-initial firmware. The unified client transparently falls back to
988+ legacy PUT/GET in that case. See PR #713 / issue #710 / issue #712
989+ for the diagnosis (Wireshark s7comm-plus dissector confirmed
990+ Struct(1800) is ``StructSecurityKey``; we don't have Siemens'
991+ public keys to generate the encrypted seed) .
1093992
1094993 Returns:
1095994 True if session setup succeeded (return_value == 0).
@@ -1112,36 +1011,23 @@ def _setup_session(self) -> bool:
11121011 0x34 ,
11131012 )
11141013
1014+ payload = bytearray ()
1015+ payload += struct .pack (">I" , self ._session_id ) # InObjectId
1016+ payload += encode_uint32_vlq (1 ) # ItemCount
1017+ payload += encode_uint32_vlq (1 ) # AddressCount
1018+ payload += encode_uint32_vlq (ObjectId .SERVER_SESSION_VERSION ) # Address: 306
1019+ payload += encode_uint32_vlq (1 ) # ItemNumber
1020+
11151021 if self ._server_session_version_raw is not None :
11161022 # Echo the Struct(314) value, but strip element 319 (the device
11171023 # PAOM string) to empty. TIA Portal does this; writing the PLC's
11181024 # own identity back appears to be rejected — the V1-initial
11191025 # S7-1200 silently drops the connection if we don't strip it.
1120- session_version_value = _strip_paom_string_in_session_version (self ._server_session_version_raw )
1026+ payload + = _strip_paom_string_in_session_version (self ._server_session_version_raw )
11211027 else :
11221028 # Test/emulator path: scalar UDInt fallback.
1123- session_version_value = bytes ([0x00 , DataType .UDINT ]) + encode_uint32_vlq (self ._server_session_version or 0 )
1124-
1125- oms_uuid = self ._oms_session_uuid
1126-
1127- payload = bytearray ()
1128- payload += struct .pack (">I" , self ._session_id ) # InObjectId
1129-
1130- if oms_uuid is not None :
1131- payload += encode_uint32_vlq (2 ) # ItemCount
1132- payload += encode_uint32_vlq (2 ) # AddressCount
1133- payload += encode_uint32_vlq (LegitimationId .SESSION_SETUP_LEGITIMATION ) # 1830
1134- payload += encode_uint32_vlq (ObjectId .SERVER_SESSION_VERSION ) # 306
1135- payload += encode_uint32_vlq (1 ) # ItemNumber 1
1136- payload += _build_v2_session_setup_legitimation_value (oms_uuid )
1137- payload += encode_uint32_vlq (2 ) # ItemNumber 2
1138- payload += session_version_value
1139- else :
1140- payload += encode_uint32_vlq (1 ) # ItemCount
1141- payload += encode_uint32_vlq (1 ) # AddressCount
1142- payload += encode_uint32_vlq (ObjectId .SERVER_SESSION_VERSION ) # 306
1143- payload += encode_uint32_vlq (1 ) # ItemNumber
1144- payload += session_version_value
1029+ payload += bytes ([0x00 , DataType .UDINT ])
1030+ payload += encode_uint32_vlq (self ._server_session_version or 0 )
11451031
11461032 payload += bytes ([0x00 ]) # Fill byte
11471033 payload += encode_object_qualifier ()
0 commit comments