Generating an X509 Certificate with OpenSSL C++


Generating our own certificate

Generating our own certificate

In this article, I want to tell you how to work with X509 certificate using OpenSSL 3.0.0 in C ++, starting from generating your certificate and ending with its validation.

Since there is almost no information on the Internet on this topic, everything that I will tell you, I learned from my sad experience with this library. I really hope that this article will be useful to you and can save you time.

In this article, I will not tell you what an X509 certificate is, I hope you already know this, and if not, then the link to the article is here.

Create a certificate

Let’s start simple, creating a certificate. First we need to include the header file. Everything is simple here, to create – we call X509_new (), and to clear the memory – X509_free ().

#include <openssl/x509v3.h>

int main() {
  X509* certificate = X509_new();
  if (certificate != nullptr) {
    X509_free(certificate);
  }
}

If we already have a completed X509 certificate structure, and we need to copy it completely into our variable, then we can use the X509_dup () function, into which we need to pass a pointer to the certificate we want to copy.

#include <openssl/x509v3.h>
int main() {
  X509* certificate = X509_new();
  if (certificate != nullptr) {
    X509_free(certificate);
  }
}

Copying the certificate

#include <openssl/x509v3.h>

int main() {
  X509* certificate = X509_new();
  if  (certificate != nullptr) {
    X509* duplicate = X509_dup(certificate);
    X509_free(duplicate);
    X509_free(certificate);
  }
}

Let’s add a property to our certificate, let’s start with a number, aka serial number. Using X509_get_serialNumber, we get a pointer to the certificate property, and then, using ASN1_INTEGER_set(), we set the required value. ASN1_INTEGER_set() will return 1 for a successful call, 0 for a failed one. This rule works for all functions when working with a certificate.

#include <iostream>
#include <openssl/x509v3.h>

int main() {
  X509* certificate = X509_new();
  if (certificate != nullptr) {
    int32_t serialNum = 1;
    
    ASN1_INTEGER* serialNumber = X509_get_serialNumber(certificate); 
    if (serialNumber != nullptr) {
      const int res = ASN1_INTEGER_set(serialNumber, serial);
      if (res != 1) {
        std::cerr << "ASN1_INTEGER_set failed" << std::endl;
      }
    }
    
    X509_free(certificate);
  }
}

We expose the version

Let’s set the version of our certificate using the X509_set_version() function. There are currently three versions of the certificate, which correspond to the following values:

  1. 0x0

  2. 0x1

  3. 0x2

Set the Subject Name

Let’s add a Country name for our certificate. Using the X509_get_subject_name function we can get a pointer to the subject name of our certificate, and by calling X509_NAME_add_entry_by_txt() we can update it.

#include <iostream>
#include <openssl/x509v3.h>

int main() {
  X509* certificate = X509_new();
  if (certificate != nullptr) {
    int32_t serialNum = 1;
    
    ASN1_INTEGER* serialNumber = X509_get_serialNumber(certificate); 
    if (serialNumber != nullptr) {
      int res = ASN1_INTEGER_set(serialNumber, serial);
      if (res != 1) {
        std::cerr << "ASN1_INTEGER_set failed" << std::endl;
      }
    }
    const long ver = 0x0; // version 1
    int res = X509_set_version(certificate, ver);
    if (res != 1) {
      std::cerr << "X509_set_version failed" << std::endl;
    }
    
    static const char* name = "Country name";
    509_NAME* subjectName = X509_get_subject_name(certificate);
    if (subjectName != nullptr) {
      res = X509_NAME_add_entry_by_txt(subjectName, "CN", MBSTRING_ASC, (unsigned char*)name, -1, -1, 0)
      if (res != 1) {
        std::cerr << "X509_NAME_add_entry_by_txt failed" << std::endl;
      }
    }
    
    X509_free(certificate);
  }
}

Using X509_NAME_add_entry_by_txt we can add any key-values ​​to our subject name:

#include <iostream>
#include <openssl/x509v3.h>

