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
2 changes: 2 additions & 0 deletions .github/workflows/security-evidence.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ jobs:
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: Run SCL corpus
run: ./build-fuzz/ariec61850_fuzz_scl fuzz/corpus/scl -runs=1000 -timeout=5
- name: Upload crash evidence
if: failure()
uses: actions/upload-artifact@v4
Expand Down
18 changes: 15 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.20)

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

Expand Down Expand Up @@ -29,7 +29,9 @@ add_library(ariec61850_core
src/sampled_values/payload_inspector.cpp
src/sampled_values/sample_counter.cpp
src/sampled_values/stream_supervisor.cpp
src/evidence/pcap_equivalence.cpp)
src/evidence/pcap_equivalence.cpp
src/scl/dataset_reference.cpp
src/scl/parser.cpp)

add_library(ARIEC61850::core ALIAS ariec61850_core)

Expand Down Expand Up @@ -107,13 +109,22 @@ if(ARIEC61850_BUILD_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}")
target_compile_features(ariec61850_evidence_tests PRIVATE cxx_std_20)
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_scl_tests tests/test_scl.cpp)
target_link_libraries(ariec61850_scl_tests PRIVATE ARIEC61850::core)
target_compile_definitions(ariec61850_scl_tests PRIVATE
ARIEC61850_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
target_compile_features(ariec61850_scl_tests PRIVATE cxx_std_20)
ariec61850_apply_warnings(ariec61850_scl_tests)
ariec61850_apply_sanitizers(ariec61850_scl_tests)
add_test(NAME ariec61850_scl_tests COMMAND ariec61850_scl_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
Expand Down Expand Up @@ -155,4 +166,5 @@ if(ARIEC61850_BUILD_FUZZERS)
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)
ariec61850_add_fuzzer(ariec61850_fuzz_scl fuzz/fuzz_scl.cpp)
endif()
71 changes: 71 additions & 0 deletions fuzz/corpus/scl/minimal-station.scd
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<SCL xmlns="http://www.iec.ch/61850/2003/SCL" version="2007" revision="B">
<Header id="AR_MINIMAL_STATION" version="1" revision="A" />
<Communication>
<SubNetwork name="ProcessBus" type="8-MMS">
<ConnectedAP iedName="MU01" apName="P1">
<SMV ldInst="LD0" cbName="MSVCB01">
<Address>
<P type="MAC-Address">01-0C-CD-04-00-01</P>
<P type="APPID">4001</P>
<P type="VLAN-ID">200</P>
<P type="VLAN-PRIORITY">4</P>
</Address>
</SMV>
<GSE ldInst="LD0" cbName="GCB01">
<Address>
<P type="MAC-Address">01-0C-CD-01-00-01</P>
<P type="APPID">1001</P>
<P type="VLAN-ID">100</P>
<P type="VLAN-PRIORITY">4</P>
</Address>
</GSE>
</ConnectedAP>
</SubNetwork>
</Communication>
<IED name="MU01" manufacturer="AR" type="MergingUnit" configVersion="1">
<AccessPoint name="P1">
<Server>
<LDevice inst="LD0">
<LN0 lnClass="LLN0" lnType="LLN0Type">
<DataSet name="dsSV">
<FCDA ldInst="LD0" lnClass="TCTR" lnInst="1" doName="Amp" daName="instMag.i" fc="MX" />
<FCDA ldInst="LD0" lnClass="TCTR" lnInst="1" doName="Amp" daName="q" fc="MX" />
</DataSet>
<DataSet name="dsGO">
<FCDA ldInst="LD0" lnClass="XCBR" lnInst="1" doName="Pos" daName="stVal" fc="ST" />
<FCDA ldInst="LD0" lnClass="XCBR" lnInst="1" doName="Pos" daName="q" fc="ST" />
<FCDA ldInst="LD0" lnClass="XCBR" lnInst="1" doName="Pos" daName="t" fc="ST" />
</DataSet>
<SampledValueControl name="MSVCB01" datSet="dsSV" smvID="MU01LD0/LLN0$MSVCB01" confRev="3" smpRate="4000" nofASDU="1" smpMod="SmpPerSec" multicast="true" />
<GSEControl name="GCB01" datSet="dsGO" appID="trip-goose" confRev="2" type="GOOSE" minTime="4" maxTime="1000" />
<ReportControl name="URCB01" datSet="dsGO" rptID="MU01LD0/LLN0$RP$URCB01" confRev="1" buffered="false" bufTime="100" intgPd="1000" />
</LN0>
<LN lnClass="TCTR" inst="1" lnType="TCTRType" />
<LN lnClass="XCBR" inst="1" lnType="XCBRType" />
</LDevice>
</Server>
</AccessPoint>
</IED>
<DataTypeTemplates>
<LNodeType id="LLN0Type" lnClass="LLN0" />
<LNodeType id="TCTRType" lnClass="TCTR">
<DO name="Amp" type="AmpType" />
</LNodeType>
<DOType id="AmpType" cdc="SAV">
<DA name="instMag" bType="Struct" type="AnalogueValue" fc="MX" />
<DA name="q" bType="Quality" fc="MX" />
</DOType>
<DAType id="AnalogueValue">
<BDA name="i" bType="INT32" />
</DAType>
<LNodeType id="XCBRType" lnClass="XCBR">
<DO name="Pos" type="PosType" />
</LNodeType>
<DOType id="PosType" cdc="DPC">
<DA name="stVal" bType="BOOLEAN" fc="ST" />
<DA name="q" bType="Quality" fc="ST" />
<DA name="t" bType="Timestamp" fc="ST" />
</DOType>
</DataTypeTemplates>
</SCL>
9 changes: 9 additions & 0 deletions fuzz/fuzz_scl.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "fuzz_targets.hpp"
#include <cstddef>
#include <cstdint>
#include <span>
extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t* data, const std::size_t size) {
ar::iec61850::fuzzing::exercise_scl(std::span<const std::uint8_t>{data, size});
return 0;
}
17 changes: 17 additions & 0 deletions fuzz/fuzz_targets.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "ariec61850/goose/pdu_codec.hpp"
#include "ariec61850/sampled_values/frame_codec.hpp"
#include "ariec61850/sampled_values/pdu_codec.hpp"
#include "ariec61850/scl/parser.hpp"

#include <cstddef>
#include <cstdint>
Expand Down Expand Up @@ -88,4 +89,20 @@ inline void exercise_pcap(const std::span<const std::uint8_t> bytes) {
}
}

inline void exercise_scl(const std::span<const std::uint8_t> bytes) {
std::string input;
if (!bytes.empty()) {
input.assign(reinterpret_cast<const char*>(bytes.data()), bytes.size());
}
try {
const auto document = scl::SclParser{}.parse(input, "fuzz-input.scl");
static_cast<void>(document.ieds.size());
static_cast<void>(document.data_sets.size());
static_cast<void>(document.goose_streams.size());
static_cast<void>(document.sampled_values_streams.size());
static_cast<void>(document.report_controls.size());
} catch (...) {
}
}

} // namespace ar::iec61850::fuzzing
29 changes: 29 additions & 0 deletions include/ariec61850/scl/dataset_reference.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: GPL-3.0-or-later

#pragma once

#include "ariec61850/scl/model.hpp"

#include <span>
#include <string>

namespace ar::iec61850::scl {

struct SclDataSetBindingResolution final {
SclDataSetBindingStatus status{SclDataSetBindingStatus::not_specified};
const SclDataSet* data_set{};
std::string canonical_reference;
std::string local_name;
};

class SclDataSetReferenceResolver final {
public:
[[nodiscard]] static SclDataSetBindingResolution resolve(
std::span<const SclDataSet> data_sets,
const std::string& ied_name,
const std::string& ld_inst,
const std::string& logical_node_path,
const std::string& raw_reference);
};

} // namespace ar::iec61850::scl
160 changes: 160 additions & 0 deletions include/ariec61850/scl/model.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// SPDX-License-Identifier: GPL-3.0-or-later

#pragma once

#include <array>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <string>
#include <vector>

namespace ar::iec61850::scl {

enum class SclEdition {
unknown,
edition1,
edition2,
edition21,
};

struct SclIed final {
std::string name;
std::string manufacturer;
std::string type;
std::string config_version;

friend bool operator==(const SclIed&, const SclIed&) = default;
};

struct SclDataSetEntry final {
std::size_t index{};
std::string signal_reference;
std::string ied_name;
std::string ld_inst;
std::string prefix;
std::string ln_class;
std::string ln_inst;
std::string do_name;
std::string da_name;
std::string functional_constraint;
std::string cdc;
std::string basic_type;
std::string type_id;
std::string enum_type;
bool is_quality{};
bool is_timestamp{};

friend bool operator==(const SclDataSetEntry&, const SclDataSetEntry&) = default;
};

struct SclDataSet final {
std::string key;
std::string ied_name;
std::string ld_inst;
std::string logical_node_path;
std::string name;
std::string reference;
std::vector<SclDataSetEntry> entries;

friend bool operator==(const SclDataSet&, const SclDataSet&) = default;
};

struct SclStreamAddress final {
std::string app_id_text;
std::optional<std::uint16_t> app_id;
std::string destination_mac_text;
std::optional<std::array<std::uint8_t, 6>> destination_mac;
std::optional<std::uint16_t> vlan_id;
std::optional<std::uint8_t> vlan_priority;

friend bool operator==(const SclStreamAddress&, const SclStreamAddress&) = default;
};

struct SclProcessBusStream {
std::string kind;
std::string ied_name;
std::string ld_inst;
std::string control_name;
std::string control_block_reference;
std::string data_set_name;
std::string data_set_reference;
std::uint32_t configuration_revision{};
SclStreamAddress address;
std::vector<SclDataSetEntry> entries;

friend bool operator==(const SclProcessBusStream&, const SclProcessBusStream&) = default;
};

struct SclGooseStream final : SclProcessBusStream {
std::string go_id;
std::uint32_t min_time_milliseconds{};
std::uint32_t max_time_milliseconds{};

friend bool operator==(const SclGooseStream&, const SclGooseStream&) = default;
};

struct SclSampledValuesStream final : SclProcessBusStream {
std::string sv_id;
std::string smv_id;
std::uint16_t sample_rate{};
std::string sample_mode;
std::uint16_t no_asdu{1U};

friend bool operator==(const SclSampledValuesStream&, const SclSampledValuesStream&) = default;
};

enum class SclDataSetBindingStatus {
not_specified,
unresolved,
resolved_empty,
resolved,
};

struct SclReportControl final {
std::string ied_name;
std::string ld_inst;
std::string logical_node_path;
std::string name;
std::string report_id;
std::string data_set_name;
std::string data_set_reference;
SclDataSetBindingStatus data_set_binding_status{SclDataSetBindingStatus::not_specified};
std::string control_block_reference;
bool buffered{};
bool indexed{true};
std::uint32_t configuration_revision{};
std::uint32_t buffer_time_milliseconds{};
std::uint32_t integrity_period_milliseconds{};
std::vector<SclDataSetEntry> entries;

friend bool operator==(const SclReportControl&, const SclReportControl&) = default;
};

struct SclConflict final {
std::string kind;
std::string key;
std::string description;

friend bool operator==(const SclConflict&, const SclConflict&) = default;
};

struct SclDocument final {
std::string source_name;
std::string namespace_uri;
std::string header_id;
std::string header_version;
std::string header_revision;
SclEdition edition{SclEdition::unknown};
std::vector<SclIed> ieds;
std::vector<SclDataSet> data_sets;
std::vector<SclGooseStream> goose_streams;
std::vector<SclSampledValuesStream> sampled_values_streams;
std::vector<SclReportControl> report_controls;
std::vector<std::string> warnings;
std::vector<SclConflict> conflicts;

friend bool operator==(const SclDocument&, const SclDocument&) = default;
};

} // namespace ar::iec61850::scl
21 changes: 21 additions & 0 deletions include/ariec61850/scl/parser.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: GPL-3.0-or-later

#pragma once

#include "ariec61850/scl/model.hpp"

#include <filesystem>
#include <string>
#include <string_view>

namespace ar::iec61850::scl {

class SclParser final {
public:
[[nodiscard]] SclDocument load(const std::filesystem::path& file_path) const;
[[nodiscard]] SclDocument parse(
std::string_view xml,
std::string source_name = {}) const;
};

} // namespace ar::iec61850::scl
Loading
Loading