sconnector_logo

SConnector C++ library

v1.1.0

Table of contents

Overview

The SConnector library implements the SAPIENT edge-node interface (UK MOD / Dstl, BSI Flex 335 v2.0). It lets a sensor edge node (for example a camera) connect to a SAPIENT fusion node, register its capabilities and tasking contract, stream status and detections, and receive and acknowledge tasking commands. The library is self-managing: a single call to init() validates the configuration and starts an internal worker thread that connects, registers, keeps the TCP link alive (reconnecting as needed), sends StatusReports on a timer, delivers incoming Tasks to a user callback and acknowledges them automatically. The library is built with C++17 and is cross-platform (Linux and Windows) — the connector uses raw sockets internally, with no external transport library. SAPIENT messages are serialised with nanopb (supplied as source under 3rdparty/, under the zlib license), so the default build has no system dependencies. An optional TLS transport, built on top of OpenSSL 1.1.1 or newer (under the OpenSSL license), is available behind a CMake flag — this is the only configuration that adds a system dependency.

SAPIENT support matrix

The tables below list the SAPIENT edge-node capabilities defined by BSI Flex 335 v2.0 and how each is covered by SConnector. Legend: ✅ supported, ⚠️ partial, ❌ not supported.

Table 1 - Messages and protocol features.

SAPIENT capability Direction SConnector Notes
Registration message edge → fusion Full mandatory tree + advertised command contract.
RegistrationAck handling fusion → edge Drives the Active state; rejection triggers reconnect.
StatusReport (periodic heartbeat) edge → fusion Sent on a timer; updatable at runtime.
StatusReport SYSTEM_GOODBYE edge → fusion Sent automatically on a clean stop(), so the fusion node releases the node ID at once.
DetectionReport edge → fusion Sent on demand.
Alert edge → fusion Sent on demand.
AlertAck handling fusion → edge ⚠️ Decoded; no automatic action taken.
Task (incoming command) fusion → edge All command types decoded and delivered to the callback.
TaskAck edge → fusion Sent automatically from the callback’s decision.
Error message both Inbound decoded and delivered to the error callback; not auto-generated for malformed input.
TCP transport + length-prefix framing 4-byte little-endian length prefix + protobuf.
Automatic connect / reconnect (FSM) Disconnected → Connecting → Registering → Active; a deterministic cryptographic fault ends in the terminal CryptoError instead of retrying.
TLS / mutual TLS Optional (OpenSSL, <PARENT>_SCONNECTOR_WITH_TLS).
FIPS 140-3 mode Optional (validated OpenSSL FIPS provider, <PARENT>_SCONNECTOR_WITH_FIPS).
UUID (v4) / ULID identifier generation Strict format (lowercase UUID v4, uppercase ULID).
Sensor edge-node role (TCP client) Primary use case.
Effector tasking fusion → edge ⚠️ Commands decoded; effector behaviour is application-level.
Fusion-node role (server) Out of scope — this is an edge-node library.
Hierarchical fusion (child/parent nodes) dependent_nodes / reporting_region not modelled.
UDP / other bearers SAPIENT is carried over TCP in practice.

Table 2 - Message content coverage (fields modelled by the C++ data model).

Message Supported fields Not modelled yet
Registration node definition (type, sub-types), capabilities, status definition (interval), modes (name/type/settle time, task with concurrent-tasks → region definition, command list), configuration data per-mode detection definitions, class/behaviour filters, performance values, scan/tracking types, dependent nodes, reporting region, taxonomy
StatusReport report id, system state, info state, mode, active task id, node location (geo), power level, free-form status entries power source/status enums, field of view, coverage, obscuration
DetectionReport report id, object id (ULID), task id, location (geo) or range/bearing, detection confidence, classification (type + confidence), behaviour, state, colour, object’s own id (e.g. tail number), track_info / object_info free-form attributes, RF signal, ENU velocity, predicted location, associated/derived detections, associated files classification sub-class recursion
Alert alert id (user-overridable), alert type, status, priority, description, location (geo) or range/bearing, region id, ranking, confidence, additional information, associated files, associated detections
Task (decode) task id/name/description, start/end times, control (start/stop/pause), region geometry (id, type, area, name), REQUEST, MODE_CHANGE, detection/report-rate/classification thresholds, LOOK_AT (azimuth/elevation or geo), MOVE_TO/PATROL (waypoints), FOLLOW (object id) region class/behaviour filters, full range/bearing cone extents
TaskAck task id, status (accepted/rejected/completed/failed), reason associated file

The “not modelled” fields are valid SAPIENT but not yet surfaced in the C++ model; they can be added without changing the wire codec’s structure. The generated nanopb code already covers the complete BSI Flex 335 v2.0 schema, so missing fields are an API-surface limit, not a wire-format one.

Versions

Table 3 - Library versions.

Version Release date What’s new
1.0.0 27.05.2026 First version. Edge-node connector for BSI Flex 335 v2.0.
1.0.1 16.07.2026 - Added datum for FoVBuf and RBBuf.
1.1.0 24.08.2026 - Removed the vendored Mbed TLS library.
- TLS transport is now built on the system OpenSSL (1.1.1 or newer).
- Added the required functionality and interface for the FIPS module.

Library files

The library is supplied as source code only, as a CMake project. The repository structure is shown below:

CMakeLists.txt --------------- Top-level CMake file of the library.
README.md -------------------- This file.
3rdparty --------------------- Vendored dependencies.
    CMakeLists.txt
    nanopb ------------------- Protobuf runtime (zlib license).
src -------------------------- Library source code.
    CMakeLists.txt ----------- CMake file.
    SConnector.h ------------- Public class declaration.
    SConnector.cpp ----------- Public class implementation (FSM + worker).
    SConnectorTypes.h -------- Public data model (config, messages, enums).
    SConnectorVersion.h ------ Version header (generated).
    SConnectorVersion.h.in --- Template for the version header.
    sapient ------------------ Generated SAPIENT v2.0 message code (nanopb).
    internal ----------------- Implementation detail (not public API):
        ITransport.h --------- Transport abstraction.
        TcpLink.h/.cpp ------- Plain TCP transport.
        TlsLink.h/.cpp ------- TLS transport (OpenSSL, optional).
        FipsContext.h/.cpp --- FIPS-validated OpenSSL module (optional).
        FrameCodec.h/.cpp ---- Length-prefix framing and reassembly.
        SapientCodec.h/.cpp -- C++ model <-> nanopb encode/decode.
        Random.h/.cpp -------- Random bytes provider.
        Ulid.h/.cpp ---------- ULID generator.
        Uuid.h/.cpp ---------- UUID generator.
test ------------------------- CTest suite (built standalone):
    CMakeLists.txt ----------- CMake file.
    TestUnit.cpp ------------- Unit tests: framing, ULID/UUID, codec, registration.
    TestIntegration.cpp ------ End-to-end FSM over TCP against a mock fusion node.
    TestTls.cpp -------------- End-to-end TLS handshake + registration (TLS build only).
    TestTlsFips.cpp ---------- End-to-end TLS in FIPS mode (FIPS build only).
SConnectorEdgeNodeTemplate --- Full example edge node (simulated camera head).
example ---------------------- Minimal example edge node (SConnectorExample).

SConnector class description

Class declaration

The SConnector class is declared in SConnector.h and lives in the cr::sapient namespace. It hides all networking and protobuf details behind a small public API; the user only ever deals with the plain C++ structs from SConnectorTypes.h.

namespace cr {
namespace sapient
{
class SConnector
{
public:

    /// Decides the fate of an incoming task; drives the automatic TaskAck.
    using TaskCallback = std::function<
        TaskDecision(const SapientTask& task, std::string& reason)>;

    /// Notified whenever the connection state changes.
    using ConnectionStateCallback = std::function<void(ConnectionState state)>;

    /// Notified when an Error message arrives from the fusion node.
    using ErrorCallback = std::function<void(const ErrorInfo& error)>;

    /// Get class version method.
    static std::string getVersion();

    /// Set incoming TASK commands callback.
    void setTaskCallback(TaskCallback cb);

    /// Set connection state change callback.
    void setConnectionStateCallback(ConnectionStateCallback cb);

    /// Set inbound Error message callback (diagnostics).
    void setErrorCallback(ErrorCallback cb);

    /// Initialization method.
    bool init(const EdgeNodeConfig& config);

    /// Stop connection.
    void stop();

    /// Get connection status.
    ConnectionState getConnectionState() const;

    /// Update status information.
    void updateStatusReport(const StatusInfo& status);

    /// Send detection report.
    bool sendDetectionReport(const Detection& detection);

    /// Send alert.
    bool sendAlert(const Alert& alert);

    /// FIPS module version this build accepts.
    static std::string getFipsModuleVersion();

    /// State of the FIPS-validated module backing this connector.
    FipsModuleState getFipsState() const;

    /// Why FIPS state is not Operational.
    FipsFailureReason getFipsFailureReason() const;

    /// Run the module's self-tests on demand.
    bool runFipsSelfTest();

    /// Approved-service indicator for the current or last TLS session.
    FipsSessionIndicator getFipsSessionIndicator() const;
};
}
}

getVersion method

The getVersion() method returns the library version string. Method declaration:

static std::string getVersion();

Returns: version string in the format "Major.Minor.Patch".

The method can be called without an SConnector instance. Example:

cout << "SConnector class version: " << SConnector::getVersion() << endl;

Console output:

SConnector class version: 1.1.0

init method

The init(…) method validates the configuration, tears down any previous session and starts the internal worker thread. Returns true when the parameters are valid and the worker started — it does not block until connected; the connection is attempted continuously in the background. Calling init() again applies a new configuration (useful when the deployment or registration content changes at runtime). Method declaration:

bool init(const EdgeNodeConfig& config);
Parameter Description
config Edge-node configuration (fusion address, identity, registration).

EdgeNodeConfig top-level fields (full declaration in src/SConnectorTypes.h):

Field Type Purpose
fusionIp std::string Fusion node IP / host. Mandatory — init() rejects an empty value.
fusionPort int Fusion node TCP port. Mandatory — must be > 0.
nodeId std::string Node UUID v4. Generated by SConnector when left empty (a fixed value keeps the node identity across restarts).
statusIntervalMs int Period between periodic StatusReports (default 1000 ms).
reconnectIntervalMs int Pause between connect attempts when the link is down (default 2000 ms).
connectTimeoutMs int Timeout for one TCP connect attempt (default 1000 ms).
registrationTimeoutMs int Maximum wait for RegistrationAck before treating the session as failed (default 5000 ms).
maxMessageSize uint32_t Inbound frame size cap in bytes — defence against oversized peers (default 1 MiB).
tlsEnabled bool If true, wrap the link in TLS. Effective only in builds with <PARENT>_SCONNECTOR_WITH_TLS=ON.
tlsCaFile std::string Path to a PEM CA bundle used to verify the fusion-node certificate. When empty, OpenSSL’s default system trust store is used.
tlsCertFile std::string Optional client certificate (for mTLS). Must be set together with tlsKeyFileinit() rejects one without the other rather than silently presenting no credential.
tlsKeyFile std::string Optional client private key (for mTLS), PEM, not passphrase-protected: init() rejects an encrypted key instead of letting OpenSSL prompt on a terminal the service may not have. In FIPS mode the file must also carry no group or other permissions at all, since a Level 1 SSP is protected only by its environment (POSIX only — see Limitations).
tlsVerifyPeer bool When true, verify the server certificate chain and hostname.
tlsRevocationCheck RevocationCheck How the server certificate’s revocation status is established: None (default), Crl (local CRLs from tlsCrlFile), OcspStapling (response stapled by the fusion node, RFC 6066) or Both. Requires tlsEnabled and tlsVerifyPeer. Every mode other than None is hard-fail: a missing, expired or unverifiable answer aborts the handshake instead of being read as “not revoked”.
tlsCrlFile std::string PEM file holding one or more CRLs, checked over the whole certification path. Mandatory for Crl and Bothinit() rejects an empty value, since that policy could only ever hard-fail. CRLs must be refreshed before they expire; an expired CRL fails the handshake. Certificates in this file are loaded the same way tlsCaFile loads them, so they become trust anchors — keep it to CRLs unless that is intended.
tlsFipsMode bool Route TLS through the FIPS-validated module and restrict the handshake to approved algorithms. Requires tlsEnabled, tlsVerifyPeer and tlsRevocationCheck != Noneinit() rejects any other combination. Effective only in builds with <PARENT>_SCONNECTOR_WITH_FIPS=ON.
tlsFipsModuleDir std::string Directory holding fips.so. When empty, OpenSSL’s default module path is used.
tlsFipsConfigFile std::string openssl.cnf that includes fipsmodule.cnf (which carries the module’s integrity MAC). When empty, the default openssl.cnf is used.
registration RegistrationInfo Content of the Registration message sent on connect. See SConnectorTypes.h for the nested fields.
initialStatus StatusInfo First StatusReport content (its mode must be non-empty). See SConnectorTypes.h for the nested fields.

Returns: true if the parameters are valid and the worker started, otherwise false.

stop method

The stop() method stops the connector: disconnects from the fusion node and joins the worker thread. Called automatically by the destructor. If the node is registered when stop() is called, a final StatusReport with SystemStatus::Goodbye (SYSTEM_GOODBYE) is sent first, so the fusion node releases the node ID immediately instead of waiting for its own disconnect timeout. Method declaration:

