Validate X509 certificate with OpenSSL C++

This article is a logical continuation of the article about certificate generation. In it, we’ll look at how we can validate a certificate using OpenSSL and C++.

In OpenSSL, the concept of storage is used to work with certificates. Let’s create it using X509_STORE_new();

std::unique_ptr<X509_STORE, decltype(&::X509_STORE_free)> store(
  X509_STORE_new(), &::X509_STORE_free);

Next, we need to “populate” our store with the certificates we plan to validate our target certificate with. If our certificate is signed by a certificate that is already in trusted storage, then we only need to call one function – X509_STORE_load_path(). It is necessary to pass a pointer to our storage, as well as the full path to the certificates. For Linux, this path might be: /etc/ssl/certs.

bool addCAPath(X509_STORE* store, const char* path) {
    return X509_STORE_load_path(store, path) == 1;
}

Also, OpenSSL has the ability to load a file with multiple certificates using the X509_STORE_load_file() function. We also pass a pointer to the storage, as well as the file name, into it. For example, /etc/ssl/certs/ca-certificates.crt.

bool addCABundle(X509_STORE* store, const char* path) {
    X509_STORE_load_file(store, path) == 1;
}

If, however, we want to add our own certificate to the storage, with which we are going to validate the target one, then we can add it separately using X509_STORE_add_cert (). In it we pass a pointer to the storage, as well as the certificate itself, which we want to add.

bool addCert(X509_STORE* store, X509* cert) {
    return X509_STORE_add_cert(store, cert) == 1;
}

Next, we can start the certificate validation process. First, we need to construct the X509_CTX context. By the way, almost all of the new OpenSSL 3.0.0 API is built around different contexts. In order not to suffer with resource management, we will create a special delimiter for our context, consisting of a call to two functions: clear the context and release memory for the context.

auto storeContextDeleter = [](X509_STORE_CTX* ctx) {
    X509_STORE_CTX_cleanup(ctx);
    X509_STORE_CTX_free(ctx);
};

std::unique_ptr<X509_STORE_CTX, decltype(storeContextDeleter)>
        storeCtx(X509_STORE_CTX_new(), storeContextDeleter);
if (storeCtx == nullptr) {
    std::cerr << "Failed X509_STORE_CTX_new" << std::endl;
    return -1;
}

So, we have created a context, then we can set a callback that will be called by OpenSSL after the certificate has been validated. You may ask – why is this needed, but it is needed because in this callback we can suppress various OpenSSL errors so that the validation is considered successful. For example, if OpenSSL returned an error to us in this callback – X509_V_ERR_CERT_HAS_EXPIRED, which means that the certificate is already “rotten”, but for our own reasons we want to allow such obscenity, then we can simply return OK to it.

static int verifyCallback(int ok, X509_STORE_CTX *ctx) {
    const int err = X509_STORE_CTX_get_error(ctx);
    if (err != 0) {
        std::cerr << "Failed to verify cert " << err << std::endl;
    }
    return ok;
}
///
X509_STORE_set_verify_cb_func(store.get(), verifyCallback);

Next, we initialize our context and call the validation function:

if (X509_STORE_CTX_init(storeCtx.get(), store.get(), cert.get(), nullptr) == 0) {
    std::cerr << "Failed X509_STORE_CTX_init" << std::endl;
    return -1;
}

if (X509_verify_cert(storeCtx.get()) != 1) {
    std::cerr << "Failed X509_verify_cert" << std::endl;
    return -1;
}

There is nothing more to tell here. I will be glad to read your comments. All examples, as before, are uploaded to my github. The next article will describe working with keys, as well as a bunch of useful functions for reading and writing keys and certificates.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *