This module is the portable SSL/TLS layer, so a caller encrypts and decrypts bytes through the same types on every supported platform. It is not a socket API. The transport is an iostream the caller already has: a TCP connection, a string stream in a test, or any other stream that moves bytes to the peer. The SSL types wrap that stream, encrypt what is written to them, and decrypt what is read from them.
Work proceeds in three steps. Certificates and private keys are loaded into a CertificateStore. A Context takes certificates from that store and holds the protocol, the identity presented to the peer, the trusted CAs, and the verification mode. An IOStream or a StreamBuffer then opens on a context and an underlying iostream, as a client (Connect) or as a server (Accept). One context is reused for many connections.
The handshake, the encrypted I/O, and the shutdown are operations on the stream, not on the context. They can run as blocking iostream operations or as non-blocking steps that consume only the bytes already available in the underlying buffer, so the same types work with a Pt::System::IOStream that fills asynchronously.
HTTPS in the HTTP module is this layer behind setSecure(): the client or server is given a Context, and send and receive stay the HTTP API. Certificate loading, protocol, verification, and the handshake itself are documented here.
A failure that is specific to SSL is an SslError, which is an I/O error. A handshake that cannot complete is a HandshakeFailed, and certificate data that cannot be used is an InvalidCertificate.
The rest of this chapter is the certificate store, then the context, then the secure stream.
This chapter covers:
SSL/TLS work starts with certificates. A server presents an identity, a client may present one, and both sides usually have a list of trusted CA certificates with which they verify the peer. Those certificates, and the private keys that belong to them, are loaded into a CertificateStore.
PKCS12 is the usual container: one file or memory buffer can hold several certificates and their keys, often protected by a password. PEM is the other encoding loadPem() accepts from memory. After a load, the store owns the certificates. A private key stays attached to its certificate and is not exposed by any function; selecting a certificate later as a context identity selects that key as well.
Certificates in the store are inspected by subject. subject() on a Certificate is the subject string, findCertificate() returns a certificate whose subject contains a substring or a null pointer, and getCertificate() does the same search and throws InvalidCertificate when nothing matches. begin() and end() walk the store. The store must outlive any reference or pointer taken from it.
The example loads PKCS12 data, lists subjects, and looks up one certificate by name. Using that certificate in a Context is the next chapter.
CertificateStore is the owner of the certificates the group described. PKCS12 data is loaded from an iostream or from a memory buffer, and PEM data is loaded from memory. Each load adds to the store; it does not replace certificates already loaded. Unreadable PKCS12 or PEM data throws InvalidCertificate.
findCertificate() searches for a subject substring and returns a pointer the store still owns, or a null pointer. getCertificate() is the same search and throws InvalidCertificate when nothing matches. size(), begin() and end() inspect the current contents.
A Certificate reference or pointer from this store is valid only while the store exists and still holds that certificate.
Certificate is the non-copyable handle the store holds for one X509 certificate. subject() is the subject string used to find it. The private key, when the store loaded one for this certificate, stays inside the handle and is not readable.
Pass the certificate by const reference to Context::setIdentity(), addCertificate(), or addCACertificate(). The store remains the owner of this object.
Context is the shared configuration for SSL/TLS connections. It is not a connection and it does not wrap a stream. Many IOStream and StreamBuffer objects can open with the same context and then run their own handshakes.
The context holds the protocol, the verification mode, how deep a peer certificate chain is checked, the identity presented to the peer, extra certificates that complete that identity's chain, and the trusted CA certificates used to verify the peer. A default-constructed context uses the highest available TLS protocol and TryVerify. setProtocol() selects an explicit version when the peer requires one. TLS is the highest available protocol; TLSv1, TLSv1_1 and TLSv1_2 pin a version.
setIdentity() sets the certificate presented to the peer. A server context must have an identity. A client context needs one only when the server asks for client authentication. Because a private key stays attached to its certificate in the store, setting the identity selects that key as well. addCertificate() adds a certificate to the chain presented together with the identity. addCACertificate() adds a trusted CA used to verify the peer; it does not change the identity.
setVerifyMode() chooses whether the peer must authenticate. NoVerify does not check a peer certificate. TryVerify checks a certificate when the peer presents one and continues if none is presented. AlwaysVerify requires a peer certificate and fails the handshake without one. setVerifyDepth() limits how many certificates in the peer chain are checked.
The context copies the certificate data it needs when identity, chain, or CA certificates are set, so the store does not have to remain after those calls. The Certificate argument of each call must still be valid for the call. Unusable certificate material throws InvalidCertificate.
Context is not copyable. assign() copies protocol, verify settings, and certificates into another context.
The example prepares a server context from certificates already in a store. Opening a stream with that context is the next chapter.
Context is the configuration the group described: protocol, verification, identity, chain, and trusted CAs, used by every stream opened with it. It is not a connected stream. Keep the context alive while a stream that opened with it is still open.
The default constructor selects TLS, the highest available protocol, and TryVerify. The protocol constructor selects that protocol and the same default verify mode. setProtocol() and setVerifyMode() change them later. setVerifyDepth() limits how many certificates in the peer's chain are checked.
setIdentity() is required on a server context and optional on a client context. addCertificate() extends the chain presented with that identity. addCACertificate() extends the trust store used to verify the peer. Each of those calls copies what it needs from the Certificate argument, which must be valid for the call. Unusable material throws InvalidCertificate.
The object is not copyable. assign() copies protocol, verify settings, and certificates from another context into this one.
A secure connection is an IOStream or a StreamBuffer that wraps another iostream. Bytes written to the SSL stream are encrypted and written to that underlying stream. Bytes read from the underlying stream are decrypted and made available on the SSL stream. The module does not own the transport: any iostream that can move bytes to the peer is enough, including a Pt::System::IOStream used with an event loop.
Opening needs a Context, the underlying iostream, and an OpenMode. Connect is a client handshake. Accept is a server handshake. The context is not copied into the stream as a snapshot the caller can then discard without care: the stream uses the context for the connection, so the context must remain while the stream is open. IOStream is the iostream most callers use. StreamBuffer is the streambuf that implements the connection, and it is the type that sets the expected peer name.
Construction or open() prepares the connection; it does not finish the handshake. readHandshake() and writeHandshake() run until isConnected() is true. readHandshake() returns true when more handshake data must be read and the underlying stream did not have enough bytes. It consumes only as many bytes as the underlying streambuf reports with in_avail(), so it can be called again when more data arrives. When it returns false, no more input is needed for this step and writeHandshake() should run. writeHandshake() returns true when handshake data was written to the underlying stream and still has to be sent. In the non-blocking case, call it again after that data has been flushed. A handshake that cannot complete throws HandshakeFailed.
Once the stream is connected, the blocking API inherited from std::iostream reads and writes decrypted application data. Non-blocking input uses import(), which decrypts from the underlying stream without reading more than in_avail() indicates. It may decrypt only part of what is available, so draining the underlying stream means calling import() until it makes no more progress. After import(), decrypted bytes can be read, and isShutdown() reports whether a shutdown alert arrived instead of application data. Writes encrypt into the SSL output buffer; flush() pushes the encrypted bytes to the underlying stream so the code that owns that stream can send them. I/O that fails for an SSL reason throws SslError. Premature end of the transport is reported by the code that maintains the underlying stream.
Either peer can start a shutdown. isShutdown() is true when a shutdown alert has been received and the acknowledge still has to be completed. If that alert arrived during a read, shutdown() writes the acknowledge to the underlying stream and returns true; the acknowledge still has to be sent, but the SSL stream then treats the connection as finished and isShutdown() is no longer true. If shutdown() is called to start the shutdown, the alert is written to the underlying stream and must be sent. When the peer's acknowledge becomes available, shutdown() must be called again to consume it, and it returns true when the acknowledge is complete. isClosed() is a premature close of the connection, not a completed TLS shutdown. Shutdown that fails throws SslError.
The expected name in the peer certificate is setPeerName() on StreamBuffer. currentCipher() is the cipher in use after the handshake, or a placeholder when no connection is open.
The example opens a client stream and runs the handshake until the stream is connected. Reading and writing application data is the IOStream chapter.
IOStream is the iostream facade for a secure connection. It wraps an underlying iostream with a Context and an OpenMode, and it is the type most callers construct. The handshake, encrypted I/O, and shutdown are the stream operations the group described; this type forwards them to its StreamBuffer.
A default-constructed stream is closed. The constructor that takes a context, an iostream, and a mode opens immediately, as does open(). isConnected() is true after the handshake completes. readHandshake() and writeHandshake() are the non-blocking handshake steps. import() decrypts available input. sslBuffer() is the buffer when the caller needs StreamBuffer::setPeerName() or the streambuf itself. currentCipher() is the negotiated cipher. close() drops the connection without a TLS shutdown.
Writes use the inherited iostream API. flush() pushes encrypted bytes to the underlying stream. Premature EOF on the transport is handled by the code that owns that stream.
The example imports available ciphertext and reads the decrypted bytes, then writes a message and flushes it so the encrypted output reaches the underlying stream.
StreamBuffer is the streambuf that implements the secure connection the group described. IOStream holds one and forwards handshake, import, and shutdown to it. Construct this type directly when the caller already works with a std::streambuf, or when the expected peer name must be set.
setPeerName() is the name expected in the peer certificate. It may be called before or after open(); an open connection receives the name immediately. import() returns the number of bytes consumed from the underlying stream. readHandshake() and writeHandshake() are the same non-blocking handshake as on IOStream. currentCipher() is the cipher in use, or a placeholder when no connection is open.
The example opens a client buffer and sets the name that must appear in the peer certificate.