Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .github/workflows/security-evidence.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: Security and Evidence

on:
push:
branches: [main, "agent/**"]
pull_request:
workflow_dispatch:

permissions:
contents: read

jobs:
sanitizers:
name: Clang / ASan + UBSan
runs-on: ubuntu-latest
env:
CC: clang
CXX: clang++
ASAN_OPTIONS: detect_leaks=1:halt_on_error=1:strict_string_checks=1
UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1
steps:
- uses: actions/checkout@v4
- name: Configure
run: >-
cmake -S . -B build-sanitizers
-DCMAKE_BUILD_TYPE=RelWithDebInfo
-DARIEC61850_ENABLE_SANITIZERS=ON
-DARIEC61850_WARNINGS_AS_ERRORS=ON
- name: Build
run: cmake --build build-sanitizers --parallel
- name: Test
run: ctest --test-dir build-sanitizers --output-on-failure

fuzz-smoke:
name: Clang / libFuzzer corpus smoke
runs-on: ubuntu-latest
timeout-minutes: 15
env:
CC: clang
CXX: clang++
ASAN_OPTIONS: detect_leaks=1:halt_on_error=1
UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1
steps:
- uses: actions/checkout@v4
- name: Configure fuzzers
run: >-
cmake -S . -B build-fuzz
-DCMAKE_BUILD_TYPE=RelWithDebInfo
-DARIEC61850_BUILD_TESTS=OFF
-DARIEC61850_BUILD_TOOLS=OFF
-DARIEC61850_BUILD_FUZZERS=ON
-DARIEC61850_ENABLE_SANITIZERS=ON
-DARIEC61850_WARNINGS_AS_ERRORS=ON
- name: Build fuzzers
run: cmake --build build-fuzz --parallel
- name: Run BER corpus
run: ./build-fuzz/ariec61850_fuzz_ber fuzz/corpus/ber -runs=1000 -timeout=5
- name: Run GOOSE corpus
run: ./build-fuzz/ariec61850_fuzz_goose fuzz/corpus/goose -runs=1000 -timeout=5
- name: Run Sampled Values corpus
run: ./build-fuzz/ariec61850_fuzz_sampled_values fuzz/corpus/sampled_values -runs=1000 -timeout=5
- name: Run PCAP corpus
run: ./build-fuzz/ariec61850_fuzz_pcap fuzz/corpus/pcap -runs=1000 -timeout=5
- name: Upload crash evidence
if: failure()
uses: actions/upload-artifact@v4
with:
name: fuzz-crash-evidence
if-no-files-found: ignore
path: |
crash-*
leak-*
timeout-*
oom-*
slow-unit-*
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
build/
build-*/
cmake-build-*/
CMakeFiles/
CMakeCache.txt
Expand All @@ -8,3 +9,10 @@ Testing/
*.vcxproj
*.vcxproj.filters
*.vcxproj.user
*.profraw
crash-*
leak-*
timeout-*
oom-*
slow-unit-*
*.interop.json
87 changes: 84 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
cmake_minimum_required(VERSION 3.20)

project(ARIEC61850Cpp
VERSION 0.5.0
VERSION 0.6.0
DESCRIPTION "Incremental C++20 port of the ARIEC61850 protocol stack"
LANGUAGES CXX)

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

add_library(ariec61850_core
Expand All @@ -25,7 +28,8 @@ add_library(ariec61850_core
src/sampled_values/quality.cpp
src/sampled_values/payload_inspector.cpp
src/sampled_values/sample_counter.cpp
src/sampled_values/stream_supervisor.cpp)
src/sampled_values/stream_supervisor.cpp
src/evidence/pcap_equivalence.cpp)

add_library(ARIEC61850::core ALIAS ariec61850_core)

Expand All @@ -42,14 +46,37 @@ function(ariec61850_apply_warnings target)
target_compile_options(${target} PRIVATE /WX)
endif()
else()
target_compile_options(${target} PRIVATE -Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion)
target_compile_options(${target} PRIVATE
-Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion)
if(ARIEC61850_WARNINGS_AS_ERRORS)
target_compile_options(${target} PRIVATE -Werror)
endif()
endif()
endfunction()

function(ariec61850_apply_sanitizers target)
if(NOT ARIEC61850_ENABLE_SANITIZERS)
return()
endif()
if(MSVC)
message(FATAL_ERROR "ARIEC61850_ENABLE_SANITIZERS currently requires GCC or Clang.")
endif()
target_compile_options(${target} PRIVATE
-fsanitize=address,undefined -fno-omit-frame-pointer)
target_link_options(${target} PRIVATE
-fsanitize=address,undefined -fno-omit-frame-pointer)
endfunction()

ariec61850_apply_warnings(ariec61850_core)
ariec61850_apply_sanitizers(ariec61850_core)

if(ARIEC61850_BUILD_TOOLS)
add_executable(ariec61850_pcap_interop_check tools/pcap_interop_check.cpp)
target_link_libraries(ariec61850_pcap_interop_check PRIVATE ARIEC61850::core)
target_compile_features(ariec61850_pcap_interop_check PRIVATE cxx_std_20)
ariec61850_apply_warnings(ariec61850_pcap_interop_check)
ariec61850_apply_sanitizers(ariec61850_pcap_interop_check)
endif()

if(ARIEC61850_BUILD_TESTS)
include(CTest)
Expand All @@ -59,19 +86,73 @@ if(ARIEC61850_BUILD_TESTS)
target_link_libraries(ariec61850_core_tests PRIVATE ARIEC61850::core)
target_compile_features(ariec61850_core_tests PRIVATE cxx_std_20)
ariec61850_apply_warnings(ariec61850_core_tests)
ariec61850_apply_sanitizers(ariec61850_core_tests)
add_test(NAME ariec61850_core_tests COMMAND ariec61850_core_tests)

foreach(test_name IN ITEMS mms goose sampled_values)
add_executable(ariec61850_${test_name}_tests tests/test_${test_name}.cpp)
target_link_libraries(ariec61850_${test_name}_tests PRIVATE ARIEC61850::core)
target_compile_features(ariec61850_${test_name}_tests PRIVATE cxx_std_20)
ariec61850_apply_warnings(ariec61850_${test_name}_tests)
ariec61850_apply_sanitizers(ariec61850_${test_name}_tests)
add_test(NAME ariec61850_${test_name}_tests COMMAND ariec61850_${test_name}_tests)
endforeach()

add_executable(ariec61850_goose_runtime_tests tests/test_goose_runtime.cpp)
target_link_libraries(ariec61850_goose_runtime_tests PRIVATE ARIEC61850::core)
target_compile_features(ariec61850_goose_runtime_tests PRIVATE cxx_std_20)
ariec61850_apply_warnings(ariec61850_goose_runtime_tests)
ariec61850_apply_sanitizers(ariec61850_goose_runtime_tests)
add_test(NAME ariec61850_goose_runtime_tests COMMAND ariec61850_goose_runtime_tests)

add_executable(ariec61850_evidence_tests tests/test_evidence.cpp)
target_link_libraries(ariec61850_evidence_tests PRIVATE ARIEC61850::core)
target_compile_features(ariec61850_evidence_tests PRIVATE cxx_std_20)
target_compile_definitions(ariec61850_evidence_tests PRIVATE
ARIEC61850_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
ariec61850_apply_warnings(ariec61850_evidence_tests)
ariec61850_apply_sanitizers(ariec61850_evidence_tests)
add_test(NAME ariec61850_evidence_tests COMMAND ariec61850_evidence_tests)

add_executable(ariec61850_fuzz_smoke_tests tests/test_fuzz_smoke.cpp)
target_link_libraries(ariec61850_fuzz_smoke_tests PRIVATE ARIEC61850::core)
target_include_directories(ariec61850_fuzz_smoke_tests PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/fuzz)
target_compile_definitions(ariec61850_fuzz_smoke_tests PRIVATE
ARIEC61850_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
target_compile_features(ariec61850_fuzz_smoke_tests PRIVATE cxx_std_20)
ariec61850_apply_warnings(ariec61850_fuzz_smoke_tests)
ariec61850_apply_sanitizers(ariec61850_fuzz_smoke_tests)
add_test(NAME ariec61850_fuzz_smoke_tests COMMAND ariec61850_fuzz_smoke_tests)

if(ARIEC61850_BUILD_TOOLS)
add_test(
NAME ariec61850_pcap_interop_smoke
COMMAND ariec61850_pcap_interop_check
${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/process_bus_csharp_vectors.pcap
--json)
endif()
endif()

if(ARIEC61850_BUILD_FUZZERS)
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang")
message(FATAL_ERROR "ARIEC61850_BUILD_FUZZERS requires Clang/libFuzzer.")
endif()

function(ariec61850_add_fuzzer name source)
add_executable(${name} ${source})
target_link_libraries(${name} PRIVATE ARIEC61850::core)
target_include_directories(${name} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/fuzz)
target_compile_features(${name} PRIVATE cxx_std_20)
target_compile_options(${name} PRIVATE
-fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer)
target_link_options(${name} PRIVATE
-fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer)
ariec61850_apply_warnings(${name})
endfunction()

ariec61850_add_fuzzer(ariec61850_fuzz_ber fuzz/fuzz_ber.cpp)
ariec61850_add_fuzzer(ariec61850_fuzz_goose fuzz/fuzz_goose.cpp)
ariec61850_add_fuzzer(ariec61850_fuzz_sampled_values fuzz/fuzz_sampled_values.cpp)
ariec61850_add_fuzzer(ariec61850_fuzz_pcap fuzz/fuzz_pcap.cpp)
endif()
70 changes: 70 additions & 0 deletions LAB_INTEROP_CHECKLIST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Isolated-Laboratory Interoperability Checklist

This checklist is intentionally receive-first and read-only. The Phase 1E tooling does
not open Npcap, transmit Ethernet frames, or operate an IED.

## 1. Safety boundary

- [ ] Use an isolated laboratory switch or an isolated VLAN with no route to a production substation LAN.
- [ ] Confirm the engineering laptop has no bridge, Internet sharing, or second adapter forwarding traffic.
- [ ] Begin with a mirror/SPAN port or passive TAP; do not connect an active publisher output first.
- [ ] Record IED manufacturer, model, firmware, configuration revision, MAC addresses, APPIDs, VLAN IDs, and sampling mode.
- [ ] Synchronize clocks only through the laboratory timing source approved for the test.

## 2. Receive-only capture

- [ ] Capture at least one stable GOOSE state and one GOOSE state transition.
- [ ] Capture Sampled Values across at least two counter wraps or ten seconds, whichever is longer.
- [ ] Preserve the original PCAP; do not edit it in Wireshark before hashing.
- [ ] Calculate SHA-256 and record the capture hash in the report template.
- [ ] Keep a copy of the SCL/CID/SCD file used to configure the streams.

## 3. Automated evidence check

Windows:

```powershell
./scripts/run-lab-check.ps1 -Pcap ./captures/device-session.pcap
```

Linux:

```bash
./scripts/run-lab-check.sh ./captures/device-session.pcap
```

Acceptance conditions:

- [ ] At least one GOOSE or Sampled Values packet is decoded.
- [ ] No process-bus packet is malformed.
- [ ] Every decoded GOOSE/SV frame re-encodes byte-for-byte to the captured frame.
- [ ] PCAP packet order, payload bytes, and microsecond-normalized timestamps round-trip.
- [ ] Stream identifiers, APPIDs, VLANs, and configuration revisions match the engineering files.

## 4. Protocol observations

GOOSE:

- [ ] `stNum` increases only on state changes.
- [ ] `sqNum` behavior and retransmission intervals match the configured profile.
- [ ] `timeAllowedToLive` remains adequate for observed arrival gaps.
- [ ] Dataset values match the expected state transition.

Sampled Values:

- [ ] `smpCnt` progression and wrap policy match the configured sampling mode.
- [ ] `confRev`, `smpSynch`, `smpRate`, and optional `refrTm` are consistent.
- [ ] Missing, duplicate, and out-of-order sample counts are zero or explained.
- [ ] `seqOfData` channel mapping and scaling are validated against SCL, not inferred from byte position alone.

## 5. Escalation and evidence retention

- [ ] Save the PCAP, JSON report, SHA-256, SCL files, device configuration export, and test notes together.
- [ ] Record every mismatch before changing the parser or normalizing captured bytes.
- [ ] Do not enable active GOOSE/SV transmission from this stack until the receive-only evidence passes and a separate approved transmit test plan exists.

## Current status

The synthetic C#-derived corpus is automated in CI. Physical IED interoperability
remains **not yet passed** until a real capture and completed report are committed or
otherwise archived as controlled evidence.
63 changes: 63 additions & 0 deletions LAB_INTEROP_REPORT_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# ARIEC61850 Isolated-Lab Interoperability Report

## Test identity

- Date/time:
- Engineer:
- Laboratory/network diagram reference:
- IED manufacturer/model:
- Firmware:
- Engineering file and revision:
- Capture filename:
- Capture SHA-256:
- Capture duration:
- Tool commit SHA:

## Network parameters

| Stream | Protocol | Source MAC | Destination MAC | VLAN/PCP | APPID | Identifier |
|---|---|---|---|---|---|---|
| 1 | | | | | | |

## Automated checker result

- Overall result: PASS / FAIL
- Packet count:
- GOOSE count:
- Sampled Values count:
- Malformed count:
- Exact decode/re-encode matches:
- Re-encode mismatches:
- Canonical PCAP round trip: PASS / FAIL
- JSON report path/hash:

## GOOSE observations

- State transitions exercised:
- `stNum`/`sqNum` observations:
- Retransmission timing:
- TTL/arrival-gap result:
- Dataset/value result:

## Sampled Values observations

- Sampling mode/rate:
- Counter wrap policy:
- Missing/duplicate/out-of-order samples:
- Synchronization and reference-time result:
- SCL-backed channel/scaling result:

## Deviations and evidence

| ID | Packet/time | Observation | Expected | Disposition |
|---|---|---|---|---|
| 1 | | | | |

## Acceptance

- [ ] Receive-only GOOSE interoperability accepted.
- [ ] Receive-only Sampled Values interoperability accepted.
- [ ] All artifacts retained under controlled storage.
- [ ] Active transmission remains disabled unless separately approved.

Approver/signature:
Loading
Loading