Skip to content

Commit 5b75db3

Browse files
committed
Add PCAP evidence, sanitizers, fuzzing, and lab tooling
1 parent b65d474 commit 5b75db3

28 files changed

Lines changed: 1423 additions & 221 deletions
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
name: Security and Evidence
2+
3+
on:
4+
push:
5+
branches: [main, "agent/**"]
6+
pull_request:
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
sanitizers:
14+
name: Clang / ASan + UBSan
15+
runs-on: ubuntu-latest
16+
env:
17+
CC: clang
18+
CXX: clang++
19+
ASAN_OPTIONS: detect_leaks=1:halt_on_error=1:strict_string_checks=1
20+
UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1
21+
steps:
22+
- uses: actions/checkout@v4
23+
- name: Configure
24+
run: >-
25+
cmake -S . -B build-sanitizers
26+
-DCMAKE_BUILD_TYPE=RelWithDebInfo
27+
-DARIEC61850_ENABLE_SANITIZERS=ON
28+
-DARIEC61850_WARNINGS_AS_ERRORS=ON
29+
- name: Build
30+
run: cmake --build build-sanitizers --parallel
31+
- name: Test
32+
run: ctest --test-dir build-sanitizers --output-on-failure
33+
34+
fuzz-smoke:
35+
name: Clang / libFuzzer corpus smoke
36+
runs-on: ubuntu-latest
37+
timeout-minutes: 15
38+
env:
39+
CC: clang
40+
CXX: clang++
41+
ASAN_OPTIONS: detect_leaks=1:halt_on_error=1
42+
UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1
43+
steps:
44+
- uses: actions/checkout@v4
45+
- name: Configure fuzzers
46+
run: >-
47+
cmake -S . -B build-fuzz
48+
-DCMAKE_BUILD_TYPE=RelWithDebInfo
49+
-DARIEC61850_BUILD_TESTS=OFF
50+
-DARIEC61850_BUILD_TOOLS=OFF
51+
-DARIEC61850_BUILD_FUZZERS=ON
52+
-DARIEC61850_ENABLE_SANITIZERS=ON
53+
-DARIEC61850_WARNINGS_AS_ERRORS=ON
54+
- name: Build fuzzers
55+
run: cmake --build build-fuzz --parallel
56+
- name: Run BER corpus
57+
run: ./build-fuzz/ariec61850_fuzz_ber fuzz/corpus/ber -runs=1000 -timeout=5
58+
- name: Run GOOSE corpus
59+
run: ./build-fuzz/ariec61850_fuzz_goose fuzz/corpus/goose -runs=1000 -timeout=5
60+
- name: Run Sampled Values corpus
61+
run: ./build-fuzz/ariec61850_fuzz_sampled_values fuzz/corpus/sampled_values -runs=1000 -timeout=5
62+
- name: Run PCAP corpus
63+
run: ./build-fuzz/ariec61850_fuzz_pcap fuzz/corpus/pcap -runs=1000 -timeout=5
64+
- name: Upload crash evidence
65+
if: failure()
66+
uses: actions/upload-artifact@v4
67+
with:
68+
name: fuzz-crash-evidence
69+
if-no-files-found: ignore
70+
path: |
71+
crash-*
72+
leak-*
73+
timeout-*
74+
oom-*
75+
slow-unit-*

‎.gitignore‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
build/
2+
build-*/
23
cmake-build-*/
34
CMakeFiles/
45
CMakeCache.txt
@@ -8,3 +9,10 @@ Testing/
89
*.vcxproj
910
*.vcxproj.filters
1011
*.vcxproj.user
12+
*.profraw
13+
crash-*
14+
leak-*
15+
timeout-*
16+
oom-*
17+
slow-unit-*
18+
*.interop.json

‎CMakeLists.txt‎

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
cmake_minimum_required(VERSION 3.20)
22

33
project(ARIEC61850Cpp
4-
VERSION 0.5.0
4+
VERSION 0.6.0
55
DESCRIPTION "Incremental C++20 port of the ARIEC61850 protocol stack"
66
LANGUAGES CXX)
77

88
option(ARIEC61850_BUILD_TESTS "Build ARIEC61850 C++ regression tests" ON)
9+
option(ARIEC61850_BUILD_TOOLS "Build read-only evidence tools" ON)
10+
option(ARIEC61850_BUILD_FUZZERS "Build LLVM libFuzzer targets" OFF)
11+
option(ARIEC61850_ENABLE_SANITIZERS "Enable AddressSanitizer and UndefinedBehaviorSanitizer" OFF)
912
option(ARIEC61850_WARNINGS_AS_ERRORS "Treat compiler warnings as errors" ON)
1013

