OpenSSL 1.1 to Botan 3.x Migration

This aims to be a rough guide for migrating applications from OpenSSL 1.1 to Botan 3.x.

This guide attempts to be, but is not, complete. If you run into a problem while migrating code that does not seem to be described here, please open an issue on GitHub.

Note

The OpenSSL code snippets in this guide may not be 100% correct. They are intended to show the differences in using OpenSSL’s and Botan’s APIs rather to be a complete and correct example.

General Remarks

  • Botan is a C++ library, whereas OpenSSL is a C library

  • Botan also provides a C API for most of its functionality, but it is not a 1:1 mapping of the C++ API

  • With OpenSSL’s API, there are sometimes multiple ways to achieve the same result, whereas Botan’s API is more consistent

  • OpenSSL’s API is mostly underdocumented, whereas Botan targets 100% Doxygen coverage for all public API

  • It is often hard to find example code for OpenSSL, whereas Botan provides many examples and lots of test code.

X.509

Consider the following application code that uses OpenSSL to verify a certificate chain consisting of an end-entity certificate, two untrusted intermediate certificates, and a trusted root certificate.

#include <openssl/x509.h>
#include <openssl/pem.h>
#include <openssl/ssl.h>

int main() {
    // Create a new X.509 store
    X509_STORE *store = X509_STORE_new();

    // Load the root certificate
    FILE* rootCertFileHandle = fopen("root.crt", "r");
    X509* rootCert = PEM_read_X509(rootCertFileHandle, NULL, NULL, NULL);
    X509_STORE_add_cert(store, rootCert);
    fclose(rootCertFileHandle);

    // Create a new X.509 store context
    X509_STORE_CTX *ctx = X509_STORE_CTX_new();
    X509_STORE_CTX_init(ctx, store, NULL, NULL);

    // Load the intermediate certificates
    FILE* intermediateCertFileHandle1 = fopen("int2.crt", "r");
    FILE* intermediateCertFileHandle2 = fopen("int1.crt", "r");
    X509* intermediateCert1 = PEM_read_X509(intermediateCertFileHandle1, NULL, NULL, NULL);
    X509* intermediateCert2 = PEM_read_X509(intermediateCertFileHandle2, NULL, NULL, NULL);
    X509_STORE_CTX_trusted_stack(ctx, sk_X509_new_null());
    sk_X509_push(X509_STORE_CTX_get0_untrusted(ctx), intermediateCert1);
    sk_X509_push(X509_STORE_CTX_get0_untrusted(ctx), intermediateCert2);
    fclose(intermediateCertFileHandle1);
    fclose(intermediateCertFileHandle2);

    // Load the end-entity certificate
    FILE* endEntityCertFileHandle = fopen("ee.crt", "r");
    X509* endEntityCert = PEM_read_X509(endEntityCertFileHandle, NULL, NULL, NULL);
    X509_STORE_CTX_set_cert(ctx, endEntityCert);
    fclose(endEntityCertFileHandle);

    // Verify the certificate chain
    int result = X509_verify_cert(ctx);
    if(result != 1) {
        // Verification failed
        X509_STORE_CTX_free(ctx);
        X509_STORE_free(store);
        return -1;
    }

    // Verification succeeded
    X509_STORE_CTX_free(ctx);
    X509_STORE_free(store);
    return 0;
}

First, we create a new X509_STORE object and add the trusted root certificate. Then we add the intermediate certificates to the untrusted certificate stack. Finally, we set the end-entity certificate and call X509_verify_cert() to verify the whole certificate chain.

Here is the equivalent C++ code using Botan:

#include <botan/certstor_system.h>
#include <botan/x509cert.h>
#include <botan/x509path.h>

int main() {
   // Create a certificate store and add a locally trusted CA certificate
   Botan::Certificate_Store_In_Memory customStore;
   customStore.add_certificate(Botan::X509_Certificate("root.crt"));

   // Additionally trust all system-specific CA certificates
   Botan::System_Certificate_Store systemStore;
   std::vector<Botan::Certificate_Store*> trusted_roots{&customStore, &systemStore};

   // Load the end entity certificate and two untrusted intermediate CAs from file
   std::vector<Botan::X509_Certificate> end_certs;
   end_certs.emplace_back(Botan::X509_Certificate("ee.crt"));    // The end-entity certificate, must come first
   end_certs.emplace_back(Botan::X509_Certificate("int2.crt"));  // intermediate 2
   end_certs.emplace_back(Botan::X509_Certificate("int1.crt"));  // intermediate 1

   // Optional: Set up restrictions, e.g. min. key strength, maximum age of OCSP responses
   Botan::Path_Validation_Restrictions restrictions;

   // Optional: Specify usage type, compared against the key usage in end_certs[0]
   Botan::Usage_Type usage = Botan::Usage_Type::UNSPECIFIED;

   // Optional: Specify hostname, if not empty, compared against the DNS name in end_certs[0]
   std::string hostname;

   Botan::Path_Validation_Result validationResult =
      Botan::x509_path_validate(end_certs, restrictions, trusted_roots, hostname, usage);

   if(!validationResult.successful_validation()) {
      // call validationResult.result() to get the overall status code
      return -1;
   }

   return 0;  // Verification succeeded
}

