Supported by
TsgcWebSocketLoadBalancerServer
*Requires the sgcCrypto pack, which ships from the Standard edition up.
The native TLS engine is a TLS 1.3 and TLS 1.2 implementation written entirely in Object Pascal on top of the sgcCrypto units. It is not a binding: there is no OpenSSL DLL to deploy, no SChannel call and no platform TLS library involved. The same source compiles from Delphi 7 to RAD Studio 13 and runs on every platform the components support.
The reason to reach for it is the key exchange. The engine offers the hybrid post-quantum groups of RFC 10024, and X25519MLKEM768 is the first group it proposes, so a client that switches to it negotiates a post-quantum key exchange against any server that supports one, with no other change to the application. The hybrid groups exist only in TLS 1.3, so a connection that falls back to TLS 1.2 uses a classic curve. See Post-Quantum Cryptography for what those groups are and why a hybrid is the sensible migration today.
Turning it on
Set the IOHandler and nothing else changes. The certificate, key, root certificate, verification and ALPN properties you already use keep working, the engine reads the same ones.
// client
oClient := TsgcWebSocketClient.Create(nil);
oClient.URL := 'wss://www.esegece.com:2053';
oClient.TLS := True;
oClient.TLSOptions.IOHandler := iohNativeTLS;
oClient.TLSOptions.VerifyCertificate := True;
oClient.TLSOptions.RootCertFile := 'cacert.pem';
oClient.Active := True;
// server
oServer := TsgcWebSocketServer.Create(nil);
oServer.SSL := True;
oServer.SSLOptions.IOHandler := iohNativeTLS;
oServer.SSLOptions.CertFile := 'certificate.pem';
oServer.SSLOptions.KeyFile := 'key.pem';
oServer.Active := True;
On a server the engine also runs on the event-driven engines. Set IOHandlerOptions.IOHandlerType to iohIOCP on Windows or iohEPOLL on Linux and the handler is built on the matching TLS Provider, so the same configuration works on all three engines.
Selecting iohNativeTLS in a build without the sgcCrypto pack raises an exception that says so, rather than falling back to another library silently.
TLS versions
The engine speaks TLS 1.3 and TLS 1.2, as a client and as a server, on the Indy client, on the Indy server and on the IOCP and EPOLL server engines. TLS 1.3 is always chosen when both sides support it. For the native engine, TLSOptions.Version on a client and SSLOptions.Version on a server is the lowest version allowed.
| Version | Negotiates |
| tlsUndefined (the default) | TLS 1.3 or TLS 1.2 |
| tls1_2 | TLS 1.3 or TLS 1.2 |
| tls1_3 | TLS 1.3 only |
| tls1_0, tls1_1 | Nothing. A client raises a configuration error when it connects and a server when it starts, because the engine supports TLS 1.2 and TLS 1.3 only. |
This is not how the OpenSSL handler reads the property. With iohOpenSSL, tls1_2 means exactly TLS 1.2, while with iohNativeTLS it means TLS 1.2 or later. The cipher suite list decides the rest. To get TLS 1.2 only, keep Version at tls1_2 and name only TLS 1.2 suites in NativeTLS_Options.CipherSuites. A list that names only TLS 1.3 suites gives TLS 1.3 only, and a list that names suites of both versions allows both. Version tls1_3 together with a list of TLS 1.2 suites leaves no version to negotiate, and the connection raises when it starts.
// TLS 1.3 and TLS 1.2, tls1_2 is the lowest version allowed
oClient.TLSOptions.IOHandler := iohNativeTLS;
oClient.TLSOptions.Version := tls1_2;
// TLS 1.3 only
oClient.TLSOptions.Version := tls1_3;
// TLS 1.2 only: name TLS 1.2 suites and no TLS 1.3 suite
oClient.TLSOptions.Version := tls1_2;
oClient.TLSOptions.NativeTLS_Options.CipherSuites :=
'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256';
Groups and cipher suites
TLSOptions.NativeTLS_Options on a client, and SSLOptions.NativeTLS_Options on a server, carry the two preference lists. Both are strings in the OpenSSL style, items separated by colons, most preferred first, and one list serves TLS 1.3 and TLS 1.2 together. Leave one empty to keep the engine default. An invalid list raises when the connection starts, so a typo is not silently ignored.
| Property | Accepted names | Default |
| Groups | X25519MLKEM768, SecP256r1MLKEM768, SecP384r1MLKEM1024, X25519, secp256r1, secp384r1. The aliases P-256, prime256v1 and P-384 are accepted too, and names are compared without regard to case. The three hybrid groups are TLS 1.3 only. | X25519MLKEM768:X25519:secp256r1:secp384r1 |
| CipherSuites | TLS 1.3: TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, TLS_AES_128_GCM_SHA256. TLS 1.2: the suites in the table below. IANA names and OpenSSL style names such as ECDHE-RSA-AES128-GCM-SHA256 are both accepted, and the engine reports the IANA name. | TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256, plus the TLS 1.2 defaults below |
The first group in the list is the one a client sends a key share for, so leaving X25519MLKEM768 in front is what makes the first flight already post-quantum. Prefixing an item with a question mark skips it when this build does not have it, and any other unknown or unavailable item makes the whole list fail.
// only the hybrid post-quantum groups, and only AES-256-GCM
oClient.TLSOptions.NativeTLS_Options.Groups :=
'X25519MLKEM768:SecP256r1MLKEM768:SecP384r1MLKEM1024';
oClient.TLSOptions.NativeTLS_Options.CipherSuites := 'TLS_AES_256_GCM_SHA384';
// tolerate a build where X25519 is not compiled in
oClient.TLSOptions.NativeTLS_Options.Groups := '?X25519MLKEM768:secp256r1';
TLS 1.2 cipher suites
Only forward secret AEAD suites are enabled by default. The AES-CBC suites and the RSA key exchange are there for peers that cannot do better, and they are offered or accepted only when CipherSuites names them.
| Suites | Enabled |
| TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 | By default, in this order. |
| TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 | Only when named in CipherSuites. |
| TLS_RSA_WITH_AES_128_GCM_SHA256, TLS_RSA_WITH_AES_256_GCM_SHA384, TLS_RSA_WITH_AES_128_CBC_SHA, TLS_RSA_WITH_AES_256_CBC_SHA, TLS_RSA_WITH_AES_128_CBC_SHA256, TLS_RSA_WITH_AES_256_CBC_SHA256 | Only when named in CipherSuites. The RSA key exchange has no forward secrecy. |
The ECDHE suites of a TLS 1.2 connection use a classic group of the Groups list, X25519, secp256r1 or secp384r1, so keep at least one of them in the list when TLS 1.2 peers must connect. The RSA key exchange uses no group at all.
// TLS 1.3, and TLS 1.2 with AES-GCM plus AES-CBC for older clients
oServer.SSLOptions.IOHandler := iohNativeTLS;
oServer.SSLOptions.Version := tls1_2;
oServer.SSLOptions.NativeTLS_Options.CipherSuites :=
'TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:' +
'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:' +
'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256:TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA';
In a build made with SGC_CRYPTO_FIPS the two groups built on X25519, the ChaCha20-Poly1305 suites and the RSA key exchange are compiled out, and the defaults change to SecP256r1MLKEM768:SecP384r1MLKEM1024:secp256r1:secp384r1 and TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256, with the four ECDHE AES-GCM suites for TLS 1.2. See FIPS Mode.
TLS 1.2 behaviour
TLS 1.2 is implemented with the protections a modern peer expects, and without the parts of the protocol that caused its known attacks.
Extended master secret. RFC 7627 is offered by a client and accepted by a server. A peer that does not support it is still accepted.
No renegotiation. A renegotiation request is refused with a no_renegotiation alert. Session resumption uses session tickets and requires the extended master secret, see Session resumption below.
No SHA-1 signatures. Following RFC 9155, SHA-1 signatures are neither sent nor accepted. A TLS 1.2 ClientHello without the signature_algorithms extension is refused, because it would mean SHA-1 only.
Downgrade protection. The downgrade protection of RFC 8446 section 4.1.3 works on both sides. A server that also allows TLS 1.3 marks every TLS 1.2 answer, and a client that offered TLS 1.3 refuses a TLS 1.2 answer carrying that mark.
Constant time CBC. CBC records are decrypted in constant time, which closes Lucky Thirteen, and the RSA key exchange applies the countermeasure of RFC 5246 section 7.4.7.1.
Certificate key usage. For the RSA key exchange the server certificate needs the keyEncipherment key usage. The ECDHE suites and TLS 1.3 need digitalSignature.
TLS 1.3 only features. KeyUpdate and exporting keying material exist only on a TLS 1.3 session.
Session resumption
The engine resumes sessions with stateless session tickets, in TLS 1.3 (RFC 8446) and in TLS 1.2 (RFC 5077). After a full handshake the server sends a ticket, sealed with AES-256-GCM under a key that only the server holds. The client keeps it in a session cache, keyed by host, port and ALPN list, and offers it on the next connection, which then skips the certificate exchange. A ticket the server cannot open, or one that has expired, gives a full handshake with a new ticket.
| Property | Default | Meaning |
| NativeTLS_Options.SessionTickets | True | A server issues tickets and a client stores and offers them. False turns resumption off, every handshake is then a full one. |
| NativeTLS_Options.TicketLifetime | 7200 | Server only. Seconds a ticket can be used to resume, at most 604800. The server ticket key rotates every TicketLifetime seconds, and the previous key stays valid for one more period. |
The options are TLSOptions.NativeTLS_Options on a client and SSLOptions.NativeTLS_Options on a server. The session cache belongs to each client component, so reconnections of the same TsgcWebSocketClient, TsgcHTTP1Client, TsgcTCPClient or TsgcLDAPClient resume, and so does the upstream connection of TsgcWebSocketProxyServer.
TLS 1.2 resumption requires the extended master secret of RFC 7627. A TLS 1.2 session without it is never resumed and gets no ticket, and no ticket is issued either when the server asks for a client certificate. A TLS 1.3 ticket never opens as a TLS 1.2 session, and the other way round.
Whether a connection was resumed is reported by the Resumed property of TsgcIdSSLIOHandlerSocketNativeTLS, the handler described below.
What was negotiated
Once the handshake is done, the client handler, TsgcIdSSLIOHandlerSocketNativeTLS in the unit sgcSSL_NativeTLS_Indy, reports the result. VersionName is TLSv1.2 or TLSv1.3, CipherSuiteName is the IANA name of the suite, GroupName is the key exchange group, empty for the RSA key exchange, and PeerSignatureSchemeName is the signature scheme the server used. The handler is the one the client passes to OnSSLAfterCreateHandler.
procedure TForm1.oClientSSLAfterCreateHandler(Sender: TObject;
aType: TwsSSLHandler; aSSLHandler: TIdSSLIOHandlerSocketBase);
begin
FHandler := aSSLHandler;
end;
procedure TForm1.oClientConnect(Connection: TsgcWSConnection);
var
oHandler: TsgcIdSSLIOHandlerSocketNativeTLS;
begin
if FHandler is TsgcIdSSLIOHandlerSocketNativeTLS then
begin
oHandler := TsgcIdSSLIOHandlerSocketNativeTLS(FHandler);
Memo1.Lines.Add(oHandler.VersionName + ' ' + oHandler.CipherSuiteName +
' ' + oHandler.GroupName + ' ' + oHandler.PeerSignatureSchemeName);
end;
end;
Certificates and verification
A server reads CertFile, KeyFile and Password. CertFile and KeyFile can be PEM, or CertFile can be a .pfx or .p12 file, recognised by its extension. A PKCS#12 file holds the certificate, the chain and the private key, so KeyFile is not needed, and it is opened with Password. It is read by the pure Pascal PKCS#12 reader of sgcCrypto_PKCS12, with no OpenSSL. A client that sets VerifyCertificate to True checks the server chain against the trusted roots described below and checks that the name matches the host it connected to. VerifyDepth is passed through, and a value above 16 is capped at 16.
The trusted roots are the certificates in RootCertFile. Setting NativeTLS_Options.UseSystemRoots to True adds the certificate authorities the operating system trusts. It is False by default. The two combine: the roots of RootCertFile are loaded first, the system roots are added to the same store, and a chain is accepted when it leads to any of them. When no system root is found, nothing is added and no error is raised, so verification relies on RootCertFile alone. The property is TLSOptions.NativeTLS_Options.UseSystemRoots on a client and SSLOptions.NativeTLS_Options.UseSystemRoots on a server, where the roots serve only to validate client certificates. Where the system roots come from depends on the platform:
Windows. The ROOT system store, the Trusted Root Certification Authorities, read through crypt32.dll.
macOS. The anchor certificates of Security.framework, with /etc/ssl/cert.pem as the fallback.
iOS. The system roots cannot be listed. When no root is loaded, a chain the engine rejects only because its root is not trusted is evaluated by Security.framework with the SSL policy and the host name. The key usage and host name checks of the engine still run afterwards.
Android. The certificate files of /apex/com.android.conscrypt/cacerts (Android 14 and later) or /system/etc/security/cacerts, PEM or DER. Roots the user disabled, listed in /data/misc/user/0/cacerts-removed, are skipped, and the user roots of /data/misc/user/0/cacerts-added are added when the application can read those folders.
Linux and other platforms. The first of these files that exists and holds at least one certificate: /etc/ssl/certs/ca-certificates.crt, /etc/pki/tls/certs/ca-bundle.crt, /etc/ssl/ca-bundle.pem, /etc/pki/tls/cacert.pem, /etc/ssl/cert.pem and /etc/ssl/certs/ca-bundle.crt.
To load a folder of certificate files yourself, TsgcTLS13TrustStore.AddDirectory(aDirectory, aRemovedDirectory) in the unit sgcSSL_NativeTLS_Certificates reads every PEM or DER file of aDirectory, skips any file whose name also exists in aRemovedDirectory, and returns how many certificates it added. It works on every platform.
To pin the name the certificate must carry rather than using the connection host, set TLSOptions.OpenSSL_Options.X509Checks.HostName or IPAddress, with the matching flag in X509Checks.Mode. The engine reads those two values even though it is not OpenSSL, so the property means the same thing whichever IOHandler is selected.
On a server, SSLOptions.VerifyCertificate asks the client for a certificate and validates it against the trusted roots described above, and SSLOptions.VerifyCertificate_Options.FailIfNoCertificate refuses a client that sends none. VerifyClientOnce has nothing to do here. TLS 1.3 has no renegotiation and the engine refuses it in TLS 1.2, so it never asks a second time.
ALPN works as usual through ALPNProtocols on a client and through the server ALPN selection, so HTTP/2 over the native engine negotiates h2 the same way it does over OpenSSL. See ALPN.
QUIC mode
The engine also runs the TLS 1.3 handshake of QUIC and HTTP/3 (RFC 9001). Set SSLQUICOptions.IOHandler on TsgcQUICClient, TLSOptions.IOHandler on TsgcHTTP3Client, QUICOptions.SSLOptions.IOHandler on TsgcQUICServer and TsgcHTTP3Server, or SSLOptions.IOHandler on a server with HTTP3Options.Enabled, to iohNativeTLS.
The sgc QUIC engine is always used, whatever OpenSSL exports. On a server QUICMode reports qmCustom.
The handshake carries the QUIC transport parameters, requires ALPN and reports TLS alerts as QUIC error codes. The server certificate is read from PEM or PFX, and the peer is verified against RootCertFile or the system roots, host name included.
Session tickets, resumption and 0-RTT work as on OpenSSL. The server sends a ticket after the handshake, the client stores it with the ALPN and the server transport parameters, and 0-RTT is accepted only once per ticket. QUIC tickets never cross over to TLS over TCP.
The packet protection is native too, so QUIC and HTTP/3 load no OpenSSL. No define is needed.
See QUIC for the engine comparison.
What it does not do
The engine is deliberately narrow. Everything below is a real limit, not a setting to look for.
TLS 1.3 and TLS 1.2 only. There is no SSL, no TLS 1.0 and no TLS 1.1. A client raises when it connects, and a server when it starts, if Version is tls1_0 or tls1_1. A peer that can do neither TLS 1.3 nor TLS 1.2 cannot connect.
No PSK without a ticket. Resumption works only with the session tickets the engine issues itself, see Session resumption above. External pre-shared keys are not supported.
No 0-RTT over TCP. On a TLS connection over TCP early data is neither sent nor accepted. Post-handshake client authentication is not implemented either.
QUIC through its own mode. QUIC and HTTP/3 use the QUIC mode described above, configured on those components.
No system roots by default. Unless NativeTLS_Options.UseSystemRoots is True, trust roots come from RootCertFile and nowhere else, so a client that verifies certificates must ship or point at a root file. That is the one behaviour difference a working OpenSSL or SChannel configuration is most likely to notice.
OnSSLVerifyPeer is not raised. That event belongs to the OpenSSL handler. The native engine has its own verification hooks, which receive the whole peer chain and the engine verdict at once instead of one certificate per depth, so an application that overrides the trust decision has to use those rather than the component event.
Nothing the peer sends raises an exception. A protocol failure ends the connection with the proper TLS alert, and the reason is reported through the component error events as usual.
The AES-GCM and AES-CBC record protection uses the AES-NI and PCLMULQDQ instructions on Windows builds made with Delphi when the CPU has them, see sgcAES_HardwareAvailable in Symmetric Encryption. For a build where no part of the library loads OpenSSL, see Running without OpenSSL.