int main() {
  X509* certificate = X509_new();
  if (certificate != nullptr) {
    int32_t serialNum = 1;
    
    ASN1_INTEGER* serialNumber = X509_get_serialNumber(certificate); 
    if (serialNumber != nullptr) {
      int res = ASN1_INTEGER_set(serialNumber, serial);
      if (res != 1) {
        std::cerr << "ASN1_INTEGER_set failed" << std::endl;
      }
    }
    const long ver = 0x0; // version 1
    int res = X509_set_version(certificate, ver);
    if (res != 1) {
      std::cerr << "X509_set_version failed" << std::endl;
    }

    static const char* key = "My Key";
    static const char* value = "Value";
    509_NAME* subjectName = X509_get_subject_name(certificate);
    if (subjectName != nullptr) {
      res = X509_NAME_add_entry_by_txt(subjectName, key, MBSTRING_ASC, (unsigned char*)value, -1, -1, 0)
      if (res != 1) {
        std::cerr << "X509_NAME_add_entry_by_txt failed" << std::endl;
      }
    }
    
    X509_free(certificate);
  }
}

Set the validity period of the certificate

Now let’s add the validity period of our certificate, from which to which date it will work. To do this, we use the X509_getm_notAfter() and X509_getm_notBefore() functions. Further in the examples, I skip error handling, but you should keep it in mind and use it in a real project.

#include <iostream>
#include <ctime>
#include <openssl/x509v3.h>

int main() {
  X509* certificate = X509_new();
  if (certificate != nullptr) {
    uint32_t y = 2023;
    uint32_t m = 12;
    uint32_t d = 25;
    int32_t offset_days = 0;
    
    struct tm base;
    memset(&base, 0, sizeof(base));
    base.tm_year = y - 1900;
    base.tm_mon = m - 1;
    base.tm_mday = d;
    time_t tm = mktime(&base);

    X509_time_adj(X509_getm_notAfter(&x), 86400L * offset_days, &tm);
    X509_free(certificate);
  }
}

Thus, we said that our certificate will be valid starting from 2023.12.25. Now let’s add a starting point. The code is exactly the same as for the initial one, except for the name of the function for getting properties – X509_getm_notBefore(). So, our certificate is valid from 2022.12.25

#include <iostream>
#include <ctime>
#include <openssl/x509v3.h>

int main() {
  X509* certificate = X509_new();
  if (certificate != nullptr) {
    uint32_t y = 2022;
    uint32_t m = 12;
    uint32_t d = 25;
    int32_t offset_days = 0;
    
    struct tm base;
    memset(&base, 0, sizeof(base));
    base.tm_year = y - 1900;
    base.tm_mon = m - 1;
    base.tm_mday = d;
    time_t tm = mktime(&base);

    X509_time_adj(X509_getm_notBefore(&x), 86400L * offset_days, &tm);
    X509_free(certificate);
  }
}

We expose the Issuer

Suppose we needed to set an Issuer for our certificate, the certificate that signed ours. To do this, use X509_set_issuer_name().

#include <iostream>
#include <openssl/x509v3.h>

int main() {
  X509* rootCertificate = X509_new();
  ///
  X509* certificate = X509_new();
  if (certificate != nullptr) {
    if (rootCertificate != nullptr) {
      X509_set_issuer_name(certificate, X509_get_subject_name(rootCertificate));
      X509_free(rootCertificate);
    }
    X509_free(certificate);
  }
}

Suppose we needed to add something to the Issuer field of our certificate, this can also be done as follows, using the already familiar X509_NAME_add_entry_by_txt():

#include <iostream>
#include <openssl/x509v3.h>

int main() {
  X509* certificate = X509_new();
  if (certificate != nullptr) {
    static const char* key = "My Key";
    static const char* value = "Value";
    
    X509_NAME* issuerName = X509_get_issuer_name(certificate);
    X509_NAME_add_entry_by_txt(issuerName, key, MBSTRING_ASC, (unsigned char*)value, -1, -1, 0) != 1) {

    X509_free(certificate);
  }
}

Standard Extensions

Let’s add a couple of standard extensions for our certificate. Standard extensions in OpenSSL have their own IDs (nid). For example, for Basic Constraints it is NID_basic_constraints and for Key Usage it is NID_key_usage. These IDs are necessary if we want to set certain extensions for our certificate.

#include <iostream>
#include <openssl/x509v3.h>

