Skip to content

SessionKey handshake for V1-initial S7-1200 PLCs#775

Open
gijzelaerr wants to merge 9 commits into
masterfrom
fix-tls-layering-v2
Open

SessionKey handshake for V1-initial S7-1200 PLCs#775
gijzelaerr wants to merge 9 commits into
masterfrom
fix-tls-layering-v2

Conversation

@gijzelaerr

Copy link
Copy Markdown
Owner

Replaces #761, rebased onto the s7commplus/ package split (#774).

Summary

Adds the full session_auth package for V1-initial S7-1200 PLCs that require a SessionKey handshake before data operations:

  • s7commplus/session_auth/: Public-key-based session authentication (Family 0 key derivation, fingerprint matching, SecurityKeyEncryptedKey blob generation, legitimation challenge solver, HMAC key derivation)
  • V3 HMAC framing for post-auth data operations
  • TIA-style CreateObject attributes (required by V1-initial firmware)
  • PAOM string stripping in ServerSessionVersion echo
  • Post-auth legitimation flow (challenge read + solved blob write)
  • Connection.connect(password=) parameter for auth flow

Test plan

Known limitations

🤖 Generated with Claude Code

Rebased onto the s7commplus/ package split (#774). All session_auth
code now lives under s7commplus/session_auth/ instead of s7/session_auth/.

Adds the full session_auth package for V1-initial S7-1200 PLCs that
require a SessionKey handshake before data operations:

- s7commplus/session_auth/: Public-key-based session authentication
  (Family 0 key derivation, fingerprint matching, SecurityKeyEncryptedKey
  blob generation, legitimation challenge solver, HMAC key derivation)
- V3 HMAC framing for post-auth data operations
- TIA-style CreateObject attributes (required by V1-initial firmware)
- PAOM string stripping in ServerSessionVersion echo
- Post-auth legitimation flow (challenge read + solved blob write)
- Connection.connect(password=) parameter for auth flow

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
gijzelaerr and others added 3 commits July 16, 2026 19:49
…ation

- Add type annotations for hashmod parameters in HKDF helpers
- Fix frame_version type annotation (int vs ProtocolVersion)
- Remove duplicated _skip_typed_value type-dispatch block (dead code after return)
- Guard auth_result unpacking with explicit None check for mypy
- Add None guards for session_key and challenge in _post_auth_legitimation
- Add assert/guard for Optional connection in substreamed client methods

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Analysis of TIA Portal V19 pcap captures (xBiggs, GH-710) revealed the
root cause of the ConnectionReset after SessionKey auth:

TIA Portal's post-SetupSession sequence:
  1. SET_VARIABLE (0x04F2) — write attribute 323 on session
  2. GET_VAR_SUBSTREAMED — read from InObj=50, addr 7920 (finalize)
  3. Data reads work immediately (no password needed)
  4. Legitimation (addr 303/1846) happens later, optionally

Our code was skipping steps 1-2 and jumping straight to legitimation
(read challenge → write blob → finalize), which caused the PLC to
return V254 SYSTEM_EVENT error 0xE9 and RST subsequent requests.

Changes:
- Add _session_activate() that mirrors TIA Portal's activation:
  SET_VARIABLE attr 323 + GET_VAR_SUBSTREAMED InObj=50 addr 7920
- Only call _post_auth_legitimation() when a password is provided
  (previously ran unconditionally, even with empty password)
- Reorder: activate first, then legitimation (if password given)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace duplicate _skip_typed_value (73 lines) with codec.skip_typed_value
  import; fixes DWORD bug (was VLQ instead of fixed 4 bytes)
- Initialize _session_auth_public_key/_session_auth_family in __init__
  instead of using hasattr() guards
- Remove redundant lazy imports (DataType, LegitimationId already at
  top level)
- Make SessionKey and TLS auth mutually exclusive in client.connect()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@gijzelaerr
gijzelaerr force-pushed the fix-tls-layering-v2 branch 2 times, most recently from 3a5660f to 6a3b019 Compare July 16, 2026 18:15
gijzelaerr and others added 2 commits July 16, 2026 20:33
Move the 95K+ lines of machine-transpiled monolith transforms, constant
tables, and binary data into family0/_generated/ to clearly separate
opaque generated code from the human-readable orchestration layer.

Add ARCHITECTURE.md documenting:
- When SessionKey auth is needed (V1-initial vs TLS)
- The full authentication flow (diagram)
- Module map: which file does what
- How the 180-byte SecurityKeyEncryptedKey blob is built
- References to upstream HarpoS7, Black Hat papers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Data2Collection table 1 had 2111 entries (4222 bytes) but its 33
operations require 2112 entries (4224 bytes). The missing final word
0x4C28 caused IndexError during session key renewal when a challenge
selected the last lookup block.

Backported from HarpoS7 commit 22b9dc0 (bonk-dev/HarpoS7#24).

Adds:
- The missing 0x4C28 entry in fp_data2.bin
- Lookup table size invariant test
- Regression test with captured renewal challenge 5B15B469...

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
gijzelaerr and others added 2 commits July 16, 2026 21:14
Analysis of all 4 TIA Portal pcaps (GH-710 + GH-728) shows TIA
consistently sends 4 requests after SetupSession, not 2:

1. SET_VARIABLE attr 323 = USINT(5)
2. GET_VAR_SUBSTREAMED InObj=50, addr 7920
3. GET_VAR_SUBSTREAMED InObj=session, addr 1842
4. GET_VAR_SUBSTREAMED InObj=50, addr 7920 (again)

Previously we only did steps 1-2. Add steps 3-4 and extract a
_build_get_var_substreamed helper to reduce boilerplate in both
_session_activate and _post_auth_legitimation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move _parse_create_object_response (120 lines, connection.py) into
  codec.py as parse_create_object_attributes — a reusable function that
  returns ServerSessionVersion, public key fingerprint, and session
  challenge in a CreateObjectAttributes dataclass. Both sync connection
  and async_client can use it.
- Keep parse_server_session_version as a thin wrapper for backward compat.
- Fix: add session_auth/**/*.bin to pyproject.toml package-data so
  binary data files (fp_data, transform12 metadata) ship in wheels.

Net: -69 lines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@bonk-dev

Copy link
Copy Markdown

Hi, I'm the creator of HarpoS7. I see that you're porting it to Python which is awesome! However, could you please include the original license text somewhere in your repository?: LICENSE-HarpoS7

@gijzelaerr

Copy link
Copy Markdown
Owner Author

hey! thanks for making HarpoS7!

But i don't fully understand your request. the licenses we put on our software is the same, it's both MIT right?

https://github.com/gijzelaerr/python-snap7/blob/master/LICENSE

Or are you talking about credits? I think the parts that are ported from HaroS7 should indicate it comes from your source code, but i happily do another iteration and check credit is given where applicable. I'll aso add you to the credits file/section.

Also, i'm not 100% convinced yet that the SessionKey handshake for V1 s7commplus should land in this package, since it is very reverse-engineery with tons of binary blobs and magic. I've been trying to polish it, but there is still quite some magic. Maybe my cleanups can be helpful on your side again :)

Let me know if i can help you out in another way.

The session_auth package is a Python port of HarpoS7 by bonk-dev.
Include the original MIT license text as requested in #775, and add
credits in the changelog and docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants