Transport Layer Security
Clients and servers can use TLS for encryption and authentication. The library uses rustls instead of OpenSSL:
- More secure and reliable: rustls is written in memory-safe Rust, which rules out the buffer overflows and use-after-free bugs behind many of OpenSSL's most serious vulnerabilities, such as Heartbleed, and the crashes they cause. It only implements TLS 1.2 and 1.3 with modern cipher suites, so there are no legacy protocols or weak options to leave enabled by mistake.
- The future: security agencies such as CISA and the NSA urge moving critical software to memory-safe languages, and rustls is backed by the Internet Security Research Group, the organization behind Let's Encrypt, as part of its effort to do that for the internet's infrastructure. With the AWS-LC cryptography provider, it already supports post-quantum key exchange.
Modbus security
Modbus Security runs Modbus TCP inside TLS, and adds role-based authorization on the server. The IANA reserved port for Modbus Security is 802.
The client's certificate carries its role in an X.509v3 extension with OID 1.3.6.1.4.1.50316.802.1, as an
ASN1:UTF8String. The role is an arbitrary string that your server uses to authorize requests. 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.
A Modbus Security server takes an AuthorizationHandler, with a method for each type of request. Each method receives
the destination unit ID, the requested range or index, and the client's role, and returns Allow or Deny. An allowed
request goes on to your request handler; a denied one is answered with an Illegal Function exception. In Rust, a method
you don't implement denies the request.
The client's certificate must carry exactly one role, without a NUL character. Otherwise, the server closes the connection after the handshake.
Supported features
- TLS v1.2 and v1.3
- The cipher suites, key exchange groups, and signature schemes are the rustls defaults for the cryptography provider: ring or aws-lc-rs in Rust, selected by the
tls(the default) ortls-aws-lcfeature, and aws-lc-rs in the prebuilt bindings on every platform. - Server name validation through the Subject Alternative Name (SAN) extension or the Common Name.
- Self-signed certificates (with a special configuration).
Configuration
Configure TLS with TlsClientConfig or TlsServerConfig: the trusted peer certificate(s), your certificate and private
key, the expected server name (client only), the minimum TLS version, and the certificate mode described below. In Rust,
the client's constructor chooses the mode: full_pki for AuthorityBased, self_signed for SelfSigned. The API
documentation describes each field.
In the bindings, pass an empty string, not null, as the password of a private key that isn't encrypted.
Name validation
In AuthorityBased mode, a client checks that the server's certificate is valid for the name you configure. The name is
matched against the certificate's Subject Alternative Name (SAN) entries, ignoring case, and wildcards are allowed. An IP
address is matched against the SAN's IP entries. If the certificate has no SAN, its Common Name must match exactly.
A client configured without a name (None in Rust, or * with allow_server_name_wildcard in the bindings) skips the
name check, but the server's certificate must still chain to a configured authority. The server doesn't check the
client's 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
Private keys are PEM files in PKCS#1 or PKCS#8 format. An encrypted key must be PKCS#8
(BEGIN ENCRYPTED PRIVATE KEY) using scrypt or PBKDF2 with AES-CBC. Legacy SHA-1 and DES/3DES encryption isn't
supported. The OpenSSL pkcs8 command can convert a key.
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 determines how the presented peer certificate is validated.
Authority-based
The AuthorityBased mode, the default in the bindings, 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.
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 NotBeforeandNotAftertime fields are valid for the current time
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_cert.pem \
-subj "/C=US/ST=Oregon/L=Bend/O=Test/CN=DO NOT USE" \
-addext "subjectAltName=DNS:test.com" -nodes -days 365