2
\$\begingroup\$

OK writing a C++ wrapper on top of openSSL.

Setting up the SSL_CTX* object. There are a whole bunch of extra functions to specify functionality on how this object works.

I wanted a way to logically organize them and correctly initialize the object in the constructor. So I have set up the constructor to take 5 parameters that describe how to set up the object correctly.

If I was simply to write out this object it would look like this:

// Note: This first part here is just to show the basic set up.
//       Below I will provide two alternative implementations.
//       It is the implementations below I am looking for help on.
//
//       This is just to give context.
enum class SSLMethodType {Client, Server};
class SSLctxBuilder;
class ProtocolInfo;
class CipherInfo;
class CertificateInfo;
class CertifcateAuthorityInfo;
class ClientCAListInfo;

class SSLctx
{
    friend class SSLctxBuilder;
    private:
        SSL_CTX*            ctx;                            // This is the openSSL
                                                            // object I am wrapping.
    public:
        SSLctx(
            SSLMethodType                  methodType,
            ProtocolInfo            const& protocol,
            CipherInfo              const& cipherList,
            CertificateInfo         const& certificateInfo,
            CertifcateAuthorityInfo const& certifcateAuthority,
            ClientCAListInfo        const& clinetCAList
        );

        ~SSLctx();

        SSLctx(SSLctx const&)                   = delete;
        SSLctx& operator=(SSLctx const&)        = delete;
};

What I want to do is all ow the user to provide only the parameters they want to override and all the others to default. The above objects can all be default constructed and provide a reasonable set of values.

Trying to do this with default values and multiple constructors becomes obnoxious to read and maintain. So I was looking at ways to make it easy to write and easy to use. I though of two techniques.

So the review is on the following code and the two alternative implementations:


The types in the parameters are relatively simple so I will provide them here for quick context:

// When specify valid protocols.
// Either use the default safe values.
// Or you must use an explicit min and max acceptable range.
enum Protocol { TLS_1_0, TLS_1_1, TLS_1_2, TLS_1_3 };
class ProtocolInfo
{
    Protocol    minProtocol     = TLS_1_2;
    Protocol    maxProtocol     = TLS_1_3;
    public:
        ProtocolInfo()
            : ProtocolInfo(TLS_1_2, TLS_1_3)
        {}
        ProtocolInfo(Protocol minVal, Protocol maxVal)
            : minProtocol(minVal)
            , maxProtocol(maxVal)
};


// CipherInfo list of acceptable ciphers that you want to use.
//     cipherList:  Used by TLS_1_2 and below
//     cipherSuite: Used by TLS_1_3 and above
// Don't need any constructor allow the user to override as applicable.
struct CipherInfo
{
    std::string         cipherList          =   "ECDHE-ECDSA-AES128-GCM-SHA256"     ":"
                                                "ECDHE-RSA-AES128-GCM-SHA256"       ":"
                                                "ECDHE-ECDSA-AES256-GCM-SHA384"     ":"
                                                "ECDHE-RSA-AES256-GCM-SHA384"       ":"
                                                "ECDHE-ECDSA-CHACHA20-POLY1305"     ":"
                                                "ECDHE-RSA-CHACHA20-POLY1305"       ":"
                                                "DHE-RSA-AES128-GCM-SHA256"         ":"
                                                "DHE-RSA-AES256-GCM-SHA384";
    std::string         cipherSuite         =   "TLS_AES_256_GCM_SHA384"            ":"
                                                "TLS_CHACHA20_POLY1305_SHA256"      ":"
                                                "TLS_AES_128_GCM_SHA256";
};

// Service specific certificate.
// Either default construct (which means no server certificate)
// or you must provide the Certificate and Key.
// If your key is password protected a user function that is called
// to retrieve the password for the key (default no password).
struct CertificateInfo
{
        std::string     certificateFileName;
        std::string     keyFileName;
        GetPasswordFunc getPassword;

    public:
        CertificateInfo();
        CertificateInfo(std::string const& certificateFileName, std::string const& keyFileName, GetPasswordFunc&& getPassword = [](int){return "";});
};

The next one is slightly convoluted as openSSL provides 3 mechanisms to specify the set of valid trusted root CA certificates: File, Dir or Store. But they all work in the same sort of way. You can load the "Default" system provided trusted root certificates or you can provide a string that is a "File/Dir/URI" that specifies where to find the object. Of course each method has its own unique C function to call:

enum AuthorityType { File, Dir, Store };

template<AuthorityType A>
struct CertifcateAuthorityDataInfo
{
    bool                        loadDefault = false; // Do we want the system defaults.
    std::vector<std::string>    items;               // they all work with multiple valiues.
};