First, we create a Certificate_Store_In_Memory object and add the trusted root certificate. Additionally, we use System_Certificate_Store to load all trusted root certificates from the operating system’s certificate store to trust. Botan provides several different Certificate Stores, including certificate stores that load certificates from a directory or from an SQL database. It even provides an interface for implementing your own certificate store. Then we add the end-entity certificate and the intermediate certificates to the end_certs chain. Optionally, we can set up path validation restrictions, specify usage and hostname for DNS, and then call x509_path_validate() to verify the certificate chain.

Random Number Generation

Consider the following application code to generate random bytes using OpenSSL.

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

int main() {
    unsigned char buffer[16]; // Buffer to hold 16 random bytes

    if(RAND_bytes(buffer, sizeof(buffer)) != 1) {
        std::cerr << "Error generating random bytes.\n";
        return 1;
    }

    // Print the random bytes in hexadecimal format
    for(int i = 0; i < sizeof(buffer); i++) {
        printf("%02X", buffer[i]);
    }
    printf("\n");

    return 0;
}

This example uses the RAND_bytes() function to generate 16 random bytes, e.g., for a 128-bit AES key, and prints it on the console.

Here is the equivalent C++ code using Botan:

#include <botan/auto_rng.h>
#include <botan/hex.h>
#include <iostream>

int main() {
    Botan::AutoSeeded_RNG rng;

    const Botan::secure_vector<uint8_t> buffer = rng.random_vec(16);

    // Print the random bytes in hexadecimal format
    std::cout << Botan::hex_encode(buffer) << std::endl;

    return 0;
}

This snippet uses the AutoSeeded_RNG class to generate the 16 random bytes. Botan provides different Random Number Generators, including system-specific as well as system-independent software and hardware-based generators, and a comprehensive interface for implementing your own random number generator, if required. AutoSeeded_RNG is the recommended random number generator for most applications. The random_vec() function returns the requested number of random bytes passed. Botan provides a hex_encode() function that converts the random bytes to a hexadecimal string.

Hash Functions

Consider the following application code to hash some data using OpenSSL.

#include <openssl/evp.h>
#include <openssl/sha.h>
#include <iostream>
#include <vector>

void printHash(EVP_MD_CTX* ctx, const std::string& name) {
    unsigned char hash[EVP_MAX_MD_SIZE];
    unsigned int lengthOfHash = 0;

    EVP_DigestFinal_ex(ctx, hash, &lengthOfHash);

    std::cout << name << ": ";
    for(unsigned int i = 0; i < lengthOfHash; ++i) {
        std::cout << std::hex << std::setw(2) << std::setfill('0') << (int)hash[i];
    }
    std::cout << std::endl;
}

int main() {
    EVP_MD_CTX *ctx1 = EVP_MD_CTX_new();
    EVP_MD_CTX *ctx2 = EVP_MD_CTX_new();
    EVP_MD_CTX *ctx3 = EVP_MD_CTX_new();

    EVP_DigestInit_ex(ctx1, EVP_sha256(), NULL);
    EVP_DigestInit_ex(ctx2, EVP_sha384(), NULL);
    EVP_DigestInit_ex(ctx3, EVP_sha3_512(), NULL);

    std::vector<uint8_t> buffer(2048);
    while(std::cin.good()) {
        std::cin.read(reinterpret_cast<char*>(buffer.data()), buffer.size());
        std::streamsize bytesRead = std::cin.gcount();

        EVP_DigestUpdate(ctx1, buffer.data(), bytesRead);
        EVP_DigestUpdate(ctx2, buffer.data(), bytesRead);
        EVP_DigestUpdate(ctx3, buffer.data(), bytesRead);
    }

    printHash(ctx1, "SHA-256");
    printHash(ctx2, "SHA-384");
    printHash(ctx3, "SHA-3-512");

    EVP_MD_CTX_free(ctx1);
    EVP_MD_CTX_free(ctx2);
    EVP_MD_CTX_free(ctx3);

    return 0;
}

