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(std::span{buf}.first(readcount));
      hash2->update(std::span{buf}.first(readcount));
      hash3->update(std::span{buf}.first(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 Botan::secure_vector<uint8_t> key = Botan::hex_decode_locked("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)
   const auto iv = rng.random_vec<std::vector<uint8_t>>(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_view 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 Botan::secure_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)");
   const auto ct = enc.encrypt(pt, rng);

   // decrypt with sk
   Botan::PK_Decryptor_EME dec(*kp, rng, "OAEP(SHA-256)");
   const auto 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
   const auto group = Botan::EC_Group::from_name("secp521r1");
   Botan::ECDSA_PrivateKey key(rng, group);

   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.