
Proof-of-concept reproducing CVE-2021-22681's hardcoded-key flaw and validating a per-device mutual TLS/CRL fix over simulated EtherNet/IP, with IEC 62443-4-2 mapping.
Four runnable scripts. Real EtherNet/IP protocol traffic, real cryptography, zero Rockwell software or licensing anywhere in the chain. Built to test a claim before writing it up, not to argue for it on faith.
Provenance: built 2026-07-31, alongside a parallel investigation into the Braham, MN WWTF — one of the four utilities publicly disclosed in the coordinated Minnesota water-sector incident of July 26–27, 2026. Context for that incident lives in CISA advisory AA26-097A (joint FBI/CISA/NSA/EPA/DOE/USCYBERCOM/Treasury; issued 2026-04-07, expanded 2026-07-22), which covers the ongoing IRGC-affiliated CyberAv3ngers campaign. Attribution caveat, held precisely: no agency has formally attributed the Minnesota incident specifically to that group — only the broader ongoing campaign is. This folder is the technical-fix side, kept separate from the incident investigation on purpose.
A disclosure packet lives or dies on not smearing these together, because each has a different fix:
The fix demonstrated here — per-device identity binding (Test 3) — addresses the fleet-key failure.
Do not conflate the two rows. The principle is proven. The vendor's specific implementation of it is credible (it's their own stated design intent) but untested by us against real equipment.
test1_baseline_vulnerable.py — the no-auth baseline, liveSpins up a real EtherNet/IP PLC simulator (cpppo, emulating an Allen-Bradley ControlLogix) and
reads + writes a control tag with zero credentials. (Scope: this is the broad
no-authentication baseline the campaign leaned on — not the specific hardcoded-key mechanism
of CVE-2021-22681. Kept distinct on purpose; see "Three different failures" above.)
python test1_baseline_vulnerable.py
test2_shared_secret_fails.py — the shape of the fleet-key flaw (narrative bridge, not a test)Two endpoints hold one static key; a credential from Device A opens Device B unchanged — the closest structural analog to CVE-2021-22681's one-key-fits-all flaw. But it is tautological: both handlers are constructed to accept that key, so there is no execution path in which it can fail. It demonstrates nothing the code doesn't define into existence. Kept as the narrative bridge from Test 1 to Test 3; it carries no evidential weight and is deliberately not a proof-leg.
python test2_shared_secret_fails.py
test3_mutual_tls_fix.py — the fix, with its negative control and both directionsReal CA, two individually-unique device certs — identity bound in the SubjectAlternativeName, not the deprecated CommonName. Five cases, all executed:
device-a (check_hostname against a real SAN). Mutual — both ends bind identity.python test3_mutual_tls_fix.py
test4_revocation.py — the lifecycle leg: revocationA real CA-signed CRL. A client credential (engineer-1) is granted; then its serial is added to
the CRL, and the same still-valid, unexpired, CA-signed credential is denied — the empirical
content of the CR 1.8 / 1.9 revocation clause. Uniqueness (Test 3) ≠ revocability; this shows a
credential can be taken back.
python test4_revocation.py
test4_revocation.py); rotation — not yet. Per-device
uniqueness (Test 3) is not the same as revocability; Test 4 closes that gap — a still-valid,
unexpired, CA-signed credential is granted before revocation and denied after, solely because the
CA-signed CRL now lists its serial. Rotation (re-issue a replacement credential and retire the
old one) is closely related and enabled by the same PKI, but is not separately demonstrated here —
so "per-device identity binding" still must not silently expand into "rotation solved."presented == KEY, identity in SAN) are not constant-time. Not exploitable here — the compared
values are public-ish identity strings and TLS has already done the real cryptographic
authentication before the compare runs — but flagged because the pattern gets copied into places
where it does matter.python -m venv venv
venv\Scripts\activate # or: source venv/bin/activate
pip install -r requirements.txt
62443-4-2_SL2_MAPPING.md (CR 1.2 / 1.8 / 1.9 / 1.14 / 3.1,
honestly tiered, every gap named; CR 1.2, CR 1.9, and CR 1.8's issuance/validation/revocation leg
are now demonstrated). Before PSIRT ([email protected] / [email protected]):
verify each CR's normative text against a purchased copy of IEC 62443-4-2:2019.PHASED_ROLLOUT.md (Phase 0 stop-the-bleeding · 1 segment · 2
compensating controls · 3 CIP Security/PKI, hardware-permitting · 4 operate). Right-sized for a small
utility; honest that CIP Security is hardware-gated, so Phases 0–2 carry the risk reduction regardless.Every external identifier here was pulled from a live source on 2026-07-31, not recalled from
training: AA26-097A (multi-source, incl. WaterISAC / Tenable / SecurityWeek), Braham as one of the
four disclosed victims, CyberAv3ngers/IRGC, PN1550 confirmed the real Rockwell advisory with its
Row-2 quote checked verbatim ("When properly deployed, CIP Security remediates this vulnerability"
ICSA-21-056-03, and 62443-4-2 CR 1.8 (PKI) + CR 3.1
(comm integrity) confirmed exact.
Discipline for the packet: re-pull every identifier from primary sources at submission time.
Advisories get renumbered, expanded, and superseded — AA26-097A already shows one expansion — so
"verified on 2026-07-31" is not "verified at submit." The full formal CR-by-CR 62443-4-2 mapping is
now written (62443-4-2_SL2_MAPPING.md) — what's left is re-pulling its cited normative text against
a purchased standard copy before submission, not writing the mapping itself.l0gic — Patrick Crosby · 2026-07-31.
Hardening log: Test 3 was strengthened to include the negative control (case 4, proving necessity not just firing), the reverse-direction case (case 5, mutual binding), and SAN-based identity (not CN), then re-verified by re-running all five cases; Test 4 (CRL revocation) was added. Test 1/2 captions were right-sized to keep the three failure classes distinct; Test 2 was demoted from "test" to narrative bridge; revocation and constant-time scope boundaries were added. The citation chain was retrieved from primary sources and stamped for re-pull at submission.
| Claim | Tier | Why |
|---|
| The architectural principle | "a single shared secret across a fleet is compromised fleet-wide by one leak; per-device identity-bound authentication closes that" | PROVEN | demonstrated with real running code including the negative control that proves the check is necessary, not merely that it fires: on a strict endpoint Device B's genuinely CA-valid cert is rejected on identity (Test 3 · case 3), but on a CA-validity-only endpoint the same cert is accepted (case 4 — the control) → "validly CA-signed alone == fleet-wide access == Test 2 in TLS clothing." The binding also holds in reverse: a rogue server presenting a valid fleet cert is rejected by the client (case 5). Test 1 separately shows the broader no-auth baseline. |
| Rockwell's specific CIP Security implementation behaves identically | "enabling CIP Security on real Rockwell hardware remediates CVE-2021-22681 exactly this way" | LEAD, sourced not verified | this is Rockwell's own advisory (PN1550) language — "When properly deployed, CIP Security remediates this vulnerability... does not make use of any hardcoded keys" — not something we've independently confirmed against real Logix hardware. We tested the principle their advisory describes, not their exact wire-level implementation. |