Skip to content

Commit a186bf5

Browse files
gijzelaerrclaude
andcommitted
fix(tls): allow TLS 1.2 for S7-1500 PLCs (#760)
The TLS context forced minimum_version to TLS 1.3, but S7-1500 PLCs with firmware < V3.0 (e.g. 1512SP-1 FW V4.1.2) only support TLS 1.2. The ClientHello offered only TLS 1.3 cipher suites and versions, so the PLC rejected the connection with a TCP RST. Lower the minimum to TLS 1.2 so Python's ssl module auto-negotiates the highest mutually-supported version. Also surface the CreateObject return value: when non-zero the PLC is rejecting the session (typically because TLS is required), but the old code silently fell through to the misleading "V1-initial SessionKey" warning. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7637ab2 commit a186bf5

3 files changed

Lines changed: 14 additions & 9 deletions

File tree

s7/_s7commplus_async_client.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -821,13 +821,16 @@ async def _create_session(self) -> None:
821821
# Parse response body: ReturnValue(VLQ) + ObjectIdCount + ObjectIds(VLQ).
822822
# The usable session id is ObjectIds[0] (NOT the header SessionId field).
823823
body = response[14:]
824-
object_ids, obj_end = parse_create_object_session_id(body)
824+
object_ids, obj_end, return_value = parse_create_object_session_id(body)
825825
if object_ids:
826826
self._session_id = object_ids[0]
827827
else:
828828
self._session_id = struct.unpack_from(">I", response, 9)[0]
829829
self._protocol_version = version
830830

831+
if return_value != 0:
832+
logger.warning(f"CreateObject returned error 0x{return_value:X} — PLC may require TLS (use_tls=True)")
833+
831834
self._server_session_version = parse_server_session_version(response[14 + obj_end :])
832835
if self._server_session_version is not None:
833836
logger.info(f"ServerSessionVersion captured: {len(self._server_session_version)} bytes")

s7/codec.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -612,15 +612,15 @@ def skip_typed_value(data: bytes, offset: int, datatype: int, flags: int) -> int
612612
return offset
613613

614614

615-
def parse_create_object_session_id(body: bytes) -> tuple[list[int], int]:
615+
def parse_create_object_session_id(body: bytes) -> tuple[list[int], int, int]:
616616
"""Parse a CreateObject response body (after the 14-byte response header).
617617
618618
Body layout: ReturnValue (UInt64 VLQ) + ObjectIdCount (1 byte) + ObjectIds (UInt32 VLQ
619619
each). The usable session id is ``ObjectIds[0]`` (not the header SessionId field).
620620
621-
Returns ``(object_ids, offset)`` where ``offset`` points just past the ObjectIds.
621+
Returns ``(object_ids, offset, return_value)`` where ``offset`` points just past the ObjectIds.
622622
"""
623-
_return_value, consumed = decode_uint64_vlq(body, 0)
623+
return_value, consumed = decode_uint64_vlq(body, 0)
624624
boff = consumed
625625
obj_count = body[boff] if boff < len(body) else 0
626626
boff += 1
@@ -629,7 +629,7 @@ def parse_create_object_session_id(body: bytes) -> tuple[list[int], int]:
629629
oid, c = decode_uint32_vlq(body, boff)
630630
boff += c
631631
object_ids.append(oid)
632-
return object_ids, boff
632+
return object_ids, boff, return_value
633633

634634

635635
def parse_server_session_version(payload: bytes) -> Optional[bytes]:

s7/connection.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -759,7 +759,7 @@ def _create_session(self) -> None:
759759
# Parse response body: ReturnValue(UInt64 VLQ) + ObjectIdCount + ObjectIds(VLQ).
760760
# The usable session id is ObjectIds[0] (NOT the header SessionId field).
761761
body = response[14:]
762-
object_ids, obj_end = parse_create_object_session_id(body)
762+
object_ids, obj_end, return_value = parse_create_object_session_id(body)
763763
if object_ids:
764764
self._session_id = object_ids[0]
765765
else:
@@ -778,6 +778,9 @@ def _create_session(self) -> None:
778778
logger.debug(f"CreateObject response payload: {response[14:].hex(' ')}")
779779
logger.debug(f"Session created: id=0x{self._session_id:08X} ({self._session_id}), version=V{version}")
780780

781+
if return_value != 0:
782+
logger.warning(f"CreateObject returned error 0x{return_value:X} — PLC may require TLS (use_tls=True)")
783+
781784
# Parse response payload to extract ServerSessionVersion
782785
self._server_session_version = parse_server_session_version(response[14 + obj_end :])
783786
if self._server_session_version is not None:
@@ -1020,12 +1023,11 @@ def _setup_ssl_context(
10201023
Configured SSLContext
10211024
"""
10221025
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
1023-
ctx.minimum_version = ssl.TLSVersion.TLSv1_3
1026+
# S7-1500 FW < V3.0 only supports TLS 1.2; newer firmware negotiates 1.3.
1027+
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
10241028

1025-
# TLS 1.3 ciphersuites are configured differently from TLS 1.2
10261029
if hasattr(ctx, "set_ciphersuites"):
10271030
ctx.set_ciphersuites("TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256")
1028-
# If set_ciphersuites not available, TLS 1.3 uses its mandatory defaults
10291031

10301032
if cert_path and key_path:
10311033
ctx.load_cert_chain(cert_path, key_path)

0 commit comments

Comments
 (0)