struct CertifcateAuthorityInfo
{
    CertifcateAuthorityDataInfo<File>   file;
    CertifcateAuthorityDataInfo<Dir>    dir;
    CertifcateAuthorityDataInfo<Store>  store;
};

Finally the set of root CA certificates that would be returned by server to client on the initial handshake, telling th eclient the root CA that we trust.

template<AuthorityType A>
struct ClientCAListDataInfo
{
    std::vector<std::string>        items;
};
struct ClientCAListInfo
{
    bool                                verifyClientCA = false;
    ClientCAListDataInfo<File>          file;
    ClientCAListDataInfo<Dir>           dir;
    ClientCAListDataInfo<Store>         store;
};


So the first implementation uses named parameters.
There are two compromises I had to make here:

  1. I had to have a separate struct with all the parameters that I wanted to name.
  2. When I have a list of things I had to bunch them up (eg Items) I have name all the value up front at the same time.

Code:

struct SSLctxInit
{
    ProtocolInfo             protocol;
    CipherInfo               cipherList;
    CertificateInfo          certificateInfo;
    CertifcateAuthorityInfo  certifcateAuthority;
    ClientCAListInfo         clientCAList;
};

class SSLctx
{
    private:
        SSL_CTX*            ctx;
    public:
        SSLctx(SSLMethodType methodType, SSLctxInit const& init = SSLctxInit{});
        ~SSLctx();

        SSLctx(SSLctx const&)                   = delete;
        SSLctx& operator=(SSLctx const&)        = delete;
};

Usage:

SSLctx    ctx(SSLMethodType::Client);          // Simple use all the defaults.

// Default but override the acceptable protocols.
SSLctx    ctx(SSLMethodType::Server, {
                 .protocol = ProtocolInfo{TLS_1_0, TLS_1_2}
              });


// Default but a list of trusted root CA certificates
SSLctx  ctx(SSLMethodType::Server,{
                .certifcateAuthority = {.file = {true, {"path1", "path2", "path3"}}}
           });   

// The other examples are very similar.
// But you can override each one in a sort of trivial way.


// Default but a list of trusted root CA certificates
SSLctx  ctx(SSLMethodType::Server,{
                .protocol = ProtocolInfo{TLS_1_0, TLS_1_2},
                .certifcateAuthority = {
                      .file = {.items = {"path1", "path2", "path3"}},
                      .dir  = {true}
                }
           });   

The alternative implementation I was considering was to use the "Builder" pattern that I have seen being used in Java a lot. The advantage in Java is that their tooling allows the "Builder classes to be auto generated". So in C++ this becomes a bit more boiler plate code.

Another disadvantage is that I have not seen this pattern used much in C++ code. What I like about it is the ability to use methods (named functions) to initialize each of the parameters individually even if they are multiple levels deep.

class SSLctx
{
    friend class SSocket;
    friend class SSLctxBuilder;
    private:
        SSL_CTX*            ctx;

        // Notice I made this private so you have to use a builder.
        // Not sure that is the correct move yet.
        SSLctx(SSLMethodType methodType,
               ProtocolInfo protocol,
               CipherInfo const& cipherList,
               CertificateInfo const& certificateInfo,
               CertifcateAuthorityInfo const& certifcateAuthority,
               ClientCAListInfo const& clinetCAList);

    public:
        ~SSLctx();

        SSLctx(SSLctx const&)                   = delete;
        SSLctx& operator=(SSLctx const&)        = delete;
};

// The Builder is like the SSLctxInit in the last example.
// but has functions to initialize the objects in a simple named way.
// The calling `build()` creates the object.
class SSLctxBuilder
{
    SSLMethodType           method;
    ProtocolInfo            protocolRange;
    CipherInfo              cipherList;
    CertificateInfo         certificate;
    CertifcateAuthorityInfo certifcateAuthority;
    ClientCAListInfo        clientCAList;

    public:
        SSLctxBuilder(SSLMethodType method): method(method)                 {}
        SSLctxBuilder& setProtocolInfo(ProtocolInfo info)                   {protocolRange = std::move(info);return *this;}
        SSLctxBuilder& setCipherInfo(CipherInfo&& info)                     {cipherList = std::move(info);return *this;}
        SSLctxBuilder& addCertificateInfo(CertificateInfo&& info)           {certificate = std::move(info);return *this;}
        SSLctxBuilder& addDefaultCertifcateAuthorityFile()                  {certifcateAuthority.file.loadDefault = true;return *this;}
        SSLctxBuilder& addDefaultCertifcateAuthorityDir()                   {certifcateAuthority.dir.loadDefault = true;return *this;}
        SSLctxBuilder& addDefaultCertifcateAuthorityStore()                 {certifcateAuthority.store.loadDefault = true;return *this;}
        SSLctxBuilder& addCertifcateAuthorityFile(std::string const& file)  {certifcateAuthority.file.items.push_back(file);return *this;}
        SSLctxBuilder& addCertifcateAuthorityDir(std::string const& dir)    {certifcateAuthority.dir.items.push_back(dir);return *this;}
        SSLctxBuilder& addCertifcateAuthorityStore(std::string const& store){certifcateAuthority.store.items.push_back(store);return *this;}
        SSLctxBuilder& validateClientCA()                                   {clientCAList.verifyClientCA = true;return *this;}
        SSLctxBuilder& addFileToClientCAList(std::string const& file)       {clientCAList.file.items.push_back(file);return *this;}
        SSLctxBuilder& addDirToClientCAList(std::string const& dir)         {clientCAList.dir.items.push_back(dir);return *this;}
        SSLctxBuilder& addStoreToClientCAList(std::string const& store)     {clientCAList.store.items.push_back(store);return *this;}

        SSLctx  build()
        {
            return SSLctx{method, protocolRange, cipherList, certificate, certifcateAuthority, clientCAList};
        }
};
  

This changes usage to (Same three examples as above).

Usage:

SSLctx    ctx = SSLctxBuilder(SSLMethodType::Client).build();          // Simple use all the defaults.

// Default but override the acceptable protocols.
SSLctx    ctx = SSLctxBuilder(SSLMethodType::Server)
                   .setProtocolInfo({TLS_1_0, TLS_1_2})
                   .build();


// Default but a list of trusted root CA certificates
SSLctx  ctx = SSLctxBuilder(SSLMethodType::Server)
                   .addDefaultCertifcateAuthorityFile()  // Notice break the list of paths into multiple calls.
                   .addCertifcateAuthorityFile("path1")
                   .addCertifcateAuthorityFile("path2")
                   .addCertifcateAuthorityFile("path3")
                   .build();

// The other examples are very similar.
// But you can override each one in a sort of trivial way.


// Default but a list of trusted root CA certificates
SSLctx  ctx = SSLctxBuilder(SSLMethodType::Server)
                   .setProtocolInfo({TLS_1_0, TLS_1_2})
                   .addCertifcateAuthorityFile("path1")  // Do files and dir as simply as just files.
                   .addCertifcateAuthorityFile("path2")
                   .addCertifcateAuthorityFile("path3")
                   .addDefaultCertifcateAuthorityDir()
                   .build();
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

Named parameters are less extensible

The named parameters approach looks nice, as it is a language feature. However, it requires C++20, only works on aggregates, and if you ever want to add more members to SSLctxInit you will break the ABI. I would therefore give a preference to the builder pattern in this case, which can avoid all these issues.

Simplify the builder pattern

You can simplify the builder pattern a little bit. Instead of having a builder object that build()s an SSLctx, I would just have an object representing a set of context parameters that can be modified, and then pass that to the constructor of SSLctx. So in the end you would write:

SSLctx ctx(SSLctxInit(SSLMethodType::Server)
           .setProtocolInfo({TLS_1_0, TLS_1_2})
           .addCertifcateAuthorityFiles("path1", "path2", "path3")
           .addDefaultCertifcateAuthorityDir()
);

Actually, SSLctxInit can still be an aggregate struct, so you could support both the builder pattern and named parameters this way.

Alternatives

I see that in OpenSSL's own API, SSL_CTX_new() only needs the method type, you set other parameters later. So you can mirror that in your interface, and let SSLctx itself implement the builder pattern:

auto ctx = SSLctx(SSLMethodType::Server)
           .setProtocolInfo({TLS_1_0, TLS_1_2})
           .addCertifcateAuthorityFiles("path1", "path2", "path3")
           .addDefaultCertifcateAuthorityDir();

Another completely different possibility is to have a variadic constructor, where you can pass whatever information you want in arbitrary order:

class SSLctx
{
    …
    template<typename... Params>
    SSLctx(SSLMethodType methodType, Params&&... params);
    …
};

And then use it like so:

SSLctx ctx(SSLMethodType::Server,
           ProtocolInfo(TLS_1_0, TLS_1_2),
           CertifcateAuthorityFiles("path1", "path2", "path3"),
           DefaultCertificateAuthorityDir()
);
\$\endgroup\$
1
  • \$\begingroup\$ I like the variadic initializer idea. Thanks for the link to aggregates. I could not find good links it took me a while to get it compiling. \$\endgroup\$ Commented Aug 30, 2023 at 21:41

Not the answer you're looking for? Browse other questions tagged or ask your own question.