DEV Community

Cover image for Pragmatically Generating a Self-Signed Certificate and Private Key using OpenSSL
Ian Spence
Ian Spence

Posted on • Edited on

Pragmatically Generating a Self-Signed Certificate and Private Key using OpenSSL

Recently I found myself needing to generate a HTTPS Server Certificate and Private Key for an iOS app using OpenSSL, what surprised me was the total lack of documentation for OpenSSL.

While there is plenty of function documentation, what OpenSSL really lacks is examples of how it all fits together. It's like having all of the pieces of a puzzle, but no picture of that the finished product will look like. Many online examples are insecure or make use of deprecated functions, a serious concern considering the consequences.

This tutorial will cover the basics for how to generate a RSA or ECDSA Private Key and a X509 Server Certificate for your application in C. For this tutorial, we will be using OpenSSL 1.1.0f.

Important note: This tutorial is written for the modern version of OpenSSL, 1.1.x, and is not backwards compatible with OpenSSL 1.0.x. If you are still on the 1.0.x train, it's highly recommended that you upgrade your application.

Generating the Private Key

A private key is required for any PKI encryption setup, and you generally have two choices for algorithms: RSA and ECDSA.

ECDSA V.S. RSA

RSA has been the defacto standard for private keys for quite a long time, and if used correctly is still secure. The security of an RSA key relies on how large the “big number is when the key is created. The recommended size of this number keeps going up and up as we're getting better and better at breaking smaller numbers.
ECDSA keys, however, are created differently than RSA keys, and are much harder to break. In fact, no tangible progress has been made on solving the problem that ECDSA keys use The Elliptic Curve Discrete Logarithm Problem (ECDLP) since 1985. Because of this, ECDSA keys provide much greater security without the need for larger numbers. For example: a 256-bit ECDSA key is equivalent to a 3,248-bit RSA key.
Not only are ECDSA keys more secure than most RSA keys, they're also much less CPU intensive. CloudFlare dissected the performance of the two in their write up on the algorithm.

                            sign/s
256 bit ecdsa (nistp256)    9516.8
rsa 2048 bits               1001.8

(openssl 1.0.2 beta on x86_64 with enable-ec_nistp_64_gcc_128)

Enter fullscreen mode Exit fullscreen mode

To put it simply: ECDSA keys are 9x less CPU intensive while providing greater security than RSA keys.

Generating an RSA Private Key

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

/* ... */

OPENSSL_init_ssl(0, NULL);
OPENSSL_init_crypto(0, NULL);

EVP_PKEY * pkey      = EVP_PKEY_new();
if (!pkey) {
    // OpenSSL Error. Use `ERR_peek_last_error_line` to find out more.
    return NULL;
}

BIGNUM   * bigNumber = BN_new();
int        exponent  = RSA_F4;
RSA      * rsa       = RSA_new();

if (BN_set_word(bigNumber, exponent) < 0) {
    // OpenSSL Error. Use `ERR_peek_last_error_line` to find out more.
    goto cleanup;
}

if (RSA_generate_key_ex(rsa,
                        2048,
                        bigNumber,
                        NULL) < 0) {
    // OpenSSL Error. Use `ERR_peek_last_error_line` to find out more.
    goto cleanup;
}

if (!EVP_PKEY_set1_RSA(pkey, rsa)) {
    // OpenSSL Error. Use `ERR_peek_last_error_line` to find out more.
    goto cleanup;
}

cleanup:
    RSA_free(rsa);
    BN_free(bigNumber);

    return pkey;
Enter fullscreen mode Exit fullscreen mode

Generating a ECDSA Private Key

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

/* ... */

OPENSSL_init_ssl(0, NULL);
OPENSSL_init_crypto(0, NULL);

EVP_PKEY * pkey = EVP_PKEY_new();

EC_KEY * ecc = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);
if (!ecc) {
    // OpenSSL Error. Use `ERR_peek_last_error_line` to find out more.
    return NULL;
}

EC_KEY_set_asn1_flag(ecc, OPENSSL_EC_NAMED_CURVE);
if (EC_KEY_generate_key(ecc) < 0) {
    // OpenSSL Error. Use `ERR_peek_last_error_line` to find out more.
    return NULL;
}

if (EVP_PKEY_assign_EC_KEY(pkey, ecc) < 0) {
    // OpenSSL Error. Use `ERR_peek_last_error_line` to find out more.
    return NULL;
}

return pkey;
Enter fullscreen mode Exit fullscreen mode

Generating the Server Certificate

The server certificate is the client-facing piece of information that details the connection to the server. It tells the client what type of cipher to use, and validates the identity of the server. We're generating a self-signed certificate in this case, so your computer won't trust the certificate until you install it locally.