void stop();

getConnectionState method

The getConnectionState() method returns the current connection state. The connector is registered and operational only in the ConnectionState::Active state. Method declaration:

ConnectionState getConnectionState() const;

Returns: current ConnectionState (Disconnected, Connecting, Registering, Active or CryptoError).

updateStatusReport method

The updateStatusReport(…) method replaces the StatusReport content. The new content is sent on the next worker cycle and used for all subsequent periodic reports. Method declaration:

void updateStatusReport(const StatusInfo& status);
Parameter Description
status New status report content.

StatusInfo top-level fields (full declaration in src/SConnectorTypes.h):

Field Type Purpose
system SystemStatus Overall node health: Ok / Warning / Error. Goodbye is set by the library on a clean stop(); the user normally leaves Ok.
info InfoStatus Whether the report content is New or Unchanged since the last report.
mode std::string Current operating mode name. Mandatory non-empty — strict fusion nodes reject a StatusReport without it.
activeTaskId std::string ULID of the task currently being executed (optional).
location std::optional<GeoPoint> Current node geographic position (lon/lat/alt).
fieldOfView std::optional<FieldOfView> Current sensor field of view (cone: pointing direction + horizontal/vertical extents + range).
power std::optional<PowerInfo> Power-supply state (battery level 0..100).
values std::vector<StatusValue> Repeated free-form {level, type, value} entries for anything the standard doesn’t model directly (PTZ position, sensor flags, …).

sendDetectionReport method

The sendDetectionReport(…) method sends a DetectionReport immediately. The objectId is filled with a fresh ULID if left empty. Method declaration:

bool sendDetectionReport(const Detection& detection);
Parameter Description
detection Detection content.

Detection top-level fields (full declaration in src/SConnectorTypes.h):

Field Type Purpose
objectId std::string ULID of the detected object. Stable across reports of the same object — use the same id while tracking. Generated by the library if empty.
taskId std::string ULID of the task this detection relates to (e.g. an active FOLLOW). Optional.
location std::optional<GeoPoint> Object position in geographic coordinates. One of location / rangeBearing is required (sendDetectionReport() rejects when both unset).
rangeBearing std::optional<Bearing> Object position in the sensor’s range/bearing frame (alternative to location).
confidence std::optional<float> Detection confidence in [0..1].
classification std::vector<Classification> Class hypotheses (type + confidence) — repeated; one per candidate class.
behaviour std::vector<Behaviour> Behaviour hypotheses (type + confidence) — e.g. "loitering".
state std::string Free-form state, e.g. "tracking" / "lost".
colour std::string Object colour (optional).
id std::string The object’s own identifier (e.g. an aircraft tail number) — distinct from objectId.
trackInfo std::vector<ObjectInfo> Repeated {type, value, error} free-form attributes mapped to the proto’s track_info[].
objectInfo std::vector<ObjectInfo> Repeated {type, value, error} free-form attributes for data the standard does not model (e.g. pixel coordinates, RCS, ML features).
signal std::vector<SignalInfo> RF signal characteristics (amplitude, frequencies, pulse duration).
associatedFile std::vector<AssociatedFile> URLs of external files related to this detection (image, audio, …).
associatedDetection std::vector<AssociatedDetection> Cross-references to detections on other nodes (with parent/child/sibling relation).
derivedDetection std::vector<AssociatedDetection> Detections this one was derived from (e.g. a fused track from per-sensor detections).
velocity std::optional<VelocityENU> Object velocity vector in the ENU frame.
prediction std::optional<PredictedLocation> Where the object is expected to be at a future timestamp.

Returns: true if the message was written to the socket (requires the Active state), otherwise false.

sendAlert method

The sendAlert(…) method sends an Alert immediately. Method declaration:

bool sendAlert(const Alert& alert);
Parameter Description
alert Alert content.

Alert top-level fields (full declaration in src/SConnectorTypes.h):

Field Type Purpose
alertId std::string ULID of the alert. Generated by the library if empty; supply your own to correlate alerts with external state.
alertType std::optional<AlertType> Severity class: Information / Warning / Critical / Error / Fatal / ModeChange.
status AlertStatusType Alert state: Active (default) / Acknowledge / Reject / Ignore / Clear.
priority std::optional<AlertPriority> Discrete priority: Low / Medium / High.
description std::string Human-readable description, typically shown on a fusion-node GUI.
location std::optional<GeoPoint> Alert position in geographic coordinates. One of location / rangeBearing is required (sendAlert() rejects when both unset).
rangeBearing std::optional<Bearing> Alert position in the sensor’s range/bearing frame (alternative to location).
regionId std::string ULID of the region this alert is from (optional).
ranking std::optional<float> Alert ranking in [0..1].
confidence std::optional<float> Confidence that the alert is not a false alarm, in [0..1].
additionalInformation std::string Free-form additional note (optional).
associatedFile std::vector<AssociatedFile> URLs of related files (image, audio, …).
associatedDetection std::vector<AssociatedDetection> Detections this alert is associated with (parent/child/sibling).

Returns: true if the message was written to the socket (requires the Active state), otherwise false.

setTaskCallback method

The setTaskCallback(…) method sets the callback invoked for each incoming Task. The callback runs on the internal worker thread; the TaskDecision it returns drives the automatic TaskAck the library sends back. Populate the reason argument to explain a rejection. Method declaration:

void setTaskCallback(TaskCallback cb);
Parameter Description
cb TaskDecision(const SapientTask& task, std::string& reason).

SapientTask top-level fields (the value delivered to the callback; full declaration in src/SConnectorTypes.h):

Field Type Purpose
taskId std::string ULID assigned by the fusion node; echo it in Detection::taskId while you act on the task.
taskName std::string Human-readable task name.
taskDescription std::string Free-form description.
taskStartTimeSec std::optional<double> Requested start time (UTC seconds since epoch).
taskEndTimeSec std::optional<double> Requested end time (UTC seconds since epoch).
control TaskControl Lifecycle action: Start / Stop / Pause. A Stop for a running task means cancel it.
commandType TaskCommandType Which command the task carries — selects which payload field below is populated.
request std::string Set when commandType == Request: vendor-specific verb (e.g. "FIRE", "DAY_ZOOM_IN").
modeChange std::string Set when commandType == ModeChange: target mode name.
commandParameter std::string Generic parameter string accompanying any command (e.g. "x,y" pixel coords).
threshold DiscreteThreshold Set when commandType ∈ {DetectionThreshold, DetectionReportRate, ClassificationThreshold}: Low / Medium / High.
lookAt LookAtTarget Set when commandType == LookAt: target as geographic location or range/bearing.
locations std::vector<GeoPoint> Set when commandType ∈ {MoveTo, Patrol}: waypoint list.
followObjectId std::string Set when commandType == Follow: ULID of the object to track (the same objectId your detector previously reported).
region std::vector<TaskRegion> Optional geo-fence regions attached to the task (id, type, area polygon, name). Filters inside each region are not surfaced.