This example uses the EVP_DigestInit_ex(), EVP_DigestUpdate(), and EVP_DigestFinal_ex() functions to hash data using SHA-256, SHA-384, and SHA-3-512. The printHash() function is used to print the hash values in hexadecimal format.

Here is the equivalent C++ code using Botan:

#include <botan/hash.h>
#include <botan/hex.h>

#include <iostream>

int main() {
   const auto hash1 = Botan::HashFunction::create_or_throw("SHA-256");
   const auto hash2 = Botan::HashFunction::create_or_throw("SHA-384");
   const auto hash3 = Botan::HashFunction::create_or_throw("SHA-3");
   std::vector<uint8_t> buf(2048);

   while(std::cin.good()) {
      // read STDIN to buffer
      std::cin.read(reinterpret_cast<char*>(buf.data()), buf.size());
      size_t readcount = std::cin.gcount();
      // update hash computations with read data
      hash1->update(buf.data(), readcount);
      hash2->update(buf.data(), readcount);
      hash3->update(buf.data(), readcount);
   }
   std::cout << "SHA-256: " << Botan::hex_encode(hash1->final()) << '\n';
   std::cout << "SHA-384: " << Botan::hex_encode(hash2->final()) << '\n';
   std::cout << "SHA-3: " << Botan::hex_encode(hash3->final()) << '\n';
   return 0;
}

This example uses the HashFunction interface to hash data using SHA-256, SHA-384, and SHA-3-512. The hash() function is used to hash the data and the output_length() function is used to determine the length of the hash value. Botan provides a comprehensive list of hash functions, including all SHA-2 and SHA-3 variants, as well as message authentication codes and key derivation functions.

Symmetric Encryption

Consider the following application code to encrypt some data with AES using OpenSSL.

#include <openssl/aes.h>
#include <openssl/evp.h>
#include <iostream>
#include <iomanip>

int main() {
    // Hex-encoded key and plaintext block
    const char* key_hex = "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F";
    const char* plaintext_hex = "00112233445566778899AABBCCDDEEFF";

    // Convert hex-encoded key and plaintext block to binary
    unsigned char key[32], plaintext[16];
    for(int i = 0; i < 32; i++) {
        sscanf(&key_hex[i*2], "%02x", &key[i]);
    }
    for(int i = 0; i < 16; i++) {
        sscanf(&plaintext_hex[i*2], "%02x", &plaintext[i]);
    }

    // Encrypt
    unsigned char ciphertext[16], iv_enc[AES_BLOCK_SIZE] = {0};
    EVP_CIPHER_CTX *ctx_enc = EVP_CIPHER_CTX_new();
    EVP_EncryptInit_ex(ctx_enc, EVP_aes_256_cbc(), NULL, key, iv_enc);
    int outlen1;
    EVP_EncryptUpdate(ctx_enc, ciphertext, &outlen1, plaintext, sizeof(plaintext));
    EVP_EncryptFinal_ex(ctx_enc, ciphertext + outlen1, &outlen1);

    // Print ciphertext in hexadecimal format
    for(int i = 0; i < 16; i++) {
        printf("%02X", ciphertext[i]);
    }
    printf("\n");

    return 0;
}

This example uses the EVP_EncryptInit_ex(), EVP_EncryptUpdate(), and EVP_EncryptFinal_ex() functions to encrypt a 128-bit plaintext block with a 256-bit key using AES. The key and plaintext block are hex-decoded and converted to binary before encryption.

Here is the equivalent C++ code using Botan:

#include <botan/auto_rng.h>
#include <botan/cipher_mode.h>
#include <botan/hex.h>
#include <botan/rng.h>

#include <iostream>

int main() {
   Botan::AutoSeeded_RNG rng;

   const std::string plaintext(
      "Your great-grandfather gave this watch to your granddad for good "
      "luck. Unfortunately, Dane's luck wasn't as good as his old man's.");
   const std::vector<uint8_t> key = Botan::hex_decode("2B7E151628AED2A6ABF7158809CF4F3C");

   const auto enc = Botan::Cipher_Mode::create_or_throw("AES-128/CBC/PKCS7", Botan::Cipher_Dir::Encryption);
   enc->set_key(key);

   // generate fresh nonce (IV)
   Botan::secure_vector<uint8_t> iv = rng.random_vec(enc->default_nonce_length());

   // Copy input data to a buffer that will be encrypted
   Botan::secure_vector<uint8_t> pt(plaintext.data(), plaintext.data() + plaintext.length());

   enc->start(iv);
   enc->finish(pt);

   std::cout << enc->name() << " with iv " << Botan::hex_encode(iv) << " " << Botan::hex_encode(pt) << '\n';
   return 0;
}

This example uses the CipherMode interface to encrypt a 128-bit plaintext block with a 256-bit key using AES in CBC mode with PKCS#7 padding. The set_key() function is used to set the key and the start() and finish() functions are used to encrypt the plaintext block.

To learn more about the BlockCipher and CipherMode interfaces, including a list of all available block ciphers and cipher modes, see the Block Ciphers and Cipher Modes handbook sections.

Asymmetric Encryption

Consider the following application code to encrypt some data with RSA using OpenSSL.

#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include <openssl/err.h>
#include <string.h>
#include <stdio.h>

int main() {
    // Load public key
    FILE* pubKeyFile = fopen("public.pem", "r");
    if(pubKeyFile == NULL) {
        fprintf(stderr, "Error opening public key file.\n");
        return 1;
    }
    EVP_PKEY* pubKey = PEM_read_PUBKEY(pubKeyFile, NULL, NULL, NULL);
    fclose(pubKeyFile);

    // Load private key
    FILE* privKeyFile = fopen("private.pem", "r");
    if(privKeyFile == NULL) {
        fprintf(stderr, "Error opening private key file.\n");
        return 1;
    }
    EVP_PKEY* privKey = PEM_read_PrivateKey(privKeyFile, NULL, NULL, NULL);
    fclose(privKeyFile);

    // String to encrypt
    unsigned char* plaintext = "Your great-grandfather gave this watch to your granddad for good luck. Unfortunately, Dane's luck wasn't as good as his old man's.";
    size_t plaintext_len = strlen(plaintext);

    // Encrypt
    EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new(pubKey, NULL);
    EVP_PKEY_encrypt_init(ctx);
    EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING);
    EVP_PKEY_CTX_set_rsa_oaep_md(ctx, EVP_sha256());
    size_t encrypted_len;
    EVP_PKEY_encrypt(ctx, NULL, &encrypted_len, plaintext, plaintext_len);
    unsigned char* encrypted = (unsigned char*)malloc(encrypted_len);
    EVP_PKEY_encrypt(ctx, encrypted, &encrypted_len, plaintext, plaintext_len);

    // Decrypt
    EVP_PKEY_CTX *ctx2 = EVP_PKEY_CTX_new(privKey, NULL);
    EVP_PKEY_decrypt_init(ctx2);
    EVP_PKEY_CTX_set_rsa_padding(ctx2, RSA_PKCS1_OAEP_PADDING);
    EVP_PKEY_CTX_set_rsa_oaep_md(ctx2, EVP_sha256());
    size_t decrypted_len;
    EVP_PKEY_decrypt(ctx2, NULL, &decrypted_len, encrypted, encrypted_len);
    unsigned char* decrypted = (unsigned char*)malloc(decrypted_len + 1);
    EVP_PKEY_decrypt(ctx2, decrypted, &decrypted_len, encrypted, encrypted_len);
    decrypted[decrypted_len] = '\0';

    // Print encrypted and decrypted strings
    for(size_t i = 0; i < encrypted_len; i++) {
        printf("%02X", encrypted[i]);
    }
    printf("\n");
    printf("%s\n", decrypted);

    // Clean up
    EVP_PKEY_free(pubKey);
    EVP_PKEY_free(privKey);
    EVP_PKEY_CTX_free(ctx);
    EVP_PKEY_CTX_free(ctx2);
    free(encrypted);
    free(decrypted);

    return 0;
}

This example uses OpenSSL’S EVP interface, specifically EVP_PKEY_encrypt() and EVP_PKEY_decrypt() functions to encrypt and decrypt a string using RSA. The public and private keys are loaded from files. The EVP_PKEY_CTX_set_rsa_padding() and EVP_PKEY_CTX_set_rsa_oaep_md() functions are used to set the padding scheme and the hash function for RSA-OAEP.

Here is the equivalent C++ code using Botan:

#include <botan/auto_rng.h>
#include <botan/hex.h>
#include <botan/pk_keys.h>
#include <botan/pkcs8.h>
#include <botan/pubkey.h>
#include <botan/rng.h>

#include <iostream>