int main() {
  X509* rootCertificate = X509_new();
  ///
  X509* certificate = X509_new();
  if (certificate != nullptr) {
    X509V3_CTX ctx; // create context
    X509V3_set_ctx_nodb(&ctx); // init context
    X509V3_set_ctx(&ctx, &issuer, &x, nullptr, nullptr, 0); // set context

    int nid = NID_basic_constraints;
    static const char* value = "critical,CA:TRUE";
    
    X509_EXTENSION* ex = X509V3_EXT_conf_nid(nullptr, &ctx, nid, value);
    if (ex != nullptr) {
        const int ret = X509_add_ext(&x, ex.get(), -1);
        if (ret != 1) {
           std::cerr << "Failed to add extension" << std::endl;
        }
      X509_EXTENSION_free(ex);
    }
    
    X509_free(rootCertificate);
    X509_free(certificate);
  }
}

Custom extensions

This all works until we need to add a custom extension, support for which was added in the third version of the certificate. Everything is a little more complicated, but also doable. To begin with, we will create an object in the OpenSSL database, in which we will later write our extension. By its nid we get the object itself, create an empty extension, link it and our object. Then, set the criticality of the extension. Now we can start preparing the data, we create a string that will store the key and value of our extension. Assign it to the extension and finally add the extension to the certificate.

#include <iostream>
#include <openssl/x509v3.h>

int main() {
  X509* certificate = X509_new();
  if (certificate != nullptr) {
    // create object in OpenSSL
    const int nid = OBJ_create(key, value, nullptr);
    ASN1_OBJECT* obj = OBJ_nid2obj(nid);

    // create empty extenstion
    X509_EXTENSION* ex = X509_EXTENSION_new();

    // set object to extension
    int res = X509_EXTENSION_set_object(ex, obj);
    if (res == 1) {
      // set critical for our extension
      res = X509_EXTENSION_set_critical(ex, 0);
      if (res == 1) {
        // create string for our data
        ASN1_OCTET_STRING* data = ASN1_OCTET_STRING_new();
        static const char* key = "My Key";
        static const char* value = "Value";

        // set key value pair
        res = ASN1_OCTET_STRING_set(data, reinterpret_cast<unsigned const char*>(value), strlen(value));
        if (res == 1) {
           // set data to our extension
           res = X509_EXTENSION_set_data(ex, data);
           if (res == 1) {
             // finally, add custom extension
             res = X509_add_ext(&x, ex, -1);
             if (res != 1) {
                std::cerr << "Failed to add extension" << std::endl;
             }
           }
        }
      }
    }

    X509_free(certificate);
  }
}

This is a rather complicated and long way, but working, as I found out in practice. But if you have a faster or more efficient option – please comment.

Well, what is a certificate without a public key? Let’s add it. Everything is very simple here, we call X509_set_pubkey () and you’re done. If you are interested in the process of generating and working with public and private keys in OpenSSL, then write comments.

Exposing the public key

#include <iostream>
#include <openssl/x509v3.h>
#include <openssl/evp.h>

int main() {
  /// load public key from somewhere
  EVP_PKEY key;
  ///
  X509* certificate = X509_new();
  if (certificate != nullptr) {
    X509_set_pubkey(certificate, &key);
    
    X509_free(certificate);
  }
}

We sign the certificate

And finally, we sign our certificate. There are several popular signature algorithms, but the main one is SHA256, which is shown in the example. From it you can understand the process, and by its name you can find the one you need in the OpenSSL source code.

#include <iostream>
#include <openssl/x509v3.h>
#include <openssl/evp.h>

int main() {
  /// load private key from somewhere
  EVP_PKEY key;
  ///
  X509* certificate = X509_new();
  if (certificate != nullptr) {
    X509_sign(certificate, &key, EVP_sha256());
    
    X509_free(certificate);
  }
}

Afterword

Finally, I would say that std::unique_ptr is a good fit for working with C functions and structures, which will greatly simplify your life and allow you to remember to free resources. I intentionally didn’t use this construct in the example so as not to clutter up the code, but in real life, it’s better to use them.

std::unique_ptr<X509_EXTENSION, decltype(&::X509_EXTENSION_free)> ex(X509_EXTENSION_new(), ::X509_EXTENSION_free);

If you liked this article and want to see a continuation describing how to work with keys and storage of certificates, as well as learn how to validate them – like and leave comments.

I hope this article will be useful to you and save you a lot of time, saving you hours of reading scanty documentation and rare manuals.

Similar Posts

Leave a Reply

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