setConnectionStateCallback method

The setConnectionStateCallback(…) method sets the callback invoked whenever the connection state changes. Method declaration:

void setConnectionStateCallback(ConnectionStateCallback cb);
Parameter Description
cb void(ConnectionState state).

setErrorCallback method

The setErrorCallback(…) method sets the callback invoked when an Error message is received from the fusion node — useful for diagnosing rejected messages. Method declaration:

void setErrorCallback(ErrorCallback cb);
Parameter Description
cb void(const ErrorInfo& error).

ErrorInfo top-level fields (the value delivered to the callback; full declaration in src/SConnectorTypes.h):

Field Type Purpose
messages std::vector<std::string> One or more diagnostic messages from the fusion node explaining what it could not accept (e.g. mandatory-field rejections).

getFipsModuleVersion method

The getFipsModuleVersion() method returns accepted FIPS module version. Method declaration:

static std::string getFipsModuleVersion();

Returns: Version string, e.g. “3.1.2”; empty without <PARENT>_SCONNECTOR_WITH_FIPS.

getFipsState method

The getFipsState() method reports the lifecycle state of the FIPS-validated module backing this connector. Like the other FIPS methods it is present in every build — without <PARENT>_SCONNECTOR_WITH_FIPS they report the “not built” values, so calling code needs no #ifdef — and safe to call from any thread. Method declaration:

FipsModuleState getFipsState() const;

FipsModuleState values (full declaration in src/SConnectorTypes.h):

Value Meaning
NotBuilt Library built without <PARENT>_SCONNECTOR_WITH_FIPS; FIPS mode is unavailable.
NotRequested FIPS-capable build, but tlsFipsMode is not set.
NotLoaded Internal to the transport. init() loads the module eagerly, so getFipsState() never returns this.
Operational Loaded, pre-operational self-tests passed, run-time security checks on, module version as pinned at build time.
Error Requested but unusable. The connector refuses to fall back, so no TLS session is established: it stops retrying and reports ConnectionState::CryptoError. getFipsFailureReason() says what to fix.

Returns: current FipsModuleState.

getFipsFailureReason method

The getFipsFailureReason() method says why getFipsState() is not Operational. Method declaration:

FipsFailureReason getFipsFailureReason() const;

Paired one-to-one with FipsModuleState: NotBuiltNotBuiltIn, NotRequestedNotRequested, NotLoaded and OperationalNone, Error → anything else.

FipsFailureReason values (full declaration in src/SConnectorTypes.h):

Value Meaning
None Nothing failed (see the pairing above).
NotBuiltIn Built without <PARENT>_SCONNECTOR_WITH_FIPS.
NotRequested tlsFipsMode is not set.
ModulePathInvalid tlsFipsModuleDir rejected by OpenSSL.
ConfigLoadFailed tlsFipsConfigFile missing or malformed, or its .include of fipsmodule.cnf does not resolve.
BaseProviderMissing No base provider, so PEM decoders are unavailable.
ModuleLoadFailed fips.so absent or unloadable, which includes a failed integrity (MAC) check — OpenSSL reports both the same way.
SelfTestFailed Pre-operational self-tests did not pass, or a runFipsSelfTest() call failed.
SecurityChecksDisabled security-checks off; the module’s Security Policy 11.1c forbids disabling it.
EmsCheckDisabled tls1-prf-ems-check off, so RFC 7627 would not be enforced for TLS 1.2.
TruncDigestAllowed drbg-no-trunc-md off.
ModuleVersionMismatch Not the module version this build pins — see getFipsModuleVersion().
ModulePathConflict Another connector in this process already loaded the module from different paths.
PolicyApplyFailed libssl rejected the approved-algorithm restrictions, so they were never in force.
InternalError Allocation or OpenSSL API failure.

Most of these point at the module installation rather than at the application. SecurityChecksDisabled, EmsCheckDisabled and TruncDigestAllowed all mean the same thing — the module was configured without the approved-mode settings — and one fipsinstall -pedantic clears all three.

Returns: failure reason; None when nothing failed.

runFipsSelfTest method

The runFipsSelfTest() method runs the validated module’s self-tests on demand. It loads the module if the connector has not connected yet, so it doubles as a pre-flight check on a node with no network path, and blocks this connector’s TLS I/O while the tests run — safe to call from a task or connection-state callback, but the link stalls until the tests finish. Method declaration:

bool runFipsSelfTest();

A failure is permanent for the life of the process: the session is torn down, getFipsState() stays Error with SelfTestFailed, and later reconnects will not reinstate the module. That is the error state ISO/IEC 19790 7.10 requires, not a transient fault to retry — the process has to be restarted.

Returns: true if the module is usable and every self-test passed. Also false when FIPS mode is not active at all; getFipsState() tells that apart from a genuine failure.

getFipsSessionIndicator method

The getFipsSessionIndicator() method returns the approved-service indicator for the current or last TLS session (FIPS 140-3 IG 2.4.C). It answers a different question from getFipsState(): the state says the validated module is loaded and healthy, this says the handshake actually ran on approved algorithms. Both are needed, because a validated module also implements non-approved services — it registers X25519/X448 with fips=yes. Approved operation is getFipsState() == Operational and this indicator’s approved. Outside FIPS mode it stays default-valued. Method declaration:

FipsSessionIndicator getFipsSessionIndicator() const;

FipsSessionIndicator fields (full declaration in src/SConnectorTypes.h):

Field Type Purpose
approved bool true only if every field below satisfies the approved-mode rules.
tlsVersion std::string e.g. "TLSv1.3"; empty when no handshake has completed yet.
cipherSuite std::string e.g. "TLS_AES_256_GCM_SHA384".
keyExchangeGroup std::string e.g. "secp384r1", "ffdhe3072".
peerSignatureDigest std::string Digest the peer signed the handshake with.
extendedMasterSecret bool RFC 7627. Required for approved TLS 1.2; not applicable to TLS 1.3, where it is reported true.

Every field is measured from the finished handshake, never copied back from the policy the client asked for — the value of the indicator is precisely that it can disagree with the request. In FIPS mode a session measured as not approved is refused rather than used, so a live connection always carries approved == true; the fields of a refused or dropped session stay readable for diagnosis.

A handshake that completes on non-approved algorithms is treated as a deterministic fault: the connector stops retrying and reports ConnectionState::CryptoError, because a peer that just negotiated the wrong parameters will negotiate them again. A handshake that never completes — a peer with no approved algorithm to offer at all — is a normal retryable failure, and getFipsState() stays Operational because the module is not at fault.

Scope is the algorithms, not the protocol: per FIPS 140-3 IG D.C no part of TLS beyond the approved algorithms and KDFs has been tested by the CAVP or CMVP, so certification-path strength and revocation are outside what these fields can show.

Returns: snapshot of the session’s negotiated parameters.

Data model

All message types are plain C++ structs declared in SConnectorTypes.h; the generated nanopb types stay hidden inside the library.

Type Purpose
EdgeNodeConfig Passed to init(): fusion address/port, node UUID, intervals, TLS settings, registration content, initial status.
RegistrationInfo Node identity, capabilities, status interval, modes (with the declared command contract) and configuration data.
RegistrationMode One operating mode; commands advertises the accepted commands (CommandDef).
StatusInfo StatusReport content: system/info state, mode, active task id, location, power, free-form StatusValue entries.
Detection DetectionReport content: object/task ids, position (location or rangeBearing), confidence, classification, behaviour, free-form trackInfo/objectInfo (e.g. pixel coordinates), ENU velocity, prediction, RF signal, associated/derived detections, files, colour, id.
Alert Alert content: alert id (user-overridable), alert type, status, priority, description, position (location or rangeBearing), region id, ranking, confidence, additional info, associated files/detections.
SapientTask Incoming command from the fusion node: ids, start/end times, control, geo-fence regions, commandType plus the matching payload (request, modeChange, threshold, lookAt, locations, followObjectId).
IncomingMessage Generic decoded inbound message (used internally for acks/errors).

The connection state, task decision and SAPIENT enums (ConnectionState, TaskDecision, TaskCommandType, NodeType, etc.) are also declared here.

Limitations

SConnector covers the edge-node side of BSI Flex 335 v2.0 end-to-end and is accepted as a full SAPIENT node by the official Dstl DMM Simulator. The wire codec is generated from the complete v2.0 schema, so every field that is not modelled below is an API-surface limitation, not a wire-format one — adding it is a small extension to SapientCodec and SConnectorTypes.h.

Architectural scope

  • Edge-node role only. The library implements the edge (ASM) side: connect → register → status / detection / alert / task. The fusion-node role (accepting connections from other nodes, sending tasks, validating registrations) is out of scope.
  • No hierarchical fusion. A SAPIENT fusion node may register child nodes via dependent_nodes / reporting_region; SConnector does not surface these. A child node connecting to an SConnector-based parent is not possible.
  • Plain TCP transport. SAPIENT is bearer-agnostic but the only deployed bearer is TCP, and that is what SConnector implements (with optional TLS via OpenSSL). UDP and other transports are not provided.

SAPIENT message fields not yet surfaced

Message Not modelled Workaround
Registration dependent_nodes, reporting_region, per-mode detection_definition, scan/tracking types, performance values, taxonomy None — purely additive when needed.
StatusReport coverage[] / obscuration[] repeated areas; Power.source / Power.status enums (only the level is exposed) field_of_view (single cone) is supported via StatusInfo::fieldOfView.
DetectionReport Recursive Classification.sub_class[] hierarchy Use flat classification[]; embed depth hints in the type string if needed.
Alert Full coverage.
Task (inbound) Task.Region.classFilter / behaviourFilter Region geometry (id / type / area / name) is decoded; filter strings can be added on request.
AlertAck (inbound) The library decodes the message but does not yet expose it through a public callback. No application-level access today; a setAlertAckCallback entry point will be added when there is a concrete need.

Build and platform

  • C++17 is required (uses std::optional, structured bindings).
  • TLS is built on OpenSSL ≥ 1.1.1, which must be installed on the system (headers included). It is the library’s only external dependency and is required only when <PARENT>_SCONNECTOR_WITH_TLS=ON. The client negotiates TLS 1.2 as the minimum version. With tlsVerifyPeer = true and an empty tlsCaFile, the OpenSSL default trust store is used; set tlsCaFile to pin a private CA.
  • FIPS mode (<PARENT>_SCONNECTOR_WITH_FIPS=ON) raises the build requirement to OpenSSL ≥ 3.0 — the provider architecture does not exist earlier, and no validated module exists for the 1.1.1 series — and implies <PARENT>_SCONNECTOR_WITH_TLS. The validated module is loaded into a private OpenSSL library context, so it never changes how a host application uses OpenSSL.
  • The private-key permission check is POSIX-only. In FIPS mode init() rejects a key file readable beyond its owner, on the premise that a Level 1 SSP is protected by its environment. Windows has no comparable mode bits, so that check is skipped there while tlsFipsMode is still accepted; an ACL equivalent is not implemented. Refusal of a passphrase-protected key works on both platforms.
  • The integration test (SConnectorTestIntegration) is POSIX-only — it builds a mock fusion node over raw sockets. On Windows it is a no-op stub. The library itself (TcpLink / TlsLink) runs on both Linux and Windows.

Concurrency model

  • init() and stop() are serialised against each other by an internal mutex (safe to call from any thread). The destructor force-closes the link.
  • The task / connection-state / error callbacks are invoked on the library’s worker thread; do not block them for long.
  • Calling sendDetectionReport / sendAlert / updateStatusReport concurrently with init() / stop() is supported but the sends will return false once the link is being torn down. The library is one worker thread per SConnector instance — there is no shared thread pool.

API contracts to be aware of

  • sendDetectionReport() and sendAlert() return false if both location and rangeBearing are unset — SAPIENT mandates a position on both messages.
  • StatusInfo::mode must be non-empty (a strict fusion node rejects a StatusReport without a mode).
  • RegistrationMode::settleTimeSec and concurrentTasks must be > 0 and >= 1 respectively (proto3 presence: zero is treated as “unset”).

Example

The smallest complete edge node built on SConnector lives in example/ and is built as the SConnectorExample target. In under 90 lines it connects to a fusion node, registers as a video camera, prints the name of every incoming Task, and sends a DetectionReport and an Alert once per second — the kind of minimal node the DMM Simulator accepts as a full edge node.

#include <atomic>
#include <chrono>
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <thread>
#include <iostream>
#include "SConnector.h"

// Link namespaces.
using namespace std;
using namespace std::chrono;
using namespace cr::sapient;

