Secure Communication

Certificate Management

SSL/TLS communication usually requires certificates and keys, i.e. for server or client authentication and to build a list of trusted CA certificates to verify the peer. Certificates and keys encoded in PKCS12 format can be loaded into a Pt::Ssl::CertificateStore, and then be used by the application.

const char* password = ...;
std::ifstream ifs("certs.p12");
store.loadPkcs12(ifs, password);
for(it = store.begin(); it != store.end(); ++it)
{
std::cout << "subject: " << it->subject() << std::endl;
}
const Pt::Ssl::Certificate* cert = store.findCertificate("SGC Mainframe");
if( ! cert)
std::cerr << "certifictate is missing" << std::endl;

The example shown above imports certificates and keys from a file, which is protected by a password. Certificates in the store can be inspected using the iterator API, or searched for by a subject string. Note, that a private key will be assoziated with its certificate and is not exposed or accessible by any API functions.

Context Initialization

Before any SSL/TLS connections can be established, a Pt::Ssl::Context has to be created, which allows to use the same set of certificates and settings for many connections.

Pt::Ssl::Context ctx(Pt::Ssl::TLSv1); // SSLv2, SSLv3or2, SSLv3, TLSv1
Pt::Ssl::Certificate& myCA = store.getCertifictate("My Certificate");
ctx.setIdentity(myCert);
Pt::Ssl::Certificate& myCA = store.getCertifictate("My CA");
ctx.addCACertificate(myCA);
ctx.setVerifyMode(Pt::Ssl::AlwaysVerify); // NoVerify, TryVerify, AlwaysVerify

A context can be created for a specific protocol type, e.g. TLSv1 which limits, the communication to that protocol. The method Pt::Ssl::Context::setIdentity() sets the certificate presented to the peer. Since private keys are always assoziated with a certificate, when loaded into the CertificateStore, selecting a certificate means also selecting it's private key. The trusted CA certificates to verify the peer's identity can be added using the Pt::Ssl::Context::addCACertificate() method. Pt::Ssl::Context::setVerifyMode() indicates whether the peer is required to authenticate itself during the handshake.

Opening a Connection

A secure connection can be established using a Pt::Ssl::IOStream or a Pt::Ssl::StreamBuffer. These classes are built on top of the iostreams library of the C++ standard and implement a std::iostream or std::streambuf, respectively. They operate on another underlying std::iostream, from which data is read and decrypted or encrypted and written to. This allows the Pt-Ssl module to be independent of any socket or transport layer.

Besides an underlying std::iostream for the data transport to the peer, a Pt::Ssl::Context is required to open a Pt::Ssl::IOStream. The settings and certificates from this context will be used for the connection. Furthermore, the stream can be opened as a client stream or server stream, indicated by the Pt::Ssl::OpenMode, being either Pt::Ssl::Connect or Pt::Ssl::Accept.

std::iostream& ios = ...;
// constructs a ssl client stream
Pt::Ssl::IOStream clientSsl(ctx, ios, Pt::Ssl::Connect);
// constructs a ssl server stream
Pt::Ssl::IOStream serverSsl(ctx, ios, Pt::Ssl::Accept);

The Pt::Ssl::OpenMode decides whether a client handshake or a server handshake will be carried out. After a Pt::Ssl::IOStream is created and opened, the handshake can be peformed with the methods Pt::Ssl::IOStream::readHandshake() and Pt::Ssl::IOStream::writeHandshake().

Pt::Ssl::IOStream& ssl = ...;
while( ! ssl.isConnected() )
{
bool wantRead = ssl.readHandshake();
if(wantRead)
{
[ make more data available in the underlying stream ]
continue;
}
bool wantWrite = ssl.writeHandshake();
if( wantWrite )
{
[ flush the underlying stream ]
continue;
}
}

Essentially, readHandshake() and writeHandshake() have to be called repeatedly, until the Pt::Ssl::IOStream is connected. If readHandshake() returns true, more data has to be read, but not enough data was available in the underlying stream. Only as many bytes will be consumed from the underlying stream as std::streambuf::in_avail() indicates. This allows non-blocking communication, since readHandshake() can be called again when more data becomes available. If readHandshake() returns false, no more data needs to be read. Then writeHandshake() is called, which returns true if data was written to the underlying stream that needs to be transfered. In the non-blocking case, we can call writeHandshake() again once the data was written.

Reading and Writing Data

Once the connection is open, data can be encrypted and decrypted. This can be achieved using the blocking I/O API inherited from std::iostream or the non-blocking API offered by Pt::Ssl::IOStream and Pt::Ssl::StreamBuffer. In the non-blocking case, available data can be imported from the underlying stream.

Pt::Ssl::IOStream& ssl = ...;
for(;;)
{
ssl.import();
std::streamsize avail = ssl.sslBuffer().in_avail();
if(avail <= 0)
break;
do
{
char buf[255];
std::streamsize n = ssl.readsome( buf, sizeof(buf) );
std::cout.write(buf, n);
}
while( ssl.sslBuffer().in_avail() > 0 );
}
if( ssl.isShutdown() )
{
std::cout << "received shutdown alert" << std::endl;
}

The method Pt::Ssl::IOStream::import() does not read more data from the underlying std::iostream than in_avail() of it's std::streambuf indicates. It might decrypt only a part of the available data, if not all bytes could be imported into the buffer of the Pt::Ssl::IOStream. To drain the underlying stream, import() should be called in a loop. After calling import(), decrypted data may be available and can be processed. It is also possible that a shutdown alert was received, which can be checked for with isShutdown(). Premature EOF has to be handled by the code that maintains the underlying std::iostream. When data is written to a Pt::Ssl::IOStream, it is encrypted and eventually written to the underying std::iostream.

Pt::Ssl::IOStream& ssl = ...;
ssl << "pi is: " << 3.1415 ;
ssl.flush();

To make sure the encrypted data is completely written to the underlying stream, the Pt::Ssl::IOStream should be flushed. If not flushed, a part or all of the data might reside in the output buffer area. The API inherited from std::ostream can be used to flush the stream, that is std::ostream::flush or the stream manipulators std::flush and std::endl. The code that provided the underlying stream then has to transfer the data to the peer. This works well with the Pt::System::IOStream as the underlying stream, which only buffers the data written to it. When a limit is reached, the data can be written asynchronously making room for more encrypted data.

Connection Shutdown

A connection shutdown alert can be initiated by either peer and the method Pt::Ssl::IOStream::isShutdown() indicates that the connection is shutting down. If a shutdown alert is received during a read operation, Pt::Ssl::IOStream::shutdown() will write the shutdown acknowledge to the underlying stream and return true. This data still needs to be sent to the peer then, but from the perspective of the Pt::Ssl::IOStream, the connection is considered closed and isShutdown() no longer returns true. If shutdown() is called to initiate a shutdown, the shutdown alert is written to the underlying std::iostream and needs to be sent to the peer. When the shutdown acknowledgement becomes available in the underlying std::iostream, shutdown() must be called again to consume it, which will return true if the shutdown acknowledge is complete.