1114
add_library(ariec61850_core
@@ -25,7 +28,8 @@ add_library(ariec61850_core
2528
src/sampled_values/quality.cpp
2629
src/sampled_values/payload_inspector.cpp
2730
src/sampled_values/sample_counter.cpp
28-
src/sampled_values/stream_supervisor.cpp)
31+
src/sampled_values/stream_supervisor.cpp
32+
src/evidence/pcap_equivalence.cpp)
2933

3034
add_library(ARIEC61850::core ALIAS ariec61850_core)
3135

@@ -42,14 +46,37 @@ function(ariec61850_apply_warnings target)
4246
target_compile_options(${target} PRIVATE /WX)
4347
endif()
4448
else()
45-
target_compile_options(${target} PRIVATE -Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion)
49+
target_compile_options(${target} PRIVATE
50+
-Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion)
4651
if(ARIEC61850_WARNINGS_AS_ERRORS)
4752
target_compile_options(${target} PRIVATE -Werror)
4853
endif()
4954
endif()
5055
endfunction()
5156

57+
function(ariec61850_apply_sanitizers target)
58+
if(NOT ARIEC61850_ENABLE_SANITIZERS)
59+
return()
60+
endif()
61+
if(MSVC)
62+
message(FATAL_ERROR "ARIEC61850_ENABLE_SANITIZERS currently requires GCC or Clang.")
63+
endif()
64+
target_compile_options(${target} PRIVATE
65+
-fsanitize=address,undefined -fno-omit-frame-pointer)
66+
target_link_options(${target} PRIVATE
67+
-fsanitize=address,undefined -fno-omit-frame-pointer)
68+
endfunction()
69+
5270
ariec61850_apply_warnings(ariec61850_core)
71+
ariec61850_apply_sanitizers(ariec61850_core)
72+
73+
if(ARIEC61850_BUILD_TOOLS)
74+
add_executable(ariec61850_pcap_interop_check tools/pcap_interop_check.cpp)
75+
target_link_libraries(ariec61850_pcap_interop_check PRIVATE ARIEC61850::core)
76+
target_compile_features(ariec61850_pcap_interop_check PRIVATE cxx_std_20)
77+
ariec61850_apply_warnings(ariec61850_pcap_interop_check)
78+
ariec61850_apply_sanitizers(ariec61850_pcap_interop_check)
79+
endif()
5380

5481
if(ARIEC61850_BUILD_TESTS)
5582
include(CTest)
@@ -59,19 +86,73 @@ if(ARIEC61850_BUILD_TESTS)
5986
target_link_libraries(ariec61850_core_tests PRIVATE ARIEC61850::core)
6087
target_compile_features(ariec61850_core_tests PRIVATE cxx_std_20)
6188
ariec61850_apply_warnings(ariec61850_core_tests)
89+
ariec61850_apply_sanitizers(ariec61850_core_tests)
6290
add_test(NAME ariec61850_core_tests COMMAND ariec61850_core_tests)
6391

6492
foreach(test_name IN ITEMS mms goose sampled_values)
6593
add_executable(ariec61850_${test_name}_tests tests/test_${test_name}.cpp)
6694
target_link_libraries(ariec61850_${test_name}_tests PRIVATE ARIEC61850::core)
6795
target_compile_features(ariec61850_${test_name}_tests PRIVATE cxx_std_20)
6896
ariec61850_apply_warnings(ariec61850_${test_name}_tests)
97+
ariec61850_apply_sanitizers(ariec61850_${test_name}_tests)
6998
add_test(NAME ariec61850_${test_name}_tests COMMAND ariec61850_${test_name}_tests)
7099
endforeach()
71100

72101
add_executable(ariec61850_goose_runtime_tests tests/test_goose_runtime.cpp)
73102
target_link_libraries(ariec61850_goose_runtime_tests PRIVATE ARIEC61850::core)
74103
target_compile_features(ariec61850_goose_runtime_tests PRIVATE cxx_std_20)
75104
ariec61850_apply_warnings(ariec61850_goose_runtime_tests)
105+
ariec61850_apply_sanitizers(ariec61850_goose_runtime_tests)
76106
add_test(NAME ariec61850_goose_runtime_tests COMMAND ariec61850_goose_runtime_tests)
107+
108+
add_executable(ariec61850_evidence_tests tests/test_evidence.cpp)
109+
target_link_libraries(ariec61850_evidence_tests PRIVATE ARIEC61850::core)
110+
target_compile_features(ariec61850_evidence_tests PRIVATE cxx_std_20)
111+
target_compile_definitions(ariec61850_evidence_tests PRIVATE
112+
ARIEC61850_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
113+
ariec61850_apply_warnings(ariec61850_evidence_tests)
114+
ariec61850_apply_sanitizers(ariec61850_evidence_tests)
115+
add_test(NAME ariec61850_evidence_tests COMMAND ariec61850_evidence_tests)
116+
117+
add_executable(ariec61850_fuzz_smoke_tests tests/test_fuzz_smoke.cpp)
118+
target_link_libraries(ariec61850_fuzz_smoke_tests PRIVATE ARIEC61850::core)
119+
target_include_directories(ariec61850_fuzz_smoke_tests PRIVATE
120+
${CMAKE_CURRENT_SOURCE_DIR}/fuzz)
121+
target_compile_definitions(ariec61850_fuzz_smoke_tests PRIVATE
122+
ARIEC61850_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
123+
target_compile_features(ariec61850_fuzz_smoke_tests PRIVATE cxx_std_20)
124+
ariec61850_apply_warnings(ariec61850_fuzz_smoke_tests)
125+
ariec61850_apply_sanitizers(ariec61850_fuzz_smoke_tests)
126+
add_test(NAME ariec61850_fuzz_smoke_tests COMMAND ariec61850_fuzz_smoke_tests)
127+
128+
if(ARIEC61850_BUILD_TOOLS)
129+
add_test(
130+
NAME ariec61850_pcap_interop_smoke
131+
COMMAND ariec61850_pcap_interop_check
132+
${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/process_bus_csharp_vectors.pcap
133+
--json)
134+
endif()
135+
endif()
136+
137+
if(ARIEC61850_BUILD_FUZZERS)
138+
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang")
139+
message(FATAL_ERROR "ARIEC61850_BUILD_FUZZERS requires Clang/libFuzzer.")
140+
endif()
141+
142+
function(ariec61850_add_fuzzer name source)
143+
add_executable(${name} ${source})
144+
target_link_libraries(${name} PRIVATE ARIEC61850::core)
145+
target_include_directories(${name} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/fuzz)
146+
target_compile_features(${name} PRIVATE cxx_std_20)
147+
target_compile_options(${name} PRIVATE
148+
-fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer)
149+
target_link_options(${name} PRIVATE
150+
-fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer)
151+
ariec61850_apply_warnings(${name})
152+
endfunction()
153+
154+
ariec61850_add_fuzzer(ariec61850_fuzz_ber fuzz/fuzz_ber.cpp)
155+
ariec61850_add_fuzzer(ariec61850_fuzz_goose fuzz/fuzz_goose.cpp)
156+
ariec61850_add_fuzzer(ariec61850_fuzz_sampled_values fuzz/fuzz_sampled_values.cpp)
157+
ariec61850_add_fuzzer(ariec61850_fuzz_pcap fuzz/fuzz_pcap.cpp)
77158
endif()

‎LAB_INTEROP_CHECKLIST.md‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Isolated-Laboratory Interoperability Checklist
2+
3+
This checklist is intentionally receive-first and read-only. The Phase 1E tooling does
4+
not open Npcap, transmit Ethernet frames, or operate an IED.
5+
6+
## 1. Safety boundary
7+
8+
- [ ] Use an isolated laboratory switch or an isolated VLAN with no route to a production substation LAN.
9+
- [ ] Confirm the engineering laptop has no bridge, Internet sharing, or second adapter forwarding traffic.
10+
- [ ] Begin with a mirror/SPAN port or passive TAP; do not connect an active publisher output first.
11+
- [ ] Record IED manufacturer, model, firmware, configuration revision, MAC addresses, APPIDs, VLAN IDs, and sampling mode.
12+
- [ ] Synchronize clocks only through the laboratory timing source approved for the test.
13+
14+
## 2. Receive-only capture
15+
16+
- [ ] Capture at least one stable GOOSE state and one GOOSE state transition.
17+
- [ ] Capture Sampled Values across at least two counter wraps or ten seconds, whichever is longer.
18+
- [ ] Preserve the original PCAP; do not edit it in Wireshark before hashing.
19+
- [ ] Calculate SHA-256 and record the capture hash in the report template.
20+
- [ ] Keep a copy of the SCL/CID/SCD file used to configure the streams.
21+
22+
## 3. Automated evidence check
23+
24+
Windows:
25+
26+
```powershell
27+
./scripts/run-lab-check.ps1 -Pcap ./captures/device-session.pcap
28+
```
29+
30+
Linux:
31+
32+
```bash
33+
./scripts/run-lab-check.sh ./captures/device-session.pcap
34+
```
35+
36+
Acceptance conditions:
37+
38+
- [ ] At least one GOOSE or Sampled Values packet is decoded.
39+
- [ ] No process-bus packet is malformed.
40+
- [ ] Every decoded GOOSE/SV frame re-encodes byte-for-byte to the captured frame.
41+
- [ ] PCAP packet order, payload bytes, and microsecond-normalized timestamps round-trip.
42+
- [ ] Stream identifiers, APPIDs, VLANs, and configuration revisions match the engineering files.
43+
44+
## 4. Protocol observations
45+
46+
GOOSE:
47+
48+
- [ ] `stNum` increases only on state changes.
49+
- [ ] `sqNum` behavior and retransmission intervals match the configured profile.
50+
- [ ] `timeAllowedToLive` remains adequate for observed arrival gaps.
51+
- [ ] Dataset values match the expected state transition.
52+
53+
Sampled Values:
54+
55+
- [ ] `smpCnt` progression and wrap policy match the configured sampling mode.
56+
- [ ] `confRev`, `smpSynch`, `smpRate`, and optional `refrTm` are consistent.
57+
- [ ] Missing, duplicate, and out-of-order sample counts are zero or explained.
58+
- [ ] `seqOfData` channel mapping and scaling are validated against SCL, not inferred from byte position alone.
59+
60+
## 5. Escalation and evidence retention
61+
62+
- [ ] Save the PCAP, JSON report, SHA-256, SCL files, device configuration export, and test notes together.
63+
- [ ] Record every mismatch before changing the parser or normalizing captured bytes.
64+
- [ ] Do not enable active GOOSE/SV transmission from this stack until the receive-only evidence passes and a separate approved transmit test plan exists.
65+
66+
## Current status
67+
68+
The synthetic C#-derived corpus is automated in CI. Physical IED interoperability
69+
remains **not yet passed** until a real capture and completed report are committed or
70+
otherwise archived as controlled evidence.

‎LAB_INTEROP_REPORT_TEMPLATE.md‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# ARIEC61850 Isolated-Lab Interoperability Report
2+
3+
## Test identity
4+
5+
- Date/time:
6+
- Engineer:
7+
- Laboratory/network diagram reference:
8+
- IED manufacturer/model:
9+
- Firmware:
10+
- Engineering file and revision:
11+
- Capture filename:
12+
- Capture SHA-256:
13+
- Capture duration:
14+
- Tool commit SHA:
15+
16+
## Network parameters
17+
18+
| Stream | Protocol | Source MAC | Destination MAC | VLAN/PCP | APPID | Identifier |
19+
|---|---|---|---|---|---|---|
20+
| 1 | | | | | | |
21+
22+
## Automated checker result
23+
24+
- Overall result: PASS / FAIL
25+
- Packet count:
26+
- GOOSE count:
27+
- Sampled Values count:
28+
- Malformed count:
29+
- Exact decode/re-encode matches:
30+
- Re-encode mismatches:
31+
- Canonical PCAP round trip: PASS / FAIL
32+
- JSON report path/hash:
33+
34+
## GOOSE observations
35+
36+
- State transitions exercised:
37+
- `stNum`/`sqNum` observations:
38+
- Retransmission timing:
39+
- TTL/arrival-gap result:
40+
- Dataset/value result:
41+
42+
## Sampled Values observations
43+
44+
- Sampling mode/rate:
45+
- Counter wrap policy:
46+
- Missing/duplicate/out-of-order samples:
47+
- Synchronization and reference-time result:
48+
- SCL-backed channel/scaling result:
49+
50+
## Deviations and evidence
51+
52+
| ID | Packet/time | Observation | Expected | Disposition |
53+
|---|---|---|---|---|
54+
| 1 | | | | |
55+
56+
## Acceptance
57+
58+
- [ ] Receive-only GOOSE interoperability accepted.
59+
- [ ] Receive-only Sampled Values interoperability accepted.
60+
- [ ] All artifacts retained under controlled storage.
61+
- [ ] Active transmission remains disabled unless separately approved.
62+
63+
Approver/signature:

0 commit comments

Comments
 (0)