
SConnector C++ library
v1.1.0
Table of contents
- Overview
- SAPIENT support matrix
- Versions
- Library files
- SConnector class description
- Class declaration
- getVersion method
- init method
- stop method
- getConnectionState method
- updateStatusReport method
- sendDetectionReport method
- sendAlert method
- setTaskCallback method
- setConnectionStateCallback method
- setErrorCallback method
- getFipsModuleVersion method
- getFipsState method
- getFipsFailureReason method
- runFipsSelfTest method
- getFipsSessionIndicator method
- Data model
- Limitations
- Example
- SConnectorEdgeNodeTemplate
- Testing against the DMM Simulator
- Build and connect to your project
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 tlsKeyFile — init() 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 Both — init() 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 != None — init() 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: NotBuilt → NotBuiltIn, NotRequested → NotRequested, NotLoaded and Operational → None, 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. WithtlsVerifyPeer = trueand an emptytlsCaFile, the OpenSSL default trust store is used; settlsCaFileto 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 whiletlsFipsModeis 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()andstop()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/updateStatusReportconcurrently withinit()/stop()is supported but the sends will returnfalseonce the link is being torn down. The library is one worker thread perSConnectorinstance — there is no shared thread pool.
API contracts to be aware of
sendDetectionReport()andsendAlert()returnfalseif bothlocationandrangeBearingare unset — SAPIENT mandates a position on both messages.StatusInfo::modemust be non-empty (a strict fusion node rejects aStatusReportwithout a mode).RegistrationMode::settleTimeSecandconcurrentTasksmust be> 0and>= 1respectively (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:
- A task callback — invoked on SConnector’s worker thread for every
Taskthe fusion node sends; here it just prints the task name and accepts it (returnTaskDecision::Rejectedwith a reason to refuse a command). - Minimal configuration — only the fusion address, a node name and a non-empty status
modeare 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. - Periodic reporting — once a second, while the link is
Active, it sends oneDetectionReportand oneAlert.
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):
FREEorTRACKING, with an X/Y target in a FullHD (1920×1080) frame. A capture latches the target and switches toTRACKING; a reset returns toFREE. - Object detector —
ON/OFF; whileONit 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
- Registration. On start, the template fills an
EdgeNodeConfigthat declares the node as aCamera, lists itscapabilities(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. - Connection.
init()starts the worker thread, which connects to the fusion node, registers and reaches theActivestate. State changes are printed via the connection-state callback. - Commands. Incoming
Tasks are delivered to the task callback, which applies them to the simulation and returnsAccepted/Rejected— the library sends the matchingTaskAckautomatically. Standard SAPIENT command types are honoured (see below); commands meant for mobile nodes (MOVE_TO/PATROL/FOLLOW) are rejected because the head is fixed. - Reporting. A loop advances the simulation (zoom/focus drives, pan/tilt motion) every 50 ms. Once per second it refreshes the
StatusReportwith the current device state (zoom, focus, field of view, pan, tilt, tracker mode, detector state) viaupdateStatusReport(), and — while the detector is ON — sendsDetectionReports for the random objects. When a detection exceeds a high-confidence threshold the template also raises anAlertviasendAlert().
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:
- DMM Simulator / test harness: https://github.com/dstl/BSI-Flex-335-v2-Test-Harness
- Dstl SAPIENT resources on GitHub: https://github.com/dstl
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.
- Start the DMM Simulator from the test harness (follow its own README).
-
Run the example node against the Data Agent port:
./build/bin/SConnectorEdgeNodeTemplate 127.0.0.1 14000 - Watch the DMM Simulator log. In order, you should see: the
Registrationaccepted (RegistrationAck { acceptance: true }), periodicStatusReports, theTasks the DMM sends (for exampleLOOK_AT) answered with aTaskAck, and — once the detector is tasked on —DetectionReports andAlerts. NoErrormessage 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 forDisconnectIntervalSecs(default 60 s) and a quick restart is refused with “Another ASM is using this ID”. Pass a differentnodeIdas the third argument to run a second node alongside the first. - The DMM addresses its
Tasks to the registered node viadestinationId; 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 ofStatusReport/DetectionReport/Alert/TaskAckand the full mandatoryRegistrationtree, plus decoding ofRegistrationAckandTask(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, periodicStatusReports,sendDetectionReport/sendAlert, an inboundTaskreaching the callback with the matching automaticTaskAck, theSYSTEM_GOODBYEsent on a cleanstop(), the destructor forcibly closing the link (withSYSTEM_GOODBYE) when aSConnectorgoes out of scope without an explicitstop(), 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 reachingActiveover 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, withtlsVerifyPeer = false, or withtlsRevocationCheck = None; a certificate without its key; a passphrase-protected key; a key readable beyond its owner; - the indicators —
getFipsSessionIndicator()against the real handshake,getFipsState()/getFipsFailureReason()across the states a configuration can reach, andrunFipsSelfTest()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_requestis shown to travel even when only CRLs are checked; - failure semantics — an unusable module drives the terminal
ConnectionState::CryptoErrorand stops the reconnect loop, while a peer with no approved algorithm to offer stays retryable and leaves the moduleOperational; - 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 matchingSCONNECTOR_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. - configuration refusals — FIPS mode without
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.