Skip to content

fix(tls): derive OMS exporter secret from TLS key log (stdlib only)#773

Merged
gijzelaerr merged 2 commits into
gijzelaerr:masterfrom
gridsociety:fix/oms-exporter-keylog
Jul 15, 2026
Merged

fix(tls): derive OMS exporter secret from TLS key log (stdlib only)#773
gijzelaerr merged 2 commits into
gijzelaerr:masterfrom
gridsociety:fix/oms-exporter-keylog

Conversation

@ale-rinaldi

@ale-rinaldi ale-rinaldi commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

CPython's ssl module doesn't expose export_keying_material (RFC 5705), so _activate_tls always fell into Could not extract OMS exporter secret and set oms_secret = None — leaving password legitimation / HMAC key derivation as dead code against real PLCs (#718).

This derives the secret using only the standard library — no new dependency:

  • Enable the TLS key log (SSLContext.keylog_filename) on a private 0600 temp file, removed immediately after the handshake.
  • Read the TLS 1.3 exporter_master_secret from it and run the RFC 8446 exporter derivation with stdlib hmac/hashlib (_tls13_exporter).

Relationship to #772

This is an alternative to #772, which solves the same problem by adding a pyOpenSSL (+cryptography) dependency. The two touch the same _activate_tls, so only one is needed. The trade-offs:

(No "ctypes" angle here — pyOpenSSL uses cffi, not ctypes; the only real difference is the added dependency.)

Validation

Tested over TLS 1.3 against two real PLCs:

  • S7-1200 CPU 1211C, FW V4.6
  • S7-1500 soft-controller CPU 1507S F, FW V21.9

On both: oms_secret is now populated (32 bytes) instead of None, and the derived value is byte-for-byte identical to pyOpenSSL's export_keying_material(b"EXPERIMENTAL_OMS", 32) on the same session (cross-checked separately). Anonymous V2 reads still work; falls back to oms_secret=None for non-TLS-1.3 sessions.

Scope / not included

This only unblocks the exporter. The legitimation challenge fetch (GetVarSubStreamed on SERVER_SESSION_REQUEST=303) is still rejected by the 1211C (return_value=0xC318890001E1FFFE), so full password legitimation needs further protocol work — tracked in #718.

Happy to add a unit test for _tls13_exporter (fixed vector) if you'd like.

🤖 Generated with Claude Code

CPython's ssl module has no export_keying_material, so _activate_tls
always logged "Could not extract OMS exporter secret" and left
oms_secret=None — leaving password legitimation / HMAC key derivation
as dead code against real hardware (see gijzelaerr#718).

Capture the TLS 1.3 exporter_master_secret via the session key log
(SSLContext.keylog_filename, written to a private 0600 temp file that is
removed right after the handshake) and run the RFC 8446 exporter
derivation ourselves with stdlib hmac/hashlib. No new dependency, no
ctypes / external shared libraries.

Validated on real hardware (S7-1200 CPU 1211C, FW V4.6): the derived
secret is byte-for-byte identical to pyOpenSSL's export_keying_material
on the same session, and oms_secret is now populated (32 bytes).

Scope: this unblocks the exporter-derived path only; the legitimation
challenge exchange itself still needs work (challenge fetch is rejected
by the PLC — tracked in gijzelaerr#718).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ale-rinaldi ale-rinaldi changed the title fix(tls): derive OMS exporter secret from TLS key log (no ctypes) fix(tls): derive OMS exporter secret from TLS key log (stdlib only) Jul 15, 2026
@gijzelaerr
gijzelaerr merged commit cb8f615 into gijzelaerr:master Jul 15, 2026
36 checks passed
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