// Entry point.
int main(int argc, char** argv)
{
    // SConnector object.
    SConnector conn;

    // Set Task callback. The fusion node sends Tasks to command the edge node;
    // Print the name of every Task the fusion node sends, and accept it.
    conn.setTaskCallback(
        [](const SapientTask& task, std::string&) -> TaskDecision
        {
            cout << "[task] " << task.taskName << endl;
            return TaskDecision::Accepted;
        });

    // Minimal configuration: an address, a name and a non-empty status mode.
    // SConnector fills the rest of the mandatory registration tree
    // (capabilities, mode, region, config data) with sensible defaults.
    EdgeNodeConfig cfg;
    cfg.fusionIp = "172.23.144.1"; // Put your fusion node IP.
    cfg.fusionPort = 14000; // Put your fusion node port.
    cfg.registration.name = "Example Camera";
    cfg.registration.nodeType = NodeType::Camera;
    cfg.initialStatus.mode = "default";

    // Init SConnector with the minimal configuration.
    if (!conn.init(cfg))
    {
        std::cerr << "init failed: invalid configuration" << std::endl;
        return 1;
    }
    cout << "SConnector v" << SConnector::getVersion() << " initialized for " <<
    cfg.fusionIp << ":" << cfg.fusionPort << " ..." << endl;

    // A fixed site location shared by the detection and the alert.
    const GeoPoint site{-1.2577, 51.7520, {}};

    // Once a second, while registered, send one DetectionReport and one Alert.
    while (true)
    {
        // Each second (for tests) send detection + alert to fusion node..
        this_thread::sleep_for(seconds(1));

        // Check connection.
        if (conn.getConnectionState() != ConnectionState::Active)
            continue;

        // Send detection results.
        Detection detection;
        detection.location   = site;
        detection.confidence = 0.9f;
        detection.classification.push_back({"vehicle", 0.8f});
        conn.sendDetectionReport(detection);

        // Send alert about the detection.
        Alert alert;
        alert.description = "Object detected";
        alert.priority    = AlertPriority::Medium;
        alert.location    = site;
        conn.sendAlert(alert);

        cout << "[sent] detection + alert\n";
    }

    return 0;
}

The whole node fits into three steps:

  1. A task callback — invoked on SConnector’s worker thread for every Task the fusion node sends; here it just prints the task name and accepts it (return TaskDecision::Rejected with a reason to refuse a command).
  2. Minimal configuration — only the fusion address, a node name and a non-empty status mode are set. SConnector fills the rest of the mandatory SAPIENT registration tree (capabilities, an operating mode with its region, and config data) with sensible defaults, so this minimal node still registers with a strict fusion node.
  3. Periodic reporting — once a second, while the link is Active, it sends one DetectionReport and one Alert.

That is the entire public surface needed to get a node on the bus: setTaskCallback(), init(), getConnectionState(), sendDetectionReport(), sendAlert() and stop().

A standalone build produces ./build/bin/SConnectorExample. The fusion address is set in main.cpp (cfg.fusionIp / cfg.fusionPort); port 14000 is the DMM Simulator’s Data Agent port, so the example points at the test harness out of the box (see Testing against the DMM Simulator). The example loops indefinitely and is meant to be stopped with SIGTERM / Ctrl-C; for a graceful SYSTEM_GOODBYE on shutdown, wire a signal handler that calls conn.stop() (see SConnectorEdgeNodeTemplate for a full lifecycle example).

./build/bin/SConnectorExample

SConnectorEdgeNodeTemplate

SConnectorEdgeNodeTemplate/ is a complete, worked example edge node built on the public SConnector API. It is both a usage template to copy from and a manual integration test against a fusion node.

What it simulates

The template simulates (it does not render any image) a pan/tilt camera sensor head consisting of:

  • Day camera — controllable zoom (0..65535 units) and focus (0..65535 units). The field of view is derived from the zoom: 20°..1° horizontally and 12°..0.5° vertically (wide at zoom 0, telephoto at zoom 65535).
  • Thermal camera — identical zoom/focus/field-of-view behaviour.
  • Pan/tilt platform — carries both cameras; pan wraps to [-180°, 180°), tilt clamps to [-90°, 90°], with a commanded speed of up to 200°/s.
  • Video tracker — state only (no real tracking): FREE or TRACKING, with an X/Y target in a FullHD (1920×1080) frame. A capture latches the target and switches to TRACKING; a reset returns to FREE.
  • Object detectorON/OFF; while ON it emits a batch of detections with random coordinates roughly once per second, and raises an Alert when an object is detected with high confidence.

How it works

  1. Registration. On start, the template fills an EdgeNodeConfig that declares the node as a Camera, lists its capabilities (day camera, thermal camera, pan/tilt platform, video tracker, object detector) and advertises its command contract (RegistrationMode::commands). The fusion node reads this contract to discover which commands the head accepts.
  2. Connection. init() starts the worker thread, which connects to the fusion node, registers and reaches the Active state. State changes are printed via the connection-state callback.
  3. Commands. Incoming Tasks are delivered to the task callback, which applies them to the simulation and returns Accepted/Rejected — the library sends the matching TaskAck automatically. Standard SAPIENT command types are honoured (see below); commands meant for mobile nodes (MOVE_TO/PATROL/FOLLOW) are rejected because the head is fixed.
  4. Reporting. A loop advances the simulation (zoom/focus drives, pan/tilt motion) every 50 ms. Once per second it refreshes the StatusReport with the current device state (zoom, focus, field of view, pan, tilt, tracker mode, detector state) via updateStatusReport(), and — while the detector is ON — sends DetectionReports for the random objects. When a detection exceeds a high-confidence threshold the template also raises an Alert via sendAlert().

The template exercises the entire public SConnector API, including getVersion() (printed at startup), init()/stop(), getConnectionState(), updateStatusReport(), sendDetectionReport(), sendAlert() and both callbacks.

Command vocabulary

The head maps to standard SAPIENT command types where they exist, and uses the generic REQUEST channel for camera-specific verbs that have no standard equivalent:

SAPIENT command type Used for
LOOK_AT Aim the platform at an azimuth/elevation (or location).
DETECTION_REPORT_RATE Detector reporting rate (turns the detector on).
DETECTION_THRESHOLD Detector sensitivity.
CLASSIFICATION_THRESHOLD Classification sensitivity.
MODE_CHANGE Switch the active sensing mode.
REQUEST Zoom / focus / continuous PTZ speed / tracker / detector verbs.

The REQUEST verbs are carried in Task.command.request plus Task.command_parameter. With <CAM> standing for DAY or THERMAL:

<CAM>_ZOOM_IN | <CAM>_ZOOM_OUT | <CAM>_ZOOM_STOP
<CAM>_ZOOM_TO_POSITION       param: 0..65535
<CAM>_ZOOM_TO_FOV            param: horizontal degrees (1..20)
<CAM>_FOCUS_FAR | <CAM>_FOCUS_NEAR | <CAM>_FOCUS_STOP
<CAM>_FOCUS_TO_POSITION      param: 0..65535
PLATFORM_MOVE_PAN_SPEED      param: -200..200 (deg/s)
PLATFORM_MOVE_TILT_SPEED     param: -200..200 (deg/s)
PLATFORM_MOVE_TO_HORIZONTAL  param: degrees
PLATFORM_MOVE_TO_VERTICAL    param: degrees
PLATFORM_STOP
TRACKER_RESET
TRACKER_CAPTURE              param: "x,y" (FullHD pixels 0..1919,0..1079)
DETECTOR_ON | DETECTOR_OFF

Running it

./build/bin/SConnectorEdgeNodeTemplate <fusionIp> <fusionPort> [nodeId]   # default 127.0.0.1 5000

nodeId is optional — if omitted, the template uses a fixed UUID so the fusion node tasks the same identity across restarts. A clean shutdown (Ctrl-C) sends SYSTEM_GOODBYE, which releases the ID at once, so re-running does not trip an “ID already in use” conflict. Pass an explicit nodeId to override it, or an empty string to have SConnector generate a unique one per run.

On startup the program prints the linked library version (SConnector v1.1.0). It then prints connection-state changes, received commands, detector activity, raised alerts and any errors from the fusion node, and runs until interrupted (Ctrl-C).

Testing against the DMM Simulator

The most practical conformance check is to run an edge node against the official Dstl test harness. Its DMM Simulator (Decision Making Module) plays the role of a SAPIENT fusion node: it accepts the edge node’s connection, validates every message against BSI Flex 335 v2.0, sends Tasks and reports any non-conformance with an Error. Reaching a steady exchange with no Error is a strong signal that the node is wire-compatible with the standard.

Links:

The edge node connects to the harness over plain TCP on the DMM Data Agent port, which is 14000 by default (the harness setting DACommunicationPort). Note that this differs from SConnectorEdgeNodeTemplate’s own default of 5000, so the port must be given explicitly.

  1. Start the DMM Simulator from the test harness (follow its own README).
  2. Run the example node against the Data Agent port:

    ./build/bin/SConnectorEdgeNodeTemplate 127.0.0.1 14000
    
  3. Watch the DMM Simulator log. In order, you should see: the Registration accepted (RegistrationAck { acceptance: true }), periodic StatusReports, the Tasks the DMM sends (for example LOOK_AT) answered with a TaskAck, and — once the detector is tasked on — DetectionReports and Alerts. No Error message means the node’s messages are accepted.

Notes:

  • The template registers with a fixed node ID so the DMM tasks the same identity across restarts. On a clean Ctrl-C it sends SYSTEM_GOODBYE, releasing the ID immediately; otherwise the harness holds it for DisconnectIntervalSecs (default 60 s) and a quick restart is refused with “Another ASM is using this ID”. Pass a different nodeId as the third argument to run a second node alongside the first.
  • The DMM addresses its Tasks to the registered node via destinationId; if you change the node ID, make sure the harness is tasking the same one.
  • TLS is not required for the harness — it speaks plain TCP on the Data Agent port. The library’s optional TLS transport is for deployments that secure the link to the fusion node.

Build and connect to your project

The library requires CMake ≥ 3.13 and a C++17 compiler. The default build needs nothing else; only <PARENT>_SCONNECTOR_WITH_TLS=ON additionally requires OpenSSL ≥ 1.1.1 with its development headers.

Install the OpenSSL development package — for TLS builds only. On Debian / Ubuntu:

sudo apt install libssl-dev

Other distributions ship it as openssl-devel (Fedora / RHEL) or openssl (Arch).

Build the library:

cmake -S . -B build
cmake --build build -j

As a standalone project this also builds the tests and the SConnectorEdgeNodeTemplate and SConnectorExample programs. When SConnector is included as a submodule, all three are off by default.

CMake options:

Option Default Effect
<PARENT>_SCONNECTOR_WITH_TLS OFF Build the TLS transport (requires OpenSSL ≥ 1.1.1).
<PARENT>_SCONNECTOR_WITH_FIPS OFF Route TLS through a FIPS-validated OpenSSL module. Implies _WITH_TLS and raises the build requirement to OpenSSL ≥ 3.0.
<PARENT>_SCONNECTOR_TEST ON standalone Build the CTest suite.
<PARENT>_SCONNECTOR_TEMPLATE ON standalone Build the SConnectorEdgeNodeTemplate example.
<PARENT>_SCONNECTOR_EXAMPLE ON standalone Build the minimal SConnectorExample.
SCONNECTOR_FIPS_MODULE_VERSION 3.1.2 Accepted FIPS module version.

These options live in the CONFIGURATION block of the top-level CMakeLists.txt and follow the house convention: they are force-written to the cache (controlled by <PARENT>_SUBMODULE_CACHE_OVERWRITE, ON by default). <PARENT> is the namespace a parent project sets; in a standalone build it is empty, so the cache entries are _SCONNECTOR_WITH_TLS, _SCONNECTOR_TEST, etc.

Enabling TLS. Flip the flag to ON in the CONFIGURATION block of the top-level CMakeLists.txt, or have a parent project set it. Because the option is force-written in a standalone build, a plain -D is overridden on reconfigure; to override from the command line, also disable the forced rewrite:

cmake -S . -B build -D_SUBMODULE_CACHE_OVERWRITE=OFF -D_SCONNECTOR_WITH_TLS=ON

OpenSSL is located with find_package(OpenSSL 3.0 REQUIRED) if FIPS mode is enabled, otherwise OpenSSL is located with find_package(OpenSSL 1.1.1 REQUIRED). If it is installed outside the default search paths (a custom prefix, or a Windows build), point CMake at it with -DOPENSSL_ROOT_DIR=<prefix>.

Enabling TLS with FIPS. In FIPS mode the TLS transport runs against a FIPS-validated OpenSSL module instead of the system provider, and the client offers approved algorithms only. The library performs no cryptography of its own: all of it comes from the OpenSSL FIPS Provider — CMVP certificate #4985, module version 3.1.2, FIPS 140-3 Level 1, sunset 10 March 2030 — loaded into a private OpenSSL library context. As required by FIPS 140-3 IG D.C, no part of the TLS protocol other than the approved cryptographic algorithms and KDFs has been tested by the CAVP or CMVP.

The module is not part of a normal OpenSSL installation and has to be built from the 3.1.2 sources. The version is what the certificate is issued to, so a module built from any other release is not the validated one — hence the checksum:

curl -LO https://github.com/openssl/openssl/releases/download/openssl-3.1.2/openssl-3.1.2.tar.gz
echo "a0ce69b8b97ea6a35b96875235aa453b966ba3cba8af2de23657d8b6767d6539  openssl-3.1.2.tar.gz" | sha256sum -c -
tar xf openssl-3.1.2.tar.gz && cd openssl-3.1.2

./Configure enable-fips --prefix=$HOME/opt/openssl-fips --openssldir=$HOME/opt/openssl-fips/ssl --libdir=lib
make -j"$(nproc)"
make install_fips

install_fips installs only fips.so and fipsmodule.cnf, leaving the system OpenSSL untouched — and the 3.1.2 module works with any OpenSSL 3.x libcrypto, for example the 3.0.13 shipped by Ubuntu 24.04 LTS. Regenerate the module configuration with the binary just built, whose -pedantic applies the settings the module’s Security Policy requires for the approved mode (security-checks, conditional-errors, drbg-no-trunc-md, tls1-prf-ems-check):

./apps/openssl fipsinstall -pedantic \
    -module $HOME/opt/openssl-fips/lib/ossl-modules/fips.so \
    -out    $HOME/opt/openssl-fips/ssl/fipsmodule.cnf

fipsmodule.cnf carries the module’s integrity MAC but does not activate it, so the library also needs a configuration file that includes it:

cat > $HOME/opt/openssl-fips/ssl/openssl.cnf <<EOF
config_diagnostics = 1
openssl_conf = openssl_init

.include $HOME/opt/openssl-fips/ssl/fipsmodule.cnf

[openssl_init]
providers = provider_sect

[provider_sect]
fips = fips_sect
base = base_sect

[base_sect]
activate = 1
EOF

Then configure SConnector. _SCONNECTOR_WITH_FIPS implies _SCONNECTOR_WITH_TLS, so two flags are enough — but as with TLS the forced cache rewrite has to be disabled, or the option is silently reset to OFF:

cmake -S . -B build -D_SUBMODULE_CACHE_OVERWRITE=OFF -D_SCONNECTOR_WITH_FIPS=ON

The accepted module version is pinned at build time: the certificate is issued to a version, not to “the FIPS provider”, so a module built from another release is refused instead of silently accepted. It defaults to 3.1.2 (certificate #4985) and is changed with -DSCONNECTOR_FIPS_MODULE_VERSION=<x.y.z>.

The build flag only makes FIPS mode available. Each connector opts in through tlsFipsMode and locates the module with tlsFipsModuleDir / tlsFipsConfigFile; without tlsFipsMode a FIPS-capable build behaves exactly like a plain TLS build and never loads the module.

In FIPS mode the client offers only AES-GCM cipher suites, NIST curves and RFC 7919 groups, and SHA-256/384 signature algorithms, and enforces a 112-bit minimum security level. It also sends the status_request extension in every ClientHello, which SP 800-52r2 §4.4.1.5 makes mandatory for a client regardless of the configured tlsRevocationCheck; an unanswered request is fatal only when OCSP stapling was actually requested. tlsVerifyPeer = false is refused — an approved mode cannot skip certification-path validation. One interoperability consequence: the module’s approved-mode configuration requires RFC 7627 Extended Master Secret for TLS 1.2, so a fusion node that does not implement it will fail the handshake. TLS 1.3 is unaffected.

Tests:

The CTest suite has four programs (the CTest names are unit, integration, tls, tls-fips):

  • SConnectorTestUnit (CTest: unit) — pure-logic unit tests, no sockets, cross-platform. Verifies the length-prefix framing (4-byte little-endian prefix, reassembly across reads, oversize-frame rejection), the UUID v4 and ULID generators (format, bulk uniqueness, ULID monotonicity), and the SAPIENT codec: encoding of StatusReport / DetectionReport / Alert / TaskAck and the full mandatory Registration tree, plus decoding of RegistrationAck and Task (REQUEST, LOOK_AT) and graceful handling of unsupported / garbage input.
  • SConnectorTestIntegration (CTest: integration) — end-to-end test of the connection FSM over a real TCP loopback against a mock fusion node (POSIX only; a no-op on Windows). Verifies config validation, connect → register → Active, periodic StatusReports, sendDetectionReport / sendAlert, an inbound Task reaching the callback with the matching automatic TaskAck, the SYSTEM_GOODBYE sent on a clean stop(), the destructor forcibly closing the link (with SYSTEM_GOODBYE) when a SConnector goes out of scope without an explicit stop(), and reconnection after re-init().
  • SConnectorTestTls (CTest: tls) — end-to-end TLS test, built only with TLS enabled (skipped on Windows). SConnector connects as a TLS client (OpenSSL) to a mock fusion node that terminates TLS with OpenSSL and a short-lived self-signed P-256 certificate minted at start-up, and verifies the handshake, registration and reaching Active over TLS.
  • SConnectorTestTlsFips (CTest: tls-fips) — the same end-to-end path in FIPS mode, built only with <PARENT>_SCONNECTOR_WITH_FIPS=ON (skipped on Windows). The reason the test exists is the one assertion nothing else makes: that the negotiated parameters are approved — a NIST curve rather than X25519, AES-GCM rather than ChaCha20-Poly1305 — measured from the finished handshake rather than assumed from the policy the client asked for. Around that it covers:

    • configuration refusals — FIPS mode without tlsEnabled, with tlsVerifyPeer = false, or with tlsRevocationCheck = None; a certificate without its key; a passphrase-protected key; a key readable beyond its owner;
    • the indicatorsgetFipsSessionIndicator() against the real handshake, getFipsState() / getFipsFailureReason() across the states a configuration can reach, and runFipsSelfTest() on a healthy module;
    • revocation — CRL and OCSP-stapling policies proven to be enforced, not merely configured: a revoked certificate, an expired CRL and a peer that staples nothing all end the handshake, and status_request is shown to travel even when only CRLs are checked;
    • failure semantics — an unusable module drives the terminal ConnectionState::CryptoError and stops the reconnect loop, while a peer with no approved algorithm to offer stays retryable and leaves the module Operational;
    • coverage of the awkward paths — TLS 1.2 as well as 1.3, mutual TLS, a module reached by an equivalent spelling of its path, identifiers drawn from the module’s DRBG, and a FIPS connector running alongside a plain-TLS one in the same process.

    Point it at the module with -DSCONNECTOR_FIPS_MODULE_DIR=<dir> and -DSCONNECTOR_FIPS_CONFIG_FILE=<openssl.cnf> at configure time, or the matching SCONNECTOR_FIPS_* environment variables at run time. It skips rather than fails when the module cannot be loaded, or when it loads but reports a version other than the one this build pins — in both cases naming what to change — so it is harmless on machines without a FIPS provider.

cmake -S . -B build              # TLS off: runs the unit + integration tests
cmake --build build -j
cd build && ctest --output-on-failure

Enable TLS as shown above to additionally build and run the tls test, and FIPS to add tls-fips.

Use as a submodule: add the repository to your project (for example under a 3rdparty/ folder), then in your CMake:

add_subdirectory(SConnector)
target_link_libraries(YourTarget PUBLIC SConnector)

Linking the SConnector target brings in its public include path transitively. SConnector is licensed under Apache 2.0; the SAPIENT protobuf definitions are Crown Copyright (Apache 2.0), and nanopb is under the zlib license. TLS builds link the system OpenSSL, which carries its own license.


Table of contents