int main(int argc, char* argv[]) {
   if(argc != 2) {
      return 1;
   }
   std::string plaintext(
      "Your great-grandfather gave this watch to your granddad for good luck. "
      "Unfortunately, Dane's luck wasn't as good as his old man's.");
   std::vector<uint8_t> pt(plaintext.data(), plaintext.data() + plaintext.length());
   Botan::AutoSeeded_RNG rng;

   // load keypair
   Botan::DataSource_Stream in(argv[1]);
   auto kp = Botan::PKCS8::load_key(in);

   // encrypt with pk
   Botan::PK_Encryptor_EME enc(*kp, rng, "OAEP(SHA-256)");
   std::vector<uint8_t> ct = enc.encrypt(pt, rng);

   // decrypt with sk
   Botan::PK_Decryptor_EME dec(*kp, rng, "OAEP(SHA-256)");
   Botan::secure_vector<uint8_t> pt2 = dec.decrypt(ct);

   std::cout << "\nenc: " << Botan::hex_encode(ct) << "\ndec: " << Botan::hex_encode(pt2);

   return 0;
}

This example uses the PK_Encryptor_EME and PK_Decryptor_EME classes to encrypt and decrypt. a message using RSA. The public and private keys are loaded from files. The padding scheme and hash function are passed as a string parameter.

Asymmetric Signatures

Consider the following application code to sign some data with ECDSA using OpenSSL.

#include <openssl/ec.h>
#include <openssl/obj_mac.h>
#include <openssl/err.h>
#include <openssl/ecdsa.h>
#include <openssl/pem.h>
#include <openssl/sha.h>
#include <iostream>

int main() {
    EC_KEY *ec_key = EC_KEY_new_by_curve_name(NID_secp521r1);

    if(ec_key == NULL) {
        fprintf(stderr, "Error creating EC_KEY structure.\n");
        return 1;
    }

    if(!EC_KEY_generate_key(ec_key)) {
        fprintf(stderr, "Error generating key.\n");
        ERR_print_errors_fp(stderr);
        EC_KEY_free(ec_key);
        return 1;
    }

    // String to sign
    std::string plaintext = "This is a tasty burger!";

    // Hash the plaintext
    unsigned char hash[SHA256_DIGEST_LENGTH];
    SHA256((unsigned char*)plaintext.c_str(), plaintext.size(), hash);

    // Sign the hash
    ECDSA_SIG* sig = ECDSA_do_sign(hash, SHA256_DIGEST_LENGTH, ec_key);
    if(sig == NULL) {
        std::cerr << "Error signing: " << ERR_error_string(ERR_get_error(), NULL) << "\n";
        return 1;
    }

    // Print the signature
    const BIGNUM* r;
    const BIGNUM* s;
    ECDSA_SIG_get0(sig, &r, &s);
    char* r_hex = BN_bn2hex(r);
    char* s_hex = BN_bn2hex(s);
    std::cout << "Signature: (" << r_hex << ", " << s_hex << ")\n";

    // Clean up
    EC_KEY_free(ec_key);
    ECDSA_SIG_free(sig);
    OPENSSL_free(r_hex);
    OPENSSL_free(s_hex);
    return 0;
}

This snippet uses OpenSSL’s ECDSA interface, specifically ECDSA_do_sign(), to sign a string message using ECDSA. The private key is loaded from a file. The SHA256() function is used to hash the plaintext before signing.

Here is the equivalent C++ code using Botan:

#include <botan/auto_rng.h>
#include <botan/ec_group.h>
#include <botan/ecdsa.h>
#include <botan/hex.h>
#include <botan/pubkey.h>

#include <iostream>

int main() {
   Botan::AutoSeeded_RNG rng;
   // Generate ECDSA keypair
   Botan::ECDSA_PrivateKey key(rng, Botan::EC_Group("secp521r1"));

   const std::string message("This is a tasty burger!");

   // sign data
   Botan::PK_Signer signer(key, rng, "SHA-256");
   signer.update(message);
   std::vector<uint8_t> signature = signer.signature(rng);
   std::cout << "Signature:\n" << Botan::hex_encode(signature);

   // now verify the signature
   Botan::PK_Verifier verifier(key, "SHA-256");
   verifier.update(message);
   std::cout << "\nis " << (verifier.check_signature(signature) ? "valid" : "invalid");
   return 0;
}

This example uses the PK_Signer and PK_Verifier classes to sign and verify a message using ECDSA. The private key is similary loaded from a file. The hash function is passed as a string parameter. PK_Verifier::check_signature() is used to verify the signature.