TLS 1.2 · 1.3 · X.509v3
Modbus/TCP Security
Carry Modbus TCP inside TLS. Client and server verify each other's X.509v3 certificates, and the server authorizes each function code by the role in the client certificate.
What it does
- Based on the MODBUS/TCP Security Protocol Specification, on standard port 802.
- Encrypted mode with TLS 1.3 and TLS 1.2 (AES-GCM) and mutual authentication.
- Authentication-only mode (TLS 1.2 NULL-SHA256) for integrity without encryption.
- Role-based authorization from the certificate role extension; denied requests get exception 01.
- Certificate revocation lists and mandatory revocation checking.
- No silent fallback to plain TCP; project files store PEM paths, never key material.
Example
Each binding shows code from a real example file in the examples folder of the PDComm SDK. The source file and line range appear above the code.
Modbus/TCP Security client in C
char address[128], security[4096] = "null", configure[5120];
if (!json_string(address, sizeof(address), argv[1]))
return 2;
if (tls) {
/* Modbus/TCP Security: trust servers issued by the CA, and present the
* client chain (ending with the root CA) and its private key. The server
* certificate must name "localhost" unless "hostname" says otherwise. */
char ca[1024], chain[1024], key[1024];
if (!json_string(ca, sizeof(ca), argv[3]) || !json_string(chain, sizeof(chain), argv[4]) ||
!json_string(key, sizeof(key), argv[5]))
return 2;
snprintf(security, sizeof(security),
"{\"mode\":\"encrypted\",\"trust\":%s,\"chain\":%s,\"private_key\":%s}", ca, chain, key);
}
snprintf(configure, sizeof(configure),
"{\"action\":\"configure\",\"config\":{\"transport\":\"tcp\",\"role\":\"client\","
"\"address\":%s,\"port\":\"\",\"unit\":1,\"baud\":19200,\"parity\":\"even\","
"\"stop_bits\":1,\"timeout_ms\":1000,\"security\":%s}}",
address, security);The --tls path of the C example: the security object names the trusted CA, the client chain ending with the root CA and the private key, in encrypted mode. The server certificate must match hostname (localhost by default).
Modbus/TCP Security client in C++
// Without --tls, "security" is null and the connection is plain Modbus TCP.
std::string security = "null";
if (tls) {
// Modbus/TCP Security: trust servers issued by the CA, and present the
// client chain (ending with the root CA) and its private key. The server
// certificate must name "localhost" unless "hostname" says otherwise.
security = "{\"mode\":\"encrypted\",\"trust\":" + quote(argv[3]) + ",\"chain\":" + quote(argv[4]) +
",\"private_key\":" + quote(argv[5]) + "}";
}
try {
pdcomm::modbus::Endpoint endpoint;
endpoint.call("{\"action\":\"configure\",\"config\":{\"transport\":\"tcp\",\"role\":\"client\","
"\"address\":" + quote(argv[1]) + ",\"port\":\"\",\"unit\":1,\"baud\":19200,"
"\"parity\":\"even\",\"stop_bits\":1,\"timeout_ms\":1000,\"security\":" + security + "}}");
endpoint.call("{\"action\":\"connect\"}");The --tls path of the C++ example: quote the file paths as JSON strings, build the security object, then configure and connect. Functions that the server's role policy doesn't allow get exception 01.
Modbus/TCP Security client in .NET
// Modbus/TCP Security: trust servers issued by the CA, and present the client chain (ending with the root CA)
// and its private key. The server certificate must name "localhost" unless "hostname" says otherwise.
// Without --tls, security is null and the connection is plain Modbus TCP.
object? security = tls
? new { mode = "encrypted", trust = args[4], chain = args[5], private_key = args[6] }
: null;
try
{
using var endpoint = new Endpoint();
endpoint.Call(new
{
action = "configure",
config = new
{
transport = "tcp", role = "client", address = args[2], port = "", unit = 1,
baud = 19200, parity = "even", stop_bits = 1, timeout_ms = 1000, security,
},
});
endpoint.Call(new { action = "connect" });The --tls path of the .NET example: security is an anonymous object, or null for plain TCP. Without a license for the modbus-security module the connect is refused with Code license_required.
Modbus/TCP Security in Qt
// Returns null (plain Modbus TCP) without a certificate directory.
QJsonValue security(const QString &directory, const QString &identity, bool server) {
if (directory.isEmpty())
return QJsonValue();
QJsonObject value{{"mode", "encrypted"},
{"trust", directory + "/ca.pem"},
{"chain", directory + "/" + identity + "-chain.pem"},
{"private_key", directory + "/" + identity + ".key"}};
if (server) {
// Clients with the "operator" role may use functions 03 and 06 only.
value.insert("roles", QJsonObject{{"operator", QJsonArray{3, 6}}});
value.insert("null_role", QJsonArray{});
}
return value;
}
QJsonObject tcp(const char *role, const QString &address, const QJsonValue &security) {
return {{"transport", "tcp"}, {"role", role}, {"address", address},
{"port", ""}, {"unit", 1}, {"baud", 19200},
{"parity", "even"}, {"stop_bits", 1}, {"timeout_ms", 1000},
{"security", security}};
}The --tls mode of the Qt example: both configurations get a security object, and the server allows only functions 03 and 06 for the operator role.