Native TLS

Supported by

 

TsgcWebSocketClient

TsgcTCPClient

TsgcHTTP1Client

TsgcHTTP2Client

TsgcWebSocketServer

TsgcWebSocketHTTPServer

TsgcHTTPServer

TsgcWebSocketProxyServer

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.

 

 

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:

 

 

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.

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.

 

 

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.