|
| 1 | +"""Tests for the session-auth low-level helpers. |
| 2 | +
|
| 3 | +The ``derive_key_id`` test vectors are reproduced verbatim from |
| 4 | +HarpoS7's ``HarpoS7.Utilities.Tests/Extensions/KeyExtensionsTests.cs``, |
| 5 | +which in turn was generated from the proprietary algorithm in |
| 6 | +Siemens' ``OMSp_core_managed.dll``. Matching them gives us strong |
| 7 | +ground-truth coverage that our port is byte-correct. |
| 8 | +""" |
| 9 | + |
| 10 | +from __future__ import annotations |
| 11 | + |
| 12 | +import pytest |
| 13 | + |
| 14 | +from s7.session_auth import KEY_ID_LENGTH, derive_key_id, get_public_key, parse_fingerprint |
| 15 | +from s7.session_auth.keys import _PUBLIC_KEYS |
| 16 | + |
| 17 | + |
| 18 | +class TestDeriveKeyId: |
| 19 | + def test_harpos7_vector_64_byte_key(self) -> None: |
| 20 | + # First TestCase from HarpoS7 KeyExtensionsTests — a 64-byte |
| 21 | + # PlcSim key (5A9B6B015F48D284 in family 3). |
| 22 | + key = bytes.fromhex( |
| 23 | + "eca6d799ddf03eaadd16b5d7245331e426c9e6ba8997877a" |
| 24 | + "7394f3286532a6b053e4229818085223432483fba4d5c43b" |
| 25 | + "d6c354c10febc903908ed271697f39e9" |
| 26 | + ) |
| 27 | + assert derive_key_id(key) == bytes.fromhex("84d2485f016b9b5a") |
| 28 | + |
| 29 | + def test_harpos7_vector_repeating_11(self) -> None: |
| 30 | + # Second TestCase from HarpoS7 — 24 bytes of 0x11. |
| 31 | + key = b"\x11" * 24 |
| 32 | + assert derive_key_id(key) == bytes.fromhex("06ddcee4adaec77a") |
| 33 | + |
| 34 | + def test_harpos7_vector_repeating_44(self) -> None: |
| 35 | + # Third TestCase from HarpoS7 — 24 bytes of 0x44. |
| 36 | + key = b"\x44" * 24 |
| 37 | + assert derive_key_id(key) == bytes.fromhex("06d0ef4b10626822") |
| 38 | + |
| 39 | + def test_returns_eight_bytes(self) -> None: |
| 40 | + assert len(derive_key_id(b"\x00" * 32)) == KEY_ID_LENGTH |
| 41 | + |
| 42 | + def test_only_first_24_bytes_used(self) -> None: |
| 43 | + # Bytes past offset 24 must not affect the output. |
| 44 | + base = b"A" * 24 |
| 45 | + assert derive_key_id(base + b"X" * 100) == derive_key_id(base + b"Y" * 100) |
| 46 | + |
| 47 | + def test_short_key_rejected(self) -> None: |
| 48 | + with pytest.raises(ValueError, match="at least 24 bytes"): |
| 49 | + derive_key_id(b"\x00" * 23) |
| 50 | + |
| 51 | + |
| 52 | +class TestKeyIdMatchesAdvertisedFingerprint: |
| 53 | + """Self-verifies: every bundled public key, when run through |
| 54 | + ``derive_key_id``, must produce the fingerprint it's keyed on. |
| 55 | +
|
| 56 | + The fingerprint string is the big-endian hex display of the same |
| 57 | + 8 bytes ``derive_key_id`` produces (in little-endian byte order). |
| 58 | + """ |
| 59 | + |
| 60 | + def test_xbiggs_s7_1200_key(self) -> None: |
| 61 | + # The headline case: the PLC fingerprint @xBiggs reports in |
| 62 | + # #710 must round-trip through our key store + derive_key_id. |
| 63 | + key = get_public_key("01:BD426B091F08731A") |
| 64 | + assert derive_key_id(key) == bytes.fromhex("1A73081F096B42BD") |
| 65 | + |
| 66 | + def test_all_bundled_keys_round_trip(self) -> None: |
| 67 | + for (family, key_id), key in _PUBLIC_KEYS.items(): |
| 68 | + expected_id_bytes = bytes.fromhex(key_id)[::-1] # BE display → LE bytes |
| 69 | + assert derive_key_id(key) == expected_id_bytes, f"derive_key_id mismatch for {family.name}/{key_id}" |
| 70 | + |
| 71 | + def test_full_fingerprint_string_round_trip(self) -> None: |
| 72 | + # End-to-end: parse the fingerprint string, look up the key, |
| 73 | + # derive its id, and confirm the id reconstructs the original |
| 74 | + # post-colon hex. |
| 75 | + fp = "01:BD426B091F08731A" |
| 76 | + family, key_id = parse_fingerprint(fp) |
| 77 | + del family # not asserted here |
| 78 | + key = get_public_key(fp) |
| 79 | + derived_le = derive_key_id(key) |
| 80 | + derived_be_hex = derived_le[::-1].hex().upper() |
| 81 | + assert derived_be_hex == key_id |
0 commit comments