Skip to content

OpenSSL TLS#

The aes70::openssl helpers (header aes70/openssl.hpp) wrap OpenSSL so AES70 traffic runs over TLS. They support TLS-PSK (psk_context) and certificate TLS (cert_context), including mutual TLS (mTLS). OpenSSL is not included with libaes70; you supply and link it. See Third-party libraries.

Prerequisites#

  • OpenSSL 1.1.1 or later (TLS 1.3 support).
  • Link OpenSSL::SSL and OpenSSL::Crypto (or equivalent) with your device/controller targets.
  • A platform TCP integration (e.g. libuv) as for plaintext AES70.

Plaintext read chunk#

ssl_session decrypts into a scratch buffer of size AES70_OPENSSL_READ_CHUNK_SIZE (default 16 KiB). Override at compile time if needed. AES70 PDUs are usually much smaller than this.

Layering#

Connections are layered as:

encrypt < io < decrypt < Connection
  • Device: wrap the AES70 port with aes70::openssl::port<…, Context>.
  • Controller: wrap an IO connector_factory with aes70::openssl::connector_factory<IoFactory, Connection, Context>.

Context defaults to psk_context so existing PSK call sites stay short.

Context contract#

A TLS context must provide:

  • SSL_CTX *ssl_ctx()
  • bool tls_is_server() const

It must be copyable/movable enough to live in port and in reconnecting controller factories (both psk_context and cert_context use a shared impl).

There is no formal C++20 concept; the requirement is duck-typed.

TLS-PSK#

Configure a shared key via psk_config (key material lives in key_storage), then build a server or client context:

aes70::openssl::psk_config cfg;
cfg.key.assign(32, 0x42);  // demo only — use real key material
cfg.min_version = TLS1_2_VERSION;
cfg.max_version = TLS1_3_VERSION;

auto server_tls = aes70::openssl::psk_context::make_server(cfg);
auto client_tls = aes70::openssl::psk_context::make_client(cfg);

Device#

using Port = aes70::openssl::port<aes70::port<aes70::dynamic_device::device>>;

libuv::tcp::port<Port> tcp(
    portno, "0.0.0.0", uv_default_loop(), std::move(server_tls), dev);

Controller#

auto factory =
    aes70::openssl::connector_factory<libuv::tcp::connector_factory>::create(
        client_tls, host, port);

auto sub = aes70::controller::observe_connection(
    factory,
    [](aes70::controller::device remote) {
      // return subscriptions for this connection
      return aes70::controller::subscription{};
    },
    1000u);

Full working code: examples/openssl/psk.

Certificate TLS#

cert_config takes DER (binary) material in memory — not file paths:

  • certificate — leaf cert (std::vector<uint8_t>)
  • private_key — matching key (key_storage, wiped on destruction)
  • ca_certificates — trusted CAs (list of DER blobs)

Use the helpers in aes70/openssl/pem.hpp when you have PEM files:

  • load_pem_certificate_file / pem_decode_certificate
  • load_pem_private_key_file / pem_decode_private_key
  • load_pem_certificates_file / pem_decode_certificates (CA bundles)
Mode Server Client
Server cert only certificate + private_key, verify_peer=false ca_certificates, verify_peer=true
mTLS cert + key + ca_certificates, verify_peer=true cert + key + ca_certificates, verify_peer=true

Server cert only (device)#

aes70::openssl::cert_config cfg;
cfg.certificate = aes70::openssl::load_pem_certificate_file("server.crt");
cfg.private_key = aes70::openssl::load_pem_private_key_file("server.key");
cfg.verify_peer = false;

auto tls = aes70::openssl::cert_context::make_server(cfg);

using Port = aes70::openssl::port<aes70::port<aes70::dynamic_device::device>,
    aes70::openssl::cert_context>;

libuv::tcp::port<Port> tcp(
    portno, "0.0.0.0", uv_default_loop(), std::move(tls), dev);

Server cert only (controller)#

aes70::openssl::cert_config cfg;
cfg.ca_certificates =
    aes70::openssl::load_pem_certificates_file("ca.crt");
cfg.verify_peer = true;

auto client_tls = aes70::openssl::cert_context::make_client(cfg);

using Factory = aes70::openssl::connector_factory<
    libuv::tcp::connector_factory, aes70::controller::connection,
    aes70::openssl::cert_context>;

auto sub = aes70::controller::observe_connection(
    Factory::create(client_tls, host, port),
    [](aes70::controller::device remote) {
      return aes70::controller::subscription{};
    },
    1000u);

Demo PEMs: examples/openssl/certs/gen_certs.sh. Examples under examples/openssl/ (server_cert, mtls).

Errors#

Three channels matter on the controller side:

  1. IO factory on_error — transport failures (connect refused, socket errors). Access via openssl::connector_factory::io_factory().on_error(...).
  2. OpenSSL factory on_error — SSL-layer failures (SSL_get_error / ERR_peek_last_error), e.g. handshake / alert failures.
  3. observe_connection on_failure — connect failed or the connection closed before OCP.1 was ready (nullptr in the failure callback). Set via observe_connection_config::on_failure.

Keepalive and auto-reconnect behavior are the same as for plaintext observe_connection; see Connecting to Devices.

Examples#

Example Mode Default port
examples/openssl/psk TLS-PSK 65123
examples/openssl/server_cert Server cert only 65124
examples/openssl/mtls mTLS 65125

See also examples/openssl/README.md.