#include <openssl/x509v3.h>

/* ... */

OPENSSL_init_ssl(0, NULL);
OPENSSL_init_crypto(0, NULL);

X509 * x509;
x509 = X509_new();
// 2 means Version 3.
X509_set_version(x509, 2L);

// Generate your private key using one of the above methods
EVP_PKEY * pkey;
X509_set_pubkey(x509, pkey);
EVP_PKEY_free(pkey);

// Generate a random serial number for this certificate.
// There are various ways to do this, depending on the platform.
// I'll leave this up to you.
unsigned long random_serial_number;

// Set Serial Number
ASN1_INTEGER_set(X509_get_serialNumber(x509), random_serial_number);

// Set Validity Date Range
// These value is appended to the systems current time stamp meaning that 0 = now.
X509_gmtime_adj((ASN1_TIME *)X509_get0_notBefore(x509), 0);
// 60 * 60 * 24 * NUMBER_OF_DAYS_TO_BE_VALID
X509_gmtime_adj((ASN1_TIME *)X509_get0_notAfter(x509), 7776000);

X509_NAME * name;
name = X509_get_subject_name(x509);

// Now to add the subject name fields to the certificate
// I use a macro here to make it cleaner.
#define addName(field, value) X509_NAME_add_entry_by_txt(name, field,  MBSTRING_ASC, (unsigned char *)value, -1, -1, 0)

// The domain name or IP address that the certificate is issued for.
addName("CN", "ecn.io");

// The organizational unit for the cert. Usually this is a department.
addName("OU", "Certificate Authority");

// The organization of the cert.
addName("O",  "ecn.io Blog");

// The city of the organization.
addName("L",  "Vancouver");

// The state/province of the organization.
addName("S",  "British Columbia");

// The country (ISO 3166) of the organization
addName("C",  "CA");

X509_set_issuer_name(x509, name);

// Modern browsers ignore the CN subject field and refer only to the Subject
// Alternative Name extension, which allows you to specify
// multiple domain names, IP addresses, and more for a single certificate.
// The SAN value is in the following format:
// <TYPE>.<INDEX>:<VALUE>
// Common types are:
// - DNS
// - IP
// - email
// - URI
// Join multiple SAN entries using a comma.
// In this example, this certificate is for both the `ecn.io` and `*.ecn.io`
// domains. Which means it'll cover all single level subdomains for ecn.io
// (E.G. blog.ecn.io but not awesome.blog.ecn.io)
const char * san_value = "DNS.1:ecn.io,DNS.2:*.ecn.io";

X509_EXTENSION * extension = X509V3_EXT_conf_nid(NULL, NULL, NID_subject_alt_name, san_value);
if (X509_add_ext(x509, extension, -1) == 0) {
    // OpenSSL Error. Use `ERR_peek_last_error_line` to find out more.
    X509_EXTENSION_free(extension);
    return;
}
X509_EXTENSION_free(extension);

// Specify the encryption algorithm of the signature.
// SHA256 should suit your needs.
if (X509_sign(x509, pkey, EVP_sha256()) < 0) {
    // OpenSSL Error. Use `ERR_peek_last_error_line` to find out more.
    return;
}
Enter fullscreen mode Exit fullscreen mode

Saving your private key and certificate

OpenSSL provides you with the mechanisms to save your private key and certificate to disk, in various formats. There are many ways to encode certificate and keys, I'm going to show you how to use the most common two, PKCS12 (P12), and PEM.

The security of the private key is the most critical component of PKI, and all of your work can easily be wasted if you don't take good care to ensure your private key is safe. There are many things you can do to keep your private key safe, but at a minimum you should be changing the file permissions to not be world-readable. Jeff Atwood has a good write-up on Keeping Private Keys Private.

PKCS12 V.S. PEM

As mentioned above, theres many ways to encode keys, but the two that I'll go over are PKCS12 and PEM. PKCS12 (also known as P12) is the successor to Microsoft's PFX format, which was criticized for being overly complex and difficult to use, and while P12 is still quite complex thankfully OpenSSL does nearly all of the heavy lifting for you, leaving you to only implement a handful of commands.

The largest advantage to using P12 is that they are always encrypted using a pass phrase, and can contain multiple certificates and keys, in so called "buckets", within a single file.

The common alternative to P12 is Privacy Enhanced Mail (PEM). PEM is a very simple standard for encoding certificates and keys using base64, which can be easily moved around as ASCII text, as opposed to binary data like with DER and P12.

As PEM is strictly just an encoding, it does not require encrypting any data, and therefor is commonly used on Web Servers so no hard-coded passwords need to be added to configuration files. As well, unlike P12, PEM encoding is 1 object per file, meaning 1 certificate file and 1 key file.

Save using PKCS12

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

/* ... */

OPENSSL_init_ssl(0, NULL);
OPENSSL_init_crypto(0, NULL);

// Prompt the user for the password to encrypt the P12 file using
// DON'T USE A HARD-CODED PASSWORD OR I WILL PERSONALLY COME OVER
// THERE AND SCREAM AT YOU
const char * export_password;

PKCS12 * p12 = PKCS12_create(export_password, NULL, pkey, x509, NULL, 0, 0, PKCS12_DEFAULT_ITER, 1, NID_key_usage);

// The full path of the P12 file that you'll create
const char * save_path = "./server.p12";

FILE * f = fopen(save_path, "wb");

if (i2d_PKCS12_fp(f, p12) != 1) {
    // OpenSSL Error. Use `ERR_peek_last_error_line` to find out more.
    fclose(f);
    return;
}
fclose(f);
finished(path, nil);
Enter fullscreen mode Exit fullscreen mode

Save using PEM

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

/* ... */

// The full path of the key and crt file that you'll create
const char * key_path = "./server.key";
const char * crt_path = "./server.crt";

FILE * f = fopen(key_path, "wb");

// Prompt the user for the password to encrypt the key file using
// DON'T USE A HARD-CODED PASSWORD OR I WILL PERSONALLY COME OVER
// THERE AND SCREAM AT YOU
const char * export_password;

// Here you write the private key (pkey) to disk. OpenSSL will encrypt the
// file using the password and cipher you provide.
if (PEM_write_PrivateKey(f,
                         pkey,
                         EVP_des_ede3_cbc(),
                         (unsigned char *)export_password,
                         (int)strlen(export_password),
                         NULL,
                         NULL) < 0) {
    // OpenSSL Error. Use `ERR_peek_last_error_line` to find out more.
    fclose(f);
}
fclose(f);

f = fopen(crt_path, "wb");

// Here you write the certificate to the disk. No encryption is needed here
// since this is public facing information
if (PEM_write_X509(f, x509) < 0) {
    // OpenSSL Error. Use `ERR_peek_last_error_line` to find out more.
    fclose(f);
}
fclose(f);
Enter fullscreen mode Exit fullscreen mode

Verifying our Server Certificate

OpenSSL has a nifty little function that lets you quickly print out the summary of a X509 certificate to a file (or stdout):

X509 * cert = ...
X509_print_fp(stdout, cert);
Enter fullscreen mode Exit fullscreen mode

Inspecting the certificate created with a RSA private key:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 1365365151 (0x5161d19f)
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN=ecn.io, OU=Certificate Authority, O=ecn.io, L=Vancouver, C=CA
        Validity
            Not Before: Oct 30 20:33:01 2017 GMT
            Not After : Oct 31 02:33:00 2018 GMT
        Subject: CN=ecn.io, OU=Certificate Authority, O=ecn.io, L=Vancouver, C=CA
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    ...omitted...
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Subject Alternative Name:
                DNS:ecn.io, DNS:*.ecn.io
    Signature Algorithm: sha256WithRSAEncryption
         ...omitted...
Enter fullscreen mode Exit fullscreen mode

And with the ECDSA key:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 1365365151 (0x5161d19f)
    Signature Algorithm: ecdsa-with-SHA256
        Issuer: CN=ecn.io, OU=Certificate Authority, O=ecn.io, L=Vancouver, C=CA
        Validity
            Not Before: Oct 30 20:33:01 2017 GMT
            Not After : Oct 31 02:33:00 2018 GMT
        Subject: CN=ecn.io, OU=Certificate Authority, O=ecn.io, L=Vancouver, C=CA
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    ...omitted...
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
            X509v3 Subject Alternative Name:
                DNS:ecn.io, DNS:*.ecn.io
    Signature Algorithm: ecdsa-with-SHA256
         ...omitted...
Enter fullscreen mode Exit fullscreen mode

If you're looking for easily verify your new X509 certificate on-the-go using your iOS mobile device, check out one of my projects: TLS Inspector. The worlds first Free & Libre X509 Certificate Inspector on iOS (GitHub).

Top comments (2)

Collapse
 
ecnepsnai profile image
Ian Spence

Hi, I'll try my best to keep this post up-to-date with API changes to libssl. If you come across any problems please do not hesitate to bring them up here, or give me a shout on Twitter, email, or whatever works.

Collapse
 
roger954 profile image
Roger954

Thanks, very clear and helpful.