Transport Layer Security
Modbus Security is a specification to wrap Modbus TCP inside a TLS secure transport to enable authentication and privacy of the communication links. It uses an X.509v3 extension to provide an authorization mechanism on the server. The IANA reserved port for Modbus Security is 802.
In the presented certificate, the client must provide an x.509v3 extension with OID 1.3.6.1.4.1.50316.802.1
containing an ASN1:UTF8String
. This arbitrary string represents the authorized role of the client and is
used to authorize operations. For example, a client with the historian
role might only be allowed to read
a certain range of values, while a client with the operator
role might be able to write coils and registers.
This library uses rustls, a modern TLS library written in safe Rust. It does not depend on OpenSSL or other system libraries, but will interoperate seamlessly with other implementations.
Rustls has been reviewed by a third party and is production ready. It outperforms OpenSSL in almost every aspect and has the backing of the ISRG.
Supported features
- TLS v1.2 and v1.3
- Supported cipher suites (in descending order of preference):
- TLS v1.3:
TLS_CHACHA20_POLY1305_SHA256
TLS_AES_256_GCM_SHA384
TLS_AES_128_GCM_SHA256
- TLS v1.2:
TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- TLS v1.3:
- Supported key exchange algorithms (in descending order of preference):
x25519
secp384r1
secp256r1
- Supported signature algorithms (in descending order of preference):
ecdsa_secp384r1_sha384
ecdsa_secp256r1_sha256
ed25519
(v1.3 only)rsa_pss_sha512
(v1.3 only)rsa_pss_sha384
(v1.3 only)rsa_pss_sha256
(v1.3 only)rsa_pkcs1_sha512
rsa_pkcs1_sha384
rsa_pkcs1_sha256
- Server name validation through the Subject Alternative Name (SAN) extension or the Common Name (CN).
- Self-signed certificates (with a special configuration).
Configuration
TLS configuration is configured using the TlsClientConfig
or TlsServerConfig
structures. They are very similar and contain the following fields:
name
(client only):- The client verifies that the certificate presented by the server is valid for this name. Check out the next section for the gory details on name validation.
- The client will advertise this name through a Server Name Indication (SNI) extension in the
Client Hello
.
peer_cert_path
:- Path to the unencrypted PEM file containing the trusted root certificate(s) or the peer self-signed certificate.
local_cert_path
:- Path to the unencrypted PEM file containing the certificate(s) to present to the peer.
private_key_path
:- Path to the PEM file containing the encrypted or plaintext private key corresponding to the public key in the presented certificate.
password
:- Password used to decrypt the private key file. This field should be left empty if the file is not encrypted. See the next section for more details.
min_tls_version
:- Minimum TLS version to support. Setting this to
Tls1_3
will force the usage of TLSv1.3.
- Minimum TLS version to support. Setting this to
certificate_mode
:- Mode used to verify the peer certificate.
Name validation
This section describes how name validation is performed when certificate_mode
is AuthorityBased
. When the mode
is SelfSigned
, name validation is performed indirectly since a byte-for-byte comparison occurs of the entire certificate.
The client always validates the server name when certificate_mode
is TrustChain
.
A valid name has the same requirements as a DNS name. This is defined in
RFC 1034 Section 3.5,
updated by RFC1123 Section 2.1,
with the additional exception that underscores _
are allowed. A brief (but incomplete)
definition of a valid name includes:
- One or more labels separated by a single period
- Labels are made of alphanumeric characters
[A-Za-z0-9]
, hyphens-
, and underscores_
- A label cannot start or end with an hyphen
-
- A label cannot be all numeric
- A label cannot be empty
- Maximum of 63 characters per label
- Maximum of 253 characters total
If the SAN extension is present, the name is validated against it. The SAN may contain multiple names and each name can contain a wildcard *
character.
The comparison is case-insensitive.
If the SAN is absent, then the Common Name
from the certificate's Subject
is extracted and compared. The Common Name cannot contain a wildcard character
and the comparison is case-sensitive. It is effectively compared byte-for-byte with the expected name.
New certificates should always include the SAN extension. Performing name verification using the Common Name
is still secure, but it is deprecated for new use cases.
Private Key Encryption
Unencrypted private keys may be stored in PKCS#1 or PKCS#8 formats:
----BEGIN RSA PRIVATE KEY-----
-----BEGIN PRIVATE KEY-----
Encrypted private keys must be stored in PKCS#8v2 format:
-----BEGIN ENCRYPTED PRIVATE KEY-----
Keys encrypted in PKCS#1 format are not supported.
The following algorithms are supported when keys are encrypted in PKCS#8v2 format:
- Key derivation functions:
- scrypt (RFC 7914) recommended
- PBKDF2 (RFC 8018) using one of the following HMAC:
- HMAC-SHA224
- HMAC-SHA256
- HMAC-SHA384
- HMAC-SHA512
- Symmetric encryption:
- AES-128-CBC
- AES-192-CBC
- AES-256-CBC recommended
SHA-1 based key derivation and DES/3DES symmetric encryption are not supported because they are insecure.
The OpenSSL pkcs8 command can be used to encrypt keys or convert private key formats.
Encrypting private keys for use with TLS rarely addresses a meaningful threat model. Encrypting a private key and storing the password in a configuration file is equivalent to locking your doors and leaving the key under the doormat.
Certificate Mode
The certificate_mode
parameter determines how the presented peer certificate is validated.
Authority-based
The default AuthorityBased
mode uses the default rustls
implementation to check the presented chain of certificates
and verify that the root certificate of the chain is one of those provided in the peer_cert_path
file.
Most, if not all applications, in the power industry, will have a single root certificate.
This mode validates the chain and the end-entity certificate in accordance with RFC 5280.
Self-Signed
The SelfSigned
mode validates that:
- Only a single certificate is presented
- It is a byte-for-byte match of the one specified in
peer_cert_path
NotBefore
andNotAfter
time fields are valid for the current time
Name validation is performed indirectly in self-signed mode, since the byte-for-byte comparison also compares the internal name fields.
AuthorizationHandler
When creating a Modbus Security server, you also need to provide an AuthorizationHandler
.
It is use to validate that the client has the authorization to perform the operation. There's
a callback for each function code. You get the unit ID of the destination, information about
what points are requested (either as an AddressRange
or a 16-bit unsigned integer) as well as
the Role ID that was extracted from the presented certificate.
If your handler returns AuthorizationResult::Authorized
, then the request is passed to
the appropriate handler and the operation is performed. If your handler returns
AuthorizationResult::NotAuthorized
, then the request is dropped and an Illegal function code
exception is returned to the client.
Generating certificates
The following OpenSSL commands are provided for testing purposes only. Real systems will use some kind of specialized CA software for certificate procurement.
Full CA chain
- Generate the root CA certificate:
openssl req -x509 -newkey rsa:4096 -keyout ./ca_key.pem -out ./ca_cert.pem -subj "/C=US/ST=Oregon/L=Bend/O=Test/CN=DO NOT USE" -nodes -days 3650
- Generate the client CSR:
openssl req -new -newkey rsa:4096 -keyout ./client_key.pem -out ./client_csr.pem -subj "/C=US/ST=Oregon/L=Bend/O=Test/CN=DO NOT USE" -addext "1.3.6.1.4.1.50316.802.1=ASN1:UTF8String:operator" -nodes -days 365
- Generate the client certificate:
openssl x509 -req -days 365 -in ./client_csr.pem -extfile <(printf "1.3.6.1.4.1.50316.802.1=ASN1:UTF8String:operator") -CA ./ca_cert.pem -CAkey ./ca_key.pem -set_serial 1 -out ./client_cert.pem -sha256
- Generate the server CSR:
openssl req -new -newkey rsa:4096 -keyout ./server_key.pem -out ./server_csr.pem -subj "/C=US/ST=Oregon/L=Bend/O=Test/CN=DO NOT USE" -addext "subjectAltName=DNS:test.com" -nodes -days 365
- Generate the server certificate:
openssl x509 -req -days 365 -in ./server_csr.pem -extfile <(printf "subjectAltName=DNS:test.com") -CA ./ca_cert.pem -CAkey ./ca_key.pem -set_serial 2 -out ./server_cert.pem -sha256
Self-signed certificate
- Generate the client certificate:
openssl req -x509 -newkey rsa:4096 -keyout ./client_key.pem -out ./client_cert.pem -subj "/C=US/ST=Oregon/L=Bend/O=Test/CN=DO NOT USE" -addext "1.3.6.1.4.1.50316.802.1=ASN1:UTF8String:operator" -nodes -days 365
- Generate the server certificate:
openssl req -x509 -newkey rsa:4096 -keyout ./server_key.pem -out ./server/entity2_cert.pem -subj "/C=US/ST=Oregon/L=Bend/O=Test/CN=DO NOT USE" -addext "subjectAltName=DNS:test.com" -